騒音のない世界 BLOG

コンピュータと、音楽と。

iOS Clean Architecture

iOS その2 Advent Calendar 2016 15日目の記事です。

現在業務で製作中のアプリではClean Architectureを採用しています。アプリ自体はまだリリースされていないのですが、少しずつ全体像は見えてきたのでClean Architectureの概要と合わせて具体的な実装例の紹介と現時点での所感などを書きたいと思います。

Clean Architecture とは

f:id:a_beco:20161211205919j:plain 引用: Clean Coder Blog

Clean Architectureは、Uncle Bob(ボブおじさん)によって2012年にThe Clean Architectureで提案されたアーキテクチャの一種です。上図はその概念を集約したものです。@tai2さんによる日本語訳もあります。

概要

こちらでは上図を元に要点のみを解説します。まず、アプリケーションのドメインとなるビジネスロジックが「中核」を担います。これは円の内側に配置されているUse CasesとEntitiesを指します。反対に、ビューやDB、Webサーバなどは「外界」です。これは図中では青い円(External Interfaces)を指します。さらに、この間に位置する緑の円がInterface Adapter、「変換層」となります。この層はアプリの「中核」と「外界」の間に立ってそれぞれの世界で都合の良いようにデータの形を変換する役割を担います。

最も重要なルールは、依存関係は常に内側に向かうようにするということです。内側の円は、外側の円については何も知りません。クラス、関数、変数、すべてのものについて知ることはありません。これは、一般的にドメインのほうが高レベルで抽象化されており、安定しているからです。安定依存の法則にあるように、安定したモジュールに依存することによって頻繁な変更に対して影響範囲を少なくすることが期待できます。

このルールにより、層をまたいで同一のデータ型にアクセスすることは許されません。横断的に使われるデータでも、影響範囲を狭めるため各層で別々に定義してInterface Adapterで変換します。これによって、外側の仕様変更が内側に影響を与えることがないようにします。

図が言わんとしていることがイメージできたでしょうか。大雑把に言えば、やりたいのは「関心の分離」や「安定依存」といったプラクティスをアーキテクチャの枠組みによって実現することです。

iOSアプリにおける実装

チームで現状採用している具体的な実装例を見ていきます。 コード量が多いため、全体の実装はGithubに上げつつ、この記事では軸にしている考え方について解説します。


Github - a-beco/iOS-CleanArchitecture-Example

あくまでも一例なのでバリエーションはあるでしょうし、まだまだ詰めきれていない部分もありますが、クラスの分け方や依存関係など、これからClean Architectureを導入しようとしている方々の参考になれば幸いです。

今回構成を決めるにあたって、こちらの記事が具体例としてとても参考になりました。ありがとうございます!

まだMVC,MVP,MVVMで消耗してるの? iOS Clean Architectureについて - Qiita

ただし、依存の方向や命名などは色々と再考しています。また、今回用意したものはアーキテクチャの構成を理解するための単なるスケルトンであり、こちらの記事のように動作的に意味のあるサンプルアプリではありませんのでご容赦ください。ライブラリは一切使わず、伝統的なdelegateパターンのみで実装しています。

それでは大事なポイントについて解説していきます。

3つのグループ分け

f:id:a_beco:20161212013114p:plain 図のように大きくPresentation, Domain, Dataという3つのグループに分けています。ディレクトリ構成もこの分け方に従っています。ここで、この3つはあくまでもグループ分けであり、層ではないということに注意してください。Clean Architectureは円形で、図の同じ色の部分が同じ層になります。

Presentation

Presentationは見た目にかかわる部分を含みます。いわゆるビューです。見た目を描画するだけでなく、タッチイベントなどのユーザアクションを検知し、ドメインロジックを呼び出します。

ビューに関するロジックはビジネスロジックには含まれないためPresentationに書かれます。UIViewControllerはビューのライフサイクルイベントをハンドリングするためPresentationの一部として考えています。iOSアプリを書く場合、UIViewControllerがエントリポイントとなるためビューを中心に考えてしまいがちですが、これらはClean Architectureにおいては外側の存在であり、あくまでもDomainが中心的な存在となります。

Interface AdapterについてはPresenterとControllerに分けています。これらはどちらも単にデータを変換して渡していくだけのシンプルな実装であり、ControllerがUseCaseへの入力、PresenterがViewへの出力を担っています。2つに分けているのは、UIViewControllerからUseCaseへ、またその逆へと通り抜けていくことを強制するためです。ここの役割はデータ変換であり、それ以上のロジックは持ちません。PresentationにおいてはUseCaseを複数使う場合などUseCaseから返ってきた処理をこの層ですぐにUseCaseに返したくなってしまうことが多いかもしれないと考えたため、入口と出口を分けることにしました。

