s.fujitaの記事一覧

GoのおすすめDIツール

はじめに

これまでラクスルではRubyを開発言語として採用することが多かったのですが、最近はコマンドラインツールやバッチ処理などでGoによる開発も増えています。
私が最近取り組んでいる印刷発注基盤の刷新プロジェクトでもGoを使ってWEBアプリケーションを開発しており、社内では先例がないこともあって色々と苦労しながらも楽しく開発を進めています。
GoによるWEBアプリケーション開発では、RubyにおけるRailsのようなデファクトスタンダードは存在しないため考慮すべき点がたくさんあります。
例えば、「パッケージ構成をどうするか」「WEBアプリケーションフレームワークを使うべきか」などですが今回はちょっと軽めのテーマとしてDIツールについて紹介します。

DIとは

DIとはDependency Injectionの略で「依存性の注入」と訳されます。
Goではインタフェース型の値をコンスタラクタの引数などで渡すことでDIを実現することが多いと思います。
具体的なコードを見てみましょう。

type DogUsecase struct {
	repo DogRepository
}

func NewDogUsecase(dogRepo DogRepository) DogUsecase {
	return DogUsecase{
		repo: dogRepo,
	}
}

func (u DogUsecase) All() []Dog {
	return u.repo.All()
}

type DogRepository interface {
	All() []Dog
}

type dogRepositoryImpl struct{}

func NewDogRepository() DogRepository {
	return dogRepositoryImpl{}
}

func (r dogRepositoryImpl) All() []Dog {
	// DBからDogを取得
}

func main() {
	// インターフェース型の値を生成
	repo := NewDogRepository()
	// コンストラクタの引数として値を渡す
	usecase := NewDogUsecase(repo)
	usecase.All()
}

上記ではDogUsecaseのAll()メソッド内でDogRepositoryを使っています。
そして、コンストラクタであるNewDogUsecaseの引数としてDogRepositoryを渡しています。
ポイントは下記の2点です。

  • DogUsecaseの内部でDogRepositoryを生成しているのではなく外から渡している
  • DogRepositoryをインターフェースにしている

DogUsecaseDogRepositoryというインターフェースにのみ依存しており、その具象型(dogRepositoryImpl)には依存していません。
そのため、DogUsecaseの単体テストを書く時にDogRepositoryの具象型をモックに変更することが可能です。
また、DogRepositoryの具象型の実装に変更(データの取得元をDBではなく外部APIに変えるなど)があった場合でも、インターフェースが変わらない限りはDogUsecaseのコードを修正する必要がありません。
このように、DIを使うことによってDBや外部APIといった実装の詳細を利用者側から隠蔽し、モジュール間を疎結合にすることで保守性やテスタビリティを高めることができます。

DIツール(dig)の紹介

上述したコードではmain関数の中でDIを行なっていました。
今回のようなシンプルなコードの場合はこれでも問題ないのですが、アプリケーションが大きくなっていくとDIのためのコードが肥大化しメンテナンスしづらくなります
そこで、これらのコードを自動化するためのDIツールが役に立ちます。
GoのDIツールはいくつかありますが、現在私たちのプロジェクトではuber-go/digを使っています。
digの特徴は何といってもシンプルで学習コストが低い点です。また、少し複雑なDIもやろうと思えばできる柔軟性も備えています。
先ほどのコードの中でDIしていた箇所をdigを使って書き直すとこのようになります。

func main() {
  container := dig.New()
  container.Provide(NewDogRepository)
  container.Provide(NewDogUsecase)
  container.Invoke(func(usecase DogUsecase) {
    usecase.All()
  })
}

