Factory Methodについて
インスタンスの生成方法をスーパークラス(Swiftではプロトコルの場合も)で定め、具体的な処理をサブクラスで行うパターンをFactory Methodパターンと呼ぶ。
オブジェクトの生成と具体的な処理を分離することで、大幅に可読性が上がり、テストの柔軟性も向上する。
本記事では、上記のメリットを、Swiftのコードを用いて体感してみる。
Clean ArchitectureにFactory Methodパターンを適用してみる。今回はDomain層からPresentation層にデータを流す部分に焦点を当てる。
Domain層
Entity
struct Message: Identifiable {
var id: String {
description
}
var description: String
var email: String
}
上記のようなMessageを取得し、画面に表示するコードを書いてみる。
UseCase
protocol MessageUseCase {
var messages: [Message] {get set}
}
class MessageUseCaseImpl: MessageUseCase {
var messages: [Message]
init() {
self.messages = [
.init(
description: "hoge",
email: "hoge@gmail.com"
),
.init(
description: "fuga",
email: "fuga@gmail.com"
)
]
}
}
本来は、Data層からMessageを取得するので、UseCaseで状態管理などの実装が必要であるが、今回は諸々省略。
Presentation層
Presenter
class MessagePresenter: ObservableObject {
@Published var messages: [Message]
init(messages: [Message] = []) {
self.messages = messages
}
}
class MessagePresenterImpl: MessagePresenter {
let useCase: MessageUseCase
init(useCase: MessageUseCase) {
self.useCase = useCase
super.init(messages: useCase.messages)
}
}
View
import SwiftUI
struct MessageView: View {
@StateObject var presenter: MessagePresenter
var body: some View {
List(presenter.messages) { message in
Text(message.description)
}
}
}
Preview
private extension MessagePresenter {
static var mock: MessagePresenter {
.init(messages: [
.init(description: "Factory", email: "Factory"),
.init(description: "Factory", email: "Factory")
])
}
}
#Preview("Presenter") {
MessageView(presenter: MessagePresenter.mock)
}
この実装でも、一見正しくモックは差し込めているが、もしメッセージについての画面以外も多数実装する必要がある、あるいは可能性がある状況であったり、複雑なロジックで生成された動的なPresenterのモックがいくつか必要な場合、より良い実装ができる。
Factory
protocol PresenterFactory {
func getMessagePresenter() -> MessagePresenter
// 以下にMessage以外のPresenterの生成も書ける
}
class PresenterFactoryImpl: PresenterFactory {
func getMessagePresenter() -> MessagePresenter {
MessagePresenterImpl(useCase: MessageUseCaseImpl())
}
// 以下にMessage以外のPresenterの生成も書ける
}
このように、MessagePresenterのオブジェクトを返すメソッドを定義する。こうすることでPresenterの生成ロジックを他のViewでも共有でき、コードの可読性が向上する。
また、様々なUseCaseをPresenterに注入できるため、テストの柔軟性も向上する。
struct MessageFactoryPatternView: View {
@StateObject var presenter: MessagePresenter
// viewFactoryを受け取るイニシャライザを追加
init(viewFactory: MessagePresenterFactory) {
_presenter = StateObject(wrappedValue: viewFactory.getMessagePresenter())
}
var body: some View {
List(presenter.messages) { message in
Text(message.description)
}
}
}
// Preview用のPresenterFactoryに準拠したFactory
private final class MessageViewFactory: PresenterFactory {
func getMessagePresenter() -> MessagePresenter {
MessagePresenter(messages: [
.init(description: "Factory", email: "Factory"),
.init(description: "Factory", email: "Factory")
])
}
}
#Preview("Factory") {
MessageFactoryPatternView(viewFactory: MessageViewFactory())
}
そして、Viewを上記のように書き換える。これで、Message以外のViewが存在する場合、オブジェクトの生成方法をすべてのPresenterで統一することができるようになった。
まとめ
Factory Methodパターンはこのようなケース以外でも、様々な場面で応用できる。そしてオブジェクトの生成を分離するだけで、格段にスケーラビリティが向上することが分かった。大規模なアプリケーション開発では、このような設計も選択肢に入れると良いと思う。