こんにちは。新人プログラマーの岩本です。
今回は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 | ユースケースを表すインターフェース |
Interactor | Use Caseインターフェースの実装 |
Input Data | Use Caseインターフェースの引数 |
Input Boundary | クラス図上でのUse Caseインターフェースの表現 |
Output Data | Output Boundaryへ渡す値 |
Output Boundary | Presenterのインターフェース 実装は下層レイヤーで行われる |
Controllers, Gateways, Presenters (Interface Adapters)
このレイヤーは、Frameworks&DriversとApplication Business Rulesとの間でそれぞれのレイヤーで利用できるような型への相互変換をする役割を持っています。
簡単に言うと、この内側と外側のレイヤーとを繋ぐ中継役のような役割です。
これがあるおかげで、内側のレイヤーはどのAPIを使うか、どのDBを使うかなどの、瑣末な情報に気にしないで済みます。
以下は主な要素例です。
名称 | 役割 |
---|---|
Gateway | Frameworks&Driversからのデータを抽象化する |
Repository | ≒ Gateway API通信の橋渡し的な役割(曖昧) |
Presenter | InteractorからOutput DataをOutput Boundaryを経由して受け取り、それをViewに適した形にして返す Viewに表示するための、データ加工を行う |
Controller | ユーザからの入力を、Use Caseのためにデータ加工する Presenterの逆バージョン |
Devices, Web, UI, DB, External Interfaces (Frameworks & Drivers)
このレイヤーはもっとも外側のレイヤーで、UIやWebAPI、DBなど瑣末な情報を処理する役割を持っています。
全体の流れ
Clean Architectureで処理を行う際には、大まかに以下のような流れをたどります。
- ControllerからUseCaseに入力データを伝える
- UseCaseの実態であるInteractorに処理が委譲される
- Interactorが処理を行い、Presenterに出力データを伝える
- Presenterが表示を行う
これを図に示したのが、前述した全体図の右下にある図です。
Clean Architectureのよくある誤解
これはClean Architectureにおけるよくある誤解の1つです。
前述したClean Architectureの全体図が広まり、それが4つのレイヤーに分かれていたので、そこから誤解が生まれたそうです。
ただこの図はあくまで例示的に4つのレイヤーに分かれているだけです。
ただこの図で伝えたいのは、依存は外から中に向かっていなくてはいけない、と言うことです。
レイヤーの構造は状況によって柔軟に変えていく必要がありますが、依存性のルールだけは守ってください。
ではここから、実際にClean Architectureを用いた実装例を紹介します。
Clean Architectureを用いた実装例
今回はGitHubAPIを用いてレポジトリ情報を取得するアプリを作成しました。
動作イメージは以下の通りです。
コードの全文を紹介することはできませんが、処理の流れに沿って解説していきます。
Presenter
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点です。
- UIから
searchRepository
関数が呼び出されると、UseCaseに処理を依頼する - GitHubRepositoryPresenterInputを通じて、UseCaseから結果が送られてくる
UseCase
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と一緒です。
- Presenterから
searchRepository
が呼ばれると、Gatewayに処理を依頼する - GitHubRepositoryUseCaseInputを通じて、Gatewayから結果が送られてくる
Gateway
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)
}
}
こちらも流れは一緒です。
- UseCaseから
searchRepository
が呼ばれると、APIClientに処理を依頼する - GitHubRepositoryGatewayInputを通じて、APIClientから結果が送られてくる
コード全文
ここではコードを全て紹介することはできませんが、GitHubで公開しているので興味がある人はぜひ読んでみてください。
まとめ
今回はClean Architectureについて解説しました。
僕自身まだ曖昧な理解ですが、基本的なことは学べたと思います。
アーキテクチャについての勉強はしておくと、コードについての理解がグッと深まるのでおすすめです。
ぜひご自身の手で色々と試してみてください。
ここまでのご閲覧ありがとうございました!
コメント