dig.New()でDIコンテナを作成し、Provideでコンテナに型を登録します。
Provideの引数は、「返り値として登録したい型を返す関数」です。
例えば、NewDogRepositoryは返り値としてDogRepository型を返す関数なので、Provideの引数として渡すことによってDogRepository型をコンテナに登録することができます。
登録した型の値を取り出したい場合には、Invokeを使用します。Invokeの引数は、「利用したい型を引数にした関数」です。
ここでは、DogUsecase型を使ってAll()メソッドを実行したいので、DogUsecase型を引数とする関数がInvokeの引数になっています。
注目していただきたいのは、container.Provide(NewDogUsecase)という行です。
NewDogUsecaseは引数としてDogRepositoryを必要としますが、この行ではDogRepositoryを渡していません。
一つ前の行のcontainer.Provide(NewDogRepository)によってDogRepository型をコンテナに登録しているため、Invokeの実行時には自動でNewDogRepositoryの引数であるDogRepository型の値をコンテナから取得することができます。

一見するとDIツールを使わなかったコードの方が短くてシンプルに思えるかもしれませんが、アプリケーションが成長してモジュールが増え依存関係も複雑になっていくと、手動でDIをする必要がなくコンストラクタの引数によって自動的にDIできることのメリットは大きくなります。

digの応用的な使い方

digを使った場合、基本的には型をベースにしてDIが実行されますが、時には同じ型で別の値を使いたいというケースもあると思います。
例として、MasterとSlaveのDBを同じRepository内で使いたいというケースを考えてみます。

func NewMasterConnection() (*sql.DB, error)
func NewSlaveConnection() (*sql.DB, error)

type DBConnection struct {
  dig.In

  MasterConn *sql.DB `name:"master"`
  SlaveConn  *sql.DB `name:"slave"`
}

func NewDogRepository(c DBConnection) DogRepository {
  return dogRepositoryImpl{
    master: c.MasterConn,
    slave:  c.SlaveConn,
  }
}

func main() {
  container := dig.New()
  container.Provide(NewMasterConnection, dig.Name("master"))
  container.Provide(NewSlaveConnection, dig.Name("slave"))
  container.Provide(NewDogRepository)
}

上記では、NewMasterConnectionNewSlaveConnectionProvideの引数として渡す際にdig.Nameを使って名前付けしています。
そして、NewDogRepositoryではDBConnectionという構造体のフィールドの中で`name:”master”``name:”slave”`といったタグを付ける事で同じ型(*sql.DB)の中でdig.Nameに対応する値を取得することができます。
このように、digは少し複雑なDIをする場合も柔軟に対応できます。
今回紹介した機能の他にもオプショナルな型であったり、同じ型の異なる値をグループ化したりといったこともできますので、詳しくは公式ドキュメントをご参照ください。

まとめ

  • ラクスルでもGoで開発している!
  • DIはモジュール間の結合度を下げて保守性やテスタビリティを高める
  • 手動DIはアプリケーションが大きくなるとつらいのでDIツールを使うのがオススメ
  • uber-go/digというDIツールが便利

宛名印刷ができる年賀状・喪中はがきをリリースしました

はじめに

ラクスルでエンジニアをしている藤田です。
先月、「年賀状・喪中はがき」という商品をリリースしました。
ラクスルで扱っている印刷サービスは基本的に、PDF等でアップロードされた印刷データを対象の商品(チラシや名刺など)に印刷してお客様へお届けするというものです。
今回リリースした商品は宛名印刷という機能があり、印刷データと一緒に宛名リストをアップロードしてもらい、印刷データと共にそれぞれの宛名をはがきに印刷します。他の商品では全く同じ印刷物が注文された部数だけ出来上がるのに対して、宛名印刷では一つ一つの印刷物が宛名の箇所だけ異なる内容になる点が特徴です。
一見するとシンプルに思われるこの機能ですが、実際に開発してみると意外と奥が深く面白かったので紹介したいと思います。

宛名印刷の難しさ

宛名印刷の難しさは「どうやって宛名を違和感なく宛名面にマッピングするか」にあります。組版処理とも呼ばれます。
下の画像を見ていただくと伝わるかと思うのですが、役職・部署の有無や文字数などで宛名面のどこに印刷するかが大きく変わります。
そのため、様々なパターンを考慮する必要があり必然的に開発も難しくなります。

