Citrus-Field TECH BLOG.

フリーランスのITエンジニア、iOSアプリの個人開発、業務委託(小売、ヘルスケア)を行っています。お仕事については、メールもしくはXのDMでご相談ください

【模索中】効率的な開発に向けたクリーンアーキテクチャの適用方法

もし、今現在、Swift用のクリーンアーキテクチャテンプレートを作るとしたらどんな感じになるだろうか

参考リンク一覧

最も影響を受けたもの。

github.com

clean-swift.com

medium.com

qiita.com

さっくりとした各コンポーネントの役割

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まわりの説明

  1. RequestViewController -> InteractorViewのイベントを通知する
  2. ResponseInteractor -> PresenterInteractorで処理した結果を通知する
  3. 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層): ViewControllerSwiftUIView
  • 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) を活用

  • ViewControllerViewModel への依存をコンストラクタで注入する。
  • 小規模プロジェクトの場合、手動で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. 工数を抑えるポイント

  • 単純化: 各レイヤーでやるべきことを最小限にする。
  • 抽象化しすぎない: すべてのクラスや関数を抽象化するのではなく、明確に必要な部分だけを抽象化する。

小規模プロジェクトでは、「完全なクリーンアーキテクチャを実装しなければならない」と考えず、必要に応じて適用する。