元ネタ:SwiftでもMinimal Cake Pattern
ほぼ上記投稿で解説されているとおりなのですが、こちらの投稿ではMixinモジュールの生成方法にclassを利用しています。
Mixinモジュールをclassで定義してしまうと、Mixinモジュールを注入したいオブジェクト内部でMixinモジュールのインスタンス化を行う必要があり、Mixinモジュールが注入先に依存してしまっているのが気になりました。
こちらの投稿ではMixinモジュールにProtocolを利用する方法で注入先がMixinモジュールの初期化に関心を持たないようにしてみようと思います。
Loggerインターフェース
Loggerインターフェースを注入するパターンとして考えてみます。
Loggerにはそれぞれ、SystemLoggerとTextLoggerという具象クラスが存在するものとします。
protocol Logger {
func write(_ message: String)
}
struct SystemLogger: Logger {
func write(_ message: String) {
print("System: " + message)
}
}
struct TextLogger: Logger {
func write(_ message: String) {
// 本当はローカルのテキストファイルに書き込む処理にするべきだが、コード量が増えるため標準出力への書き込みとしている。
print("Text: " + message)
}
}
モジュールを提供するためのインターフェース
モジュールを提供するためのインターフェースを定義します。
今回注入したいのはLoggerですので、UsesLoggerと命名します。
MixinモジュールをProtocolで定義する都合、プロパティはGetOnlyとします。
protocol UsesLogger {
var logger: Logger { get }
}
Mixinモジュール
それぞれ、SystemLoggerとTextLoggerを提供する方法を定義したモジュールを定義します。
protocol MixinSystemLogger: UsesLogger {}
extension MixinSystemLogger {
var logger: Logger { return SystemLogger() }
}
protocol MixinTextLogger: UsesLogger {}
extension MixinTextLogger {
var logger: Logger { return TextLogger() }
}
依存を注入するクライアント
クライアントオブジェクトに対して、注入したいMixinモジュールを定義すれば完了です。
Protocolを適宜追加するだけで複数の依存を注入することも出来ます。
処理を差し替えたいときは、UsesLoggerに適合したMixinモジュールを差し替えてあげれば良く、間違えて同じUsesLoggerに適合したMixinモジュールを指定した場合はコンパイルエラーとなるので安全でもあります。
struct SystemLoggerClient: MixinSystemLogger {
func test() {
logger.write("test")
}
}
struct TextLoggerClient: MixinTextLogger {
func test() {
logger.write("test")
}
}
let systemLoggerClient = SystemLoggerClient()
systemLoggerClient.test() // "System: test"
let textLoggerClient = TextLoggerClient()
textLoggerClient.test() // "Text: test"
ちなみに、Mixinモジュールは毎回インスタンスの生成が行われれしまうのでパフォーマンスが気になるときはクライアント側に依存オブジェクトをキャッシュするプロパティを用意しておくと良いです(この場合、オブジェクトはclassで定義しないとならない)。
final class SystemLoggerClient: MixinSystemLogger {
private lazy var _logger: Logger = self.logger
func test() {
_logger.write("test")
}
}
テスト
Swiftではextensionにwhere制約を設けることで多態性を表現することが出来ます。
この機能を利用して、MixinモジュールがMockオブジェクトになるようにします。
// Mockとして差し替えたいLoggerオブジェクト
private struct MockLogger: Logger {
func write(_ message: String) {
print("Mock: " + message)
}
}
// Mockに差し替えるためのマーカープロトコルを定義
private protocol Mock {}
// Mixin先オブジェクトがMockの抽象ならMock用Loggerを返すようにする
private extension MixinSystemLogger where Self: Mock {
var logger: Logger { return MockLogger() }
}
// テストしたいクライアントにMockをのプロトコルを適用することでMockオブジェクトに差し替える事ができる
private extension SystemLoggerClient: Mock {}
let mockClient = SystemLoggerClient()
mockClient.test() // "Mock: test"
ということで、テストまで含めてSwiftならDIを静的に解決出来ました。
ソースコードはGistにおいてあります。
実際に何らかのプロジェクトでこの方法を試したわけではないので有用性についてはそれほど考慮できてはいないですし、問題点についても洗い出せていませんのでこれを読んで皆様色々と考えてみてくれると嬉しい限りでございます。