今回はこのよう組版処理を実装するにあたって、開発スピード等を考慮して既成の組版ソフトを利用する方針としました。このソフトはWindows上でしか動作しないため、別途windowsサーバーをたてる必要がありました。

システム概要

少し簡略化していますが、組版処理に関連するシステムの概要図は上記のようになっており、次の通り処理されていきます。上述の組版ソフトは⑥で使用しており、SQSからキューを取得したりS3にアップロードしたりといった周辺の処理はWindowsとも相性が良いGolangで開発しました。

1. ユーザーが印刷データと宛名リスト(CSV)をアップロード
2. 印刷データと宛名リストをS3に保存
3. SQSに組版処理をエンキュー
4. SQSからキューを取得
5. S3から印刷データと宛名リストを取得
6. 組版処理を実施
7. 処理結果ファイルをS3に保存

Webアプリケーションと組版ソフトが動作するWindows Serverを繋げる方法として、Amazon SQSを利用しました。Windows Server上にAPIサーバーをたてる方法もありますが、やや開発コストが増える点と、宛名リストの件数によっては組版ソフトの処理に時間がかかるためキューイングが必要だったこともあり、SQSを採用しました。

マニアックな機能: CIDチェック

今回、開発した機能の中で面白かったものがCIDチェックです。CIDとはアドビ社が定めている文字ごとに一意に振られる番号のことで、フォントの文字を識別するために用いられます。例えば、フォントでは異体字の字形を区別する必要がありますが、Unicode等の一般的な文字コードでは同じコードが割り当てられていることがあるため、CIDが利用されます。
宛名印刷では、宛名に用いられるフォントとして正楷書CB1、リュウミンなど4種類から選択することができます。これらのフォントはカバーしているCIDの範囲以外の文字には使用することができないため、宛名リストで入力された文字が範囲内に含まれているかどうかをチェックすることが必要です。
CIDチェックの実装方法ですが、

1. 入力された文字に紐づくCIDを取得する
2. 取得したCIDが範囲内に含まれるかチェック

という順序でチェックをしていきます。
私が探した範囲ではRubyのライブラリ(CIDチェックはRailsアプリ内で行います)で文字列からCIDを取得するようなものはなかったため、UTF-8のコードとCIDをマッピングしたファイルを作成してUTF-8からCIDを引けるようにしました。
幸い、アドビ社が提供しているcid2code.txtというファイルにCIDと主な文字コードとの対応表がありましたので、このファイルを利用することでマッピングファイルを作成することができました。
細かい話になりますが、最終的に実装は下記のようになりました。

# app/validators/adobe_japan13_validator.rb

class AdobeJapan13Validator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return if value.nil?
    chars = invalid_chars(value)
    record.errors.add(attribute, :invalid_character, invalid_chars: chars.uniq.join) if chars.present?
  end

  class << self
    def valid_strings_map
      return @valid_strings_map if @valid_strings_map
      @valid_strings_map = {}
      File.open(Rails.root.join('config', 'adobe_japan13.dat'), 'rb') do |f|
        f.each_line do |line|
          line.chomp!
          _cid, utf8 = line.split("\t")
          @valid_strings_map[utf8] = true
        end
      end
      @valid_strings_map
    end
  end

  private

  def invalid_chars(target_string)
    target_string.each_char.reject do |c|
      utf8 = c.unpack1('H*')
      self.class.valid_strings_map.key?(utf8)
    end
  end
end

バリデーターの中で読み込んでいる adobe_japan13.dat がマッピングファイルになっており、次のような内容になっています。左側の数字がCID、右側がUTF-8に対応しています。

# config/adobe_japan13.dat
1	20
2	21
3	22
4	23
5	24
6	25
7	26
8	27
9	28
10	29
11	2a
12	2b
13	2c
14	e28091
# 以下略

おわりに

この記事では宛名印刷という開発案件について紹介しました。
組版処理やCIDチェックといったものは印刷サービスならではで、他の会社ではあまり経験できないラクスルらしいテーマを紹介できたかなと思っています。