はじめに
Minimal Cake Pattern1とは、株式会社ドワンゴの一部で採用されているDI(Dependency Injection)の手法である。このMinimal Cake Patternは主にScalaで行われている手法であるが、この記事ではこの手法をSwiftに移植することを目指す。
この記事で紹介したソースコードは次のリポジトリに置かれている。
もしこの記事を読んで、疑問や改善するべき点を見つけた場合は、気軽にコメントなどで指摘して欲しい。
Minimal Cake Patternとは?
こちらの記事に詳細な解説があるので、まずはこちらを読んでいただきたいが、DIについての知識がある場合は飛ばしてもよい。
Scalaにおける最適なDependency Injectionの方法を考察する 〜なぜドワンゴアカウントシステムの生産性は高いのか〜
SwiftによるMinimal Cake Pattern
例として「ファイルを読み込んで、その内容と読み込んだ時刻を合せて返すサービス」の作成を例に説明していく。また、もしファイルの読み込みに失敗した場合は、適切なログを出す必要があるものとする。
時刻を返す
現在の時刻を単純に取得してしまうと、その部分のテストを作る時に、テストを実行する時間によっては成功したり失敗したりする不味いテストになる可能性がある。そこで、時間を取得するインターフェースを作り、DIができるようにする。
protocol Clock {
func now() -> NSDate
}
このインターフェースはnow
というメソッドを持ち、このメソッドが時間を返すものとなっている。これを用いて、現在の時刻を返す実装と、テスト用の実装を次のようにそれぞれ定義する。
class MixInSystemClock: Clock {
func now() -> NSDate {
return NSDate()
}
}
class MixInMockClock: Clock {
let date: NSDate
init(_ str: String) {
let inputFormatter = NSDateFormatter()
inputFormatter.dateFormat = "yyyy-MM-dd"
date = inputFormatter.dateFromString(str)!
}
func now() -> NSDate {
return date
}
}
SystemClock
は現在の時刻を返すようになっているが、MockClock
は引数で時刻を受け取り、now
メソッドは必ずそれを返すようになっている。
次に、このClock
を用いることを示すインターフェースを次のように定義する。
protocol UsesClock {
var clock: Clock { get }
}
ログを出力する
ログは、ファイルに出力するべき時もある一方で、テストの際はファイルIOの失敗によりテストが失敗することを防ぐために、ロガーが呼ばれたことだけをチェックし、ログは標準出力に出せば十分であることがある。このように、いろいろな実装が考えられることから、ロガーについてもDIできた方がよい。Clock
と同様に、まずはロガーのインターフェースを次のように定義する。
protocol Logger {
func info(m: String) -> Void
func error(m: String) -> Void
}
そして、今回は標準出力にログを出力する実装だけを用意する。
class MixInPrintLogger: Logger {
func info(m: String) -> Void {
print("Info: " + m)
}
func error(m: String) -> Void {
print("Error: " + m)
}
}
最後に、このロガーを使うことを表すインターフェースを定義する。
protocol UsesLogger {
var logger: Logger { get }
}
ファイルを読み込むサービス
さて、時刻を取得する部分とログを出す部分が完成したので、次はファイルを読み込むサービスを作る。まず、このサービスはファイルを読み込んで、内容と時刻を返すメソッドreadWithDate
を持つことを示すインターフェースを定義する2。
protocol ReadFileService: UsesClock, UsesLogger {
func readWithDate(fileName: String) -> Optional<(NSDate, String)>
}
このインターフェースはUsesClock
とUsesLogger
を実装しなければならないので、このインターフェースの実装にもしDIが行われていなければコンパイルに失敗することになる。Minimal Cake Patternのような静的なDIは、このようにDIに失敗したことをコンパイルタイムに教えてくれるというメリットがある。そして、readWithDate
メソッドの内容を次のように与える。
extension ReadFileService {
func readWithDate(fileName: String) -> Optional<(NSDate, String)> {
if let dir : NSString = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.AllDomainsMask, true).first {
let pathFileName = dir.stringByAppendingPathComponent(fileName)
logger.info(pathFileName)
do {
let text = try NSString(contentsOfFile: pathFileName, encoding: NSUTF8StringEncoding)
return (clock.now(), text as String)
} catch {
logger.error("fail to read the file!")
return Optional.None
}
} else {
logger.error("fail to search directory to read the file!")
return Optional.None
}
}
}
このメソッドの中では、UsesClock
とUsesLogger
により、時刻の取得とログの出力が抽象的に行える。さて、これに具体的な実装をDIすると、次のようになる。
class MixInReadFileService: ReadFileService {
var clock: Clock = MixInSystemClock()
var logger: Logger = MixInPrintLogger()
}
class MixInReadFileServiceTest: ReadFileService {
var clock: Clock = MixInMockClock("2016-06-18")
var logger: Logger = MixInPrintLogger()
}
このように、デフォルトの実装であるMixInReadFileService
では現在の時刻を返すMixInSystemClock
がDIされている。一方で、テストで使うMixInReadFileServiceTest
はMixInMockClock
で常に現在時刻が2016年6月18日となるようにしている。これで、テストが時間によって成功したり失敗したりするという事態を回避することができる。
ReadFileService
を使うMainService
さて、ReadFileService
を使ってtest.txt
の中身を表示MainService
を考える。次のように、まずはインターフェースを定義する。
protocol MainService: UsesReadFileService {
func main() -> Void
}
そして、メソッドの内容を与える。
extension MainService {
func main() -> Void {
let opt = readFileService.readWithDate("test.txt")
_ = opt.map({(f: (NSDate, String)) -> Void in
print(f.0)
print(f.1)
})
}
}
そして、具体的な実装をDIして終了である。
class MixInMainService: MainService {
var readFileService: ReadFileService = MixInReadFileService()
}
MainService
を使う
MainService
の実装であるMixInMainService
を次のように使う。
MixInMainService().main()
もしtest.txt
が存在する場合は、次のような出力が得られる。
Info: /Users/hikaru_yoshimura/Documents/test.txt
2016-06-18 10:24:20 +0000
foobar
hogehoge
存在しない場合、次のようにエラーログが出力される。
Info: /Users/hikaru_yoshimura/Documents/test.txt
Error: fail to read the file!
まとめ
このように、Minimal Cake PatternをSwiftでも用いることができた。DIについて、Swift界隈でも議論が起きればよいと思う。
参考文献
- Scalaにおける最適なDependency Injectionの方法を考察する 〜なぜドワンゴアカウントシステムの生産性は高いのか〜
- Mix-in injection における最強のテスト用インスタンス構築パターン
- Minimal Cake Pattern のお作法
- Swift2で静的なDIを実現する謎のソースコード