【Swift】新人エンジニアが教える「Clean Architecture」

設計パターン

こんにちは。新人プログラマーの岩本です。

今回はClean Architectureについて学んだことを紹介します

Clean Architectureは色々なアーキテクチャの基盤となるようなものです。

これを学ぶと様々なアーキテクチャに対する解像度がグッと高くなると思います。

ぜひ最後までご覧ください。

この記事は新卒エンジニアが執筆しています。
そのため内容に間違いや不備がある場合があります。
もし間違いを発見しましたら、どんどん指摘していただけると幸いです。
また所々、知識が曖昧なものがあります。それを踏まえてお読みください。

Clean Architectureとは

Clean Architectureとは、2011年にロバート・C・マーティン氏(通称ボブおじさん)がブログで提唱したシステムアーキテクチャの1種です。

MVVMなどのGUIアーキテクチャとの違いは、システムアーキテクチャはアプリケーション全体の構成まで踏み込でいることです。

MVVMを例にします。これはModel, View, ViewModelに分けますが、システムアーキテクチャはModelの内部表現も細分化します。

GUIアーキテクチャよりもより広範に構成するという認識を持っていれば大丈夫です。

Clean Architectureの構成要素

Clean Architectureといえば、以下の画像が有名です。

この図は依存関係の流れを表しています。

Clean Architectureの根本思想を一言で表すとこんな感じです。

外側のレイヤーは内側のレイヤーに依存していて、内側のレイヤーは外側のレイヤーについては何も知らない状況を実現する

これを表したのが上の図で、これを実現するためにいくつかの構成要素に分解されています。

もっとも一般的な分け方は以下の通りです。

  • Entities
  • Use Case
  • Presenter / Gateway
  • UI / DB / API

ここからは1つずつ詳しく解説していきます。

Entities (Enterprise Business Rules)

Entitiesにはドメインルールやデータモデルなどを定義します。

ドメインルールとは、ソフトウェアによって解決したい課題をコードに落とし込んだものです。

Clean Architectureの提唱者ロバート・C・マーティンが書いた書籍『Clean Architecture』に、ドメインルールのわかりやすい説明があったので、紹介します。

例えば、銀行がローンにN%の利子をつけているとすると、それは銀行のお金を生むためのビジネスルールになる。利子をコンピュータで計算しようと、そろばんで計算しようと、全く関係はない。

『Clean Architecture』より引用

Entitiesはレイヤーの最も内側に位置しているので、何にも依存しません。

Use Cases (Application Business Rules)

Use Caseは、細かいビジネスロジックをまとめて一連の流れを実行するためのものです。

Use Caseが何かするというよりも、適切な場所にタスクを依頼し、適切な場所に結果を返します。イメージとしては優秀なマネージャです。

以下は主な要素例です。

名称役割
UseCaseユースケースを表すインターフェース
InteractorUse Caseインターフェースの実装
Input DataUse Caseインターフェースの引数
Input Boundaryクラス図上でのUse Caseインターフェースの表現
Output DataOutput Boundaryへ渡す値
Output BoundaryPresenterのインターフェース
実装は下層レイヤーで行われる

Controllers, Gateways, Presenters (Interface Adapters)

このレイヤーは、Frameworks&DriversとApplication Business Rulesとの間でそれぞれのレイヤーで利用できるような型への相互変換をする役割を持っています。

簡単に言うと、この内側と外側のレイヤーとを繋ぐ中継役のような役割です。

これがあるおかげで、内側のレイヤーはどのAPIを使うか、どのDBを使うかなどの、瑣末な情報に気にしないで済みます。

以下は主な要素例です。

名称役割
GatewayFrameworks&Driversからのデータを抽象化する
Repository≒ Gateway
API通信の橋渡し的な役割(曖昧)
PresenterInteractorからOutput DataをOutput Boundaryを経由して受け取り、それをViewに適した形にして返す
Viewに表示するための、データ加工を行う
Controllerユーザからの入力を、Use Caseのためにデータ加工する
Presenterの逆バージョン

Devices, Web, UI, DB, External Interfaces (Frameworks & Drivers)

このレイヤーはもっとも外側のレイヤーで、UIやWebAPI、DBなど瑣末な情報を処理する役割を持っています。

全体の流れ

Clean Architectureで処理を行う際には、大まかに以下のような流れをたどります。

  1. ControllerからUseCaseに入力データを伝える
  2. UseCaseの実態であるInteractorに処理が委譲される
  3. Interactorが処理を行い、Presenterに出力データを伝える
  4. Presenterが表示を行う

これを図に示したのが、前述した全体図の右下にある図です。

Clean Architectureのよくある誤解

レイヤーをこの4つに分割しなければならない。

これはClean Architectureにおけるよくある誤解の1つです。

前述したClean Architectureの全体図が広まり、それが4つのレイヤーに分かれていたので、そこから誤解が生まれたそうです。

ただこの図はあくまで例示的に4つのレイヤーに分かれているだけです。

ただこの図で伝えたいのは、依存は外から中に向かっていなくてはいけない、と言うことです。