ただし、この構成はClean Architectureにおいて特にルールとして言及があるわけではないことと、ドメインで同期的な処理をしたい場合に冗長な記述になってしまうため、考え直すか例外を認める必要があるかもしれないと感じています。

Domain

アプリのドメイン部分です。ビジネスロジックを集約します。ビジネスロジックとは、このアプリが提供する機能です。特定の画面でしか使わないものはありえますが、特定の画面と密に紐付くものではありません。PresentationとDomainは多対多の関係です。1つの画面が複数のUseCaseを持ち得るし、1つのUseCaseを複数の画面から使いまわすこともあり得ます。

DomainはPresentationのロジックやDataの取得先の変更に対して全く依存しないように作ることが理想的です。

UseCaseクラスの粒度をどうするかは難しいところですが、例えば認証についてはAuthUseCaseにまとめ、Presentationからは単にloginやlogoutを呼び出すようにする、などが考えられます。ログイン処理の詳細についてはDomainに隠蔽されます。

ボブおじさんの元記事ではEntityはEnterprise wideなビジネスルールを、UseCaseにはApplication specificなビジネスルールを書く、という役割分担がなされています。Entityのほうがより業界的に一般的なルールを書くということかと思うのですが、単に「データを取ってきて表示する」みたいな単純な処理だとEntityに当たる処理が無いことも多いのではないでしょうか。

Data

Domainからアクセスされ、外部データにアクセスします。取得・操作するデータの実体を抽象化します。例えばデータベースなのか、外部サーバなのか、ローカルキャッシュなのか、などはDomainから意識しないものになっている必要があります。例えばDomainからはgetUserDataのようにするだけでどこからかデータが取れる、といったイメージです。

認証機能などデータアクセスとして捉えるか難しいものもありますが、外部サーバにアクセスするものは現状Dataにまとめてしまっています。

所有関係・依存関係

f:id:a_beco:20161212040820p:plain ボブおじさんの元記事でも言及されていますが、所有関係と依存関係は明確に区別して考える必要があります。

呼び出すものは呼び出されるものを参照する必要があります。図のデータの流れの通り、ControllerはUseCaseを所有し、UseCaseはGatewayを所有しています。しかし、Clean Architectureの重要なルールとして依存関係は内側に向かわなければなりません。この問題は依存性逆転の原則に基づいてprotocolを円の内側に置くことで所有関係と依存関係を逆転させて解決しています。イメージとしては、円の内側にいるものが外側にいるものに対してprotocolでインタフェースを定義し、実装を要求するような形です。詳しくはソースコードを参照してもらうとよいかと思います。

Clean Architectureから外れたもの

定数や横断的に使うクラスについてはClean Architectureに含めないようにしています。例えばサンプルにはありませんが、DIコンテナやLoggerなどはアプリ全体で使うためそれに当たります。

また、開発中のアプリでは画像のロードにKingfisherを利用しています。このライブラリではUIImageViewのextensionとして画像のURLを渡すとロードして表示する機能が提供されていますが、Clean Architecture的には画像の取得は外部からのデータ取得なのでDataに置くべき処理と考えられます。Kingfisherでは画像のダウンロードのみを行うこともできるため、そのように作ることもできますが、工数がかかりすぎるためこれについては利便性をとり、Presentationで画像をロードするようにしています。

これによってアプリがライブラリの作りに強く依存してしまったため、画像ロード部分を別なライブラリで置き換えるのは難しくなります。これはリスクですが、実装コストは節約できました。採用するライブラリによってはこのような判断を迫られることも現実問題としてあるかと思います。

メリット・デメリット

開発が進んできて感じているメリットとデメリットについてご紹介します。

メリット

テスタビリティが向上する

クラスが疎結合かつ小さな単位で分割されることがほぼ強制されるため、モデルのかなりの部分がスタブを用いてユニットテストを書きやすい構成になります。ビューに関してもきちんと書いていれば純粋なビューロジックのみになるため、XcodeのUI Testを利用することでデグレやすい部分のテストなど自動化しやすいのではないでしょうか。

サーバ実装ができていない部分に関してモックで代替するような実装も簡単にできます。

再利用性が向上する

今回製作中のアプリがApp Extensionを利用するものなため、感覚的には2つのアプリを作成しているような感じなのですが、ビューは異なりつつも多くの部分のロジックが共通していました。その一方で認証やデータの取得先など部分的に異なる箇所があったのですが、Clean Architectureを採用していたことで異なる部分のみをコンポーネントとして作るだけで済み、手戻りなくスムーズに実装できたことで時間を節約できたように思います。

