もし、今現在、Swift用のクリーンアーキテクチャテンプレートを作るとしたらどんな感じになるだろうか
参考リンク一覧
最も影響を受けたもの。
さっくりとした各コンポーネントの役割
Presentation Layer
ViewControllerとPresenter、Interactorについては以下のように一方向のつながりにするよう制約します。
ViewControllerは、Viewが検知したタップイベントなどをうけて、Interactorにそのイベントに対応するビジネスロジックを実行するよう依頼します。 InteractorはUseCaseを呼び出しビジネスロジックを実行してPresenterにその結果を通知します。 PresenterはInteractorからの結果通知をうけてViewControllerに表示内容を指示します。 ViewControllerはPresenterからの指示に従いViewの更新を行ったり、Routerを介して画面遷移を行ったりします。
以下のコンポーネント間はプロトコルでI/Fを定義しそれぞれI/Fに依存した実装をします。
ViewController -> Interactor Interactor -> Presenter Presenter -> ViewController
また、それぞれのコンポーネント間のメッセージは、以下のModel(struct)を定義してやりとりします。
Modelまわりの説明
- RequestViewController -> InteractorViewのイベントを通知する
- ResponseInteractor -> PresenterInteractorで処理した結果を通知する
- ViewModelPresenter -> ViewController表示する内容を指示する
struct ThreadList { struct Filter { struct Request { var star: Bool } struct Response { var star: Bool } struct ViewModel { var filterLabel: String } } }
Domain Layer
UseCaseはInteractorから依頼されたビジネスロジックを実行します。 必要に応じてUseCaseからはRepositoryを介してDataStoreへアクセスしDBアクセスやAPI通信行います。 ここでもそれぞれプロトコルでI/Fを定義して、そのプロトコルに依存した実装にします。
また、UseCaseはDBアクセスなどの結果取得したEntityを、TranslatorにてPresentation Layerで利用できるModelに変換してから渡すようにします。
Data Layer
実際にDBにアクセスしたりAPI通信をしたりするDataStoreを置きます。 ここでもプロトコルでI/Fを定義して、そのプロトコルに依存した実装にします。 RepositoryからはそのI/FでDataStoreを呼びます。
ここまでの印象
一つのviewアクションの追加に、修正量が多すぎないか
ViewControllerでViewイベントを受け取ったらViewModelにイベント通知して(API通信などを行った上で)その結果をうけてViewControllerが画面を更新する流れに比べると、だいぶ細分化される。
個人開発など、プロジェクトの大部分の開発がかのうなら、開発コストやオーバーヘッドも大事。
そして、再考。
必要以上に複雑化しないよう注意する。
1. レイヤードアーキテクチャを簡素化
クリーンアーキテクチャの基本概念を取り入れるには、以下のように簡素なレイヤー分割を導入する
- View (UI層):
ViewController
やSwiftUI
のView
。 - ViewModel (プレゼンテーション層): ビジネスロジックとUI更新を分離。
- UseCase/Interactor (ユースケース層): ビジネスロジックを記述。
- Repository (データアクセス層): データソースの抽象化(API通信やデータベース操作など)。
例:
protocol UserRepository { func fetchUserData(completion: @escaping (Result<User, Error>) -> Void) } class DefaultUserRepository: UserRepository { func fetchUserData(completion: @escaping (Result<User, Error>) -> Void) { // API通信の実装 } } class FetchUserUseCase { private let userRepository: UserRepository init(userRepository: UserRepository) { self.userRepository = userRepository } func execute(completion: @escaping (Result<User, Error>) -> Void) { userRepository.fetchUserData(completion: completion) } } class UserViewModel: ObservableObject { @Published var user: User? private let fetchUserUseCase: FetchUserUseCase init(fetchUserUseCase: FetchUserUseCase) { self.fetchUserUseCase = fetchUserUseCase } func loadUserData() { fetchUserUseCase.execute { [weak self] result in switch result { case .success(let user): self?.user = user case .failure(let error): print("Error: \(error)") } } } }
2. 依存性逆転の原則 (Dependency Inversion Principle)
- 上記例のように、リポジトリ層を
protocol
で抽象化し、具体的な実装(例えばAPI通信)は外部で注入する形にする。 - これによりテスト可能性が向上し、モックやスタブを利用してユニットテストが簡単になります。
3. DI (Dependency Injection) を活用
-
ViewController
やViewModel
への依存をコンストラクタで注入する。 - 小規模プロジェクトの場合、手動でDIを行い、サードパーティのDIフレームワークは必要ないことが多いです。
例:
let userRepository = DefaultUserRepository() let fetchUserUseCase = FetchUserUseCase(userRepository: userRepository) let viewModel = UserViewModel(fetchUserUseCase: fetchUserUseCase)
4. 非同期処理のシンプル化
Combine
(もう辞めたい) やSwift Concurrency (async/await)
を活用することで、非同期処理のコードがよりシンプルに。
例 (async/await を使用):
class DefaultUserRepository: UserRepository { func fetchUserData() async throws -> User { // API通信の実装 } } class FetchUserUseCase { private let userRepository: UserRepository init(userRepository: UserRepository) { self.userRepository = userRepository } func execute() async throws -> User { return try await userRepository.fetchUserData() } } class UserViewModel: ObservableObject { @Published var user: User? private let fetchUserUseCase: FetchUserUseCase init(fetchUserUseCase: FetchUserUseCase) { self.fetchUserUseCase = fetchUserUseCase } func loadUserData() async { do { user = try await fetchUserUseCase.execute() } catch { print("Error: \(error)") } } }
5. 工数を抑えるポイント
- 単純化: 各レイヤーでやるべきことを最小限にする。
- 抽象化しすぎない: すべてのクラスや関数を抽象化するのではなく、明確に必要な部分だけを抽象化する。
小規模プロジェクトでは、「完全なクリーンアーキテクチャを実装しなければならない」と考えず、必要に応じて適用する。