レイヤーの構造は状況によって柔軟に変えていく必要がありますが、依存性のルールだけは守ってください。

ではここから、実際にClean Architectureを用いた実装例を紹介します。

Clean Architectureを用いた実装例

今回はGitHubAPIを用いてレポジトリ情報を取得するアプリを作成しました。

動作イメージは以下の通りです。

コードの全文を紹介することはできませんが、処理の流れに沿って解説していきます。

Presenter

GitHubRepositoryPresenter
protocol GitHubRepositoryPresenterProtocol {
    func searchRepository(keyword: String, perPage: Int, page: Int) async
}

protocol GitHubRepositoryPresenterInput: AnyObject {
    func searchRepository(repositories: [GitHubAPIRepository])
}

final class GitHubRepositoryPresenter: GitHubRepositoryPresenterProtocol, ObservableObject {
    @Published private(set) var repositories: [GitHubAPIRepository] = []
    private let useCase: GitHubRepositoryUseCase
    
    init(useCase: GitHubRepositoryUseCase) {
        self.useCase = useCase
    }
    
    func searchRepository(keyword: String, perPage: Int, page: Int) async {
        await useCase.searchRepository(keyword: keyword, perPage: perPage, page: page)
    }
}

extension GitHubRepositoryPresenter: GitHubRepositoryPresenterInput {
    func searchRepository(repositories: [GitHubAPIRepository]) {
        self.repositories = repositories
    }
}

コードのポイントは、以下の2点です。

  1. UIから searchRepository 関数が呼び出されると、UseCaseに処理を依頼する
  2. GitHubRepositoryPresenterInputを通じて、UseCaseから結果が送られてくる

UseCase

GitHubRepositoryUseCase
protocol GitHubRepositoryUseCase {
    func searchRepository(keyword: String, perPage: Int, page: Int) async
}

protocol GitHubRepositoryUseCaseInput: AnyObject {
    func searchRepository(repositories: [GitHubAPIRepository])
}

final class GitHubRepositoryInteractor: GitHubRepositoryUseCase {
    private weak var presenter: GitHubRepositoryPresenterInput?
    private let gateway: GitHubRepositoryGatewayProtocol
    
    init(gateway: GitHubRepositoryGatewayProtocol) {
        self.gateway = gateway
    }
    
    func inject(presenter: GitHubRepositoryPresenterInput) {
        self.presenter = presenter
    }
    
    func searchRepository(keyword: String, perPage: Int, page: Int) async {
        await gateway.searchRepository(keyword: keyword, perPage: perPage, page: page)
    }
}

extension GitHubRepositoryInteractor: GitHubRepositoryUseCaseInput {
    func searchRepository(repositories: [GitHubAPIRepository]) {
        presenter?.searchRepository(repositories: repositories)
    }
}

こちらも大まかな流れはPresenterと一緒です。

  1. Presenterから searchRepository が呼ばれると、Gatewayに処理を依頼する
  2. GitHubRepositoryUseCaseInputを通じて、Gatewayから結果が送られてくる

Gateway

GitHubRepositoryGateway
protocol GitHubRepositoryGatewayProtocol {
    func searchRepository(keyword: String, perPage: Int, page: Int) async
}

protocol GitHubRepositoryGatewayInput: AnyObject {
    func searchRepository(repositories: [GitHubAPIRepository])
}

final class GitHubRepositoryGateway: GitHubRepositoryGatewayProtocol {
    private weak var useCase: GitHubRepositoryUseCaseInput?
    private let dataSouce: GitHubAPIClientProtocol
    
    init(dataSouce: GitHubAPIClientProtocol) {
        self.dataSouce = dataSouce
    }
    
    func inject(useCase: GitHubRepositoryUseCaseInput) {
        self.useCase = useCase
    }
    
    func searchRepository(keyword: String, perPage: Int, page: Int) async {
        await dataSouce.searchRepository(keyword: keyword, perPage: perPage, page: page)
    }
}

extension GitHubRepositoryGateway: GitHubRepositoryGatewayInput {
    func searchRepository(repositories: [GitHubAPIRepository]) {
        useCase?.searchRepository(repositories: repositories)
    }
}

こちらも流れは一緒です。

  1. UseCaseから searchRepository が呼ばれると、APIClientに処理を依頼する
  2. GitHubRepositoryGatewayInputを通じて、APIClientから結果が送られてくる

コード全文

ここではコードを全て紹介することはできませんが、GitHubで公開しているので興味がある人はぜひ読んでみてください。

GitHub - Yuzuki0709/CleanArcitecture-SwiftUI
Contribute to Yuzuki0709/CleanArcitecture-SwiftUI development by creating an account on GitHub.

まとめ

今回はClean Architectureについて解説しました。

僕自身まだ曖昧な理解ですが、基本的なことは学べたと思います。

アーキテクチャについての勉強はしておくと、コードについての理解がグッと深まるのでおすすめです。

ぜひご自身の手で色々と試してみてください。

ここまでのご閲覧ありがとうございました!

参考にした記事

コメント

タイトルとURLをコピーしました