もし何も考えずに作っていれば同じ処理の実装を共通化できなかったり、不格好なマクロで分岐させる必要があったかもしれません。

機能を置き換えやすくなる

再利用性でも少し触れましたが、DBを別なものに置き換えたり、サーバから取ってきていたものをローカルから取るようにしたりなど、一部分を置き換えるのが楽です。

見通しが良くなる

変更するときの影響範囲がわかり易いです。円の外側の変更が内側にまで影響することはありません。これはビューの見た目やデータの取り方などが変わってもドメインの処理には影響しない、ということです。以前のプロジェクトでは依存が連鎖して思いがけない箇所に依存したことで問題になったケースもありましたが、今回は心配なさそうだなと思います。

デメリット

学習コストが高い

シンプルなMVCなどのアーキテクチャと比べると理解すべきことが多く、慣れないうちは読み書きするのが難しいようです。パッと見たときにファイルが多くて複雑に感じてしまいがちなのもあるかもしれません。複数人で開発する場合、チーム内で共通認識を持って開発していくためにドキュメント化や勉強会の開催などの布教活動が必要になるかもしれません。

面倒なことが多い

ちょっとあのデータにアクセスしたい、といったときにいちいちInterface Adapterを用意して。。というのはなかなか手間です。このような面倒な手間も結果としては良いプラクティスであることが多いように思いますが、養成ギプスのような痛みを伴うのも事実です。

何度も心が折れそうになり、「このへんは例外的にグローバルアクセスできてもいいかな。。」などの特に根拠もない悪魔の囁きに耳を傾けかけたこともありましたが、踏みとどまって面倒な手順を踏むことで見通しは良くなり、スムーズな再利用に繋がって良かったとは思っています。

逆にここを妥協するとアーキテクチャの恩恵が薄れてしまうので、なあなあにならないよう注意したいところです。

ライブラリやSDKの設計思想と食い違う

前述したKingfisherの例もそうですが、ライブラリの設計思想がアーキテクチャにそぐわないことは多々あるかと思います。

例えば、RealmからデータをFetchすると得られるObjectは、複数箇所のビューで同じデータを使っていてもObjectを持たせておくことで自動的にすべて同期されるという便利な特徴があります。

しかしこれを活かそうとするとDataからPresentationまで広範囲に依存するデータ型を作ることになってしまいます。最終的にこれはClean Architectureの文脈に沿わないと判断し、Realmは単なるデータストアとして使うことにしました。

また、画像処理をEntityでやりたいと思っても、使いたいライブラリがUIと密に結びついていると難しいだとか、ライブラリで定義されているデータ型を他の処理で使いたいと思ったときに層をまたぐデータを作ってしまってClean Architectureのルールに反してしまうといったこともありえます。

ライブラリ導入時にはそのような不整合をどうするかについて悩まされることになるかもしれません。

最後に

Clean Architectureについて、実例を交えながらメリット・デメリットなど解説させていただきましたが、いかがでしたでしょうか。

僕はClean Architectureに関して一言で表現するならば様々な経験則に基づいた「Modelの細分化」なのかなと考えています。アーキテクチャの歴史はModelとViewを分離することからはじまり、MVCやMVP,MVVMなどModelとViewをつなぐ部分で様々な提案がなされてきています。Clean ArchitectureではそのうちのModelを小さく明確な役割によって分割しつつ、それぞれを疎に結合し、影響範囲のスコープを狭めることで心配事を分割し、変更に強い柔軟なつくりを実現しようとしているのかなと。

一方で、近年インタラクティブな演出やシームレスなアニメーションなどのリッチ化が進むビューの設計に関してはClean Architectureはせいぜいビジネスロジックの混在を防ぐのに留まっており、なんの指針も与えていません。ビューは複雑化の一途を辿っており、実際のところ、現在のプロジェクトで一番行数が多く複雑なクラスはビューコンポーネントです。特にゲームなどビューロジックがファットになるものに関してはさらなるアイディアが必要になるでしょう。

この世界に銀の弾丸はなかなか見つかりませんが、Clean Architectureのようなアイディアは我々がソフトウェアが本質的に持つ複雑性と戦う上で強力な武器となりうる、と信じたいです。

Conforming to these simple rules is not hard, and will save you a lot of headaches going forward.

引用: Clean Coder Blog


何かの参考になれば幸いです。

参考文献