こんにちは、最近ほんのり?ふくよかになったオープンエイトの正原です。 ダイエットをしたいとは思ってはいるのですが、 「まだ寒い、まだ慌てるような時間じゃない」と自分に言い聞かせています。 そもそも一度に10kgのみかんが届くことなどないよう、ふるさと納税は計画的にすべきでしたね。
さて、今回のテックブログなのですが、 最近私が設計しているiOSアプリケーションにおけるクリーンアーキテクチャとRxSwiftを導入する際につまづいた点についてお話したいと思います。
クリーンアーキテクチャ
[引用元] : The Clean Code Blog
クリーンアーキテクチャはよく分からなくても「この図は見たことある!」という方も多いのではないでしょうか? 2012年にRobert C. Martin (Uncle Bob)氏がThe Clean Code Blogにて示した図です。 この図では主に「関心事の分離」と「依存性のルール」を表現しているのですが、 この図に関しては前述の本家のブログやその日本語訳版、 Clean Architecture 達人に学ぶソフトウェアの構造と設計、または多くのテックブログにて説明されているのでここでは割愛します。
Clean Architecture 達人に学ぶソフトウェアの構造と設計はソフトウェア開発における問題に対して、 クリーンアーキテクチャ(またはクリーンアーキテクチャで使われるテクニック)でどのように解消するかが分かりやすく書いてあるので、 一度は読んでおいて損はないと思います!ぜひぜひ!
どんな構成にしたの?
恐らく、多くの方が最初は私と同様に「理屈はなんとなく分かるけど、どのように実装したら良いかが分からない」といった状況になるのではないでしょうか。 私は多くのテックブログやgithub上にあるサンプルアプリケーションを参考にして以下のような構成にしました。
以降ではこの構成を基に今回のトピックである「どのようにつまづいたり転んだりしたか」をみていきます。
参照と依存の向きによる違和感
クリーンアーキテクチャを勉強したら「実際に少し書いてみよう」となるでしょう。 しかし、実際に書くとなると何か参考になるソースコードが欲しくなると思います。 そんなとき、githubで公開されているサンプルアプリケーションが非常に役立ちます。
ビューコントローラとプレゼンター
では、まずエントリポイントとなるビューコントローラとプレゼンターを見ていきたいと思います。
// SomeViewController.swift class SomeViewController: UIViewController { var presenter: SomePresenter? override func viewDidLoad() { super.viewDidLoad() presenter?.doByPresenter() } } extension SomeViewController: SomePresenterDelegate { func doByDelegate() { } }
// SomePresenter.swift protocol SomePresenter { func doByPresenter() } protocol SomePresenterDelegate: AnyObject { func doByDelegate() } class SomePresenterImpl: SomePresenter { weak var delegate: SomePresenterDelegate? func doByPresenter() { } }
ビューコントローラおよびプレゼンターはお互いインターフェース(プロトコル)のみに依存して詳細を知らないようにします。 そしてDIコンテナにて以下のように依存性を注入すると思います。
let viewController = SomeViewController() let presenter = SomePresenterImpl() viewController.presenter = presenter presenter.delegate = viewController
ここまではまだ違和感を抱かないで理解できるのではないでしょうか。 ちなみに、同じファイルにプロトコルとその実装を書くことで、 Xcodeでジャンプしても迷子にならずにすみます。 参照元が要求しているのはプロトコルなので、プロトコル定義にジャンプしてしまい、 実装まで直接ジャンプできなくなってしまうんですね。
ゲートウェイとレポジトリ
では、次にゲートウェイとリポジトリを見ていきたいと思います。
// SomeGateway.swift protocol SomeGateway { func doByGateway() } protocol SomeGatewayDelegate: AnyObject { func doByUseCase() } class SomeGatewayImpl: SomeGateway { private let repository: SomeRepository weak var delegate: SomeGatewayDelegate? init(repository: SomeRepository) { self.repository = repository } func doByGateway() { } }
// SomeRepository.swift protocol SomeRepository { func doByRepository() } protocol SomeRepositoryDelegate: AnyObject { func doByGateway() } class SomeRepositoryImpl: SomeRepository { weak var delegate: SomeRepositoryDelegate? func doByRepository() { } }
この辺で「ん?ゲートウェイがレポジトリを持つ形になって良いのか・・・?」となりました。 先程の構成図に依存性の方向を入れた図を見てみたいと思います。
レポジトリは最も円の外側であるDBやAPIなどの詳細が含まれている領域です。 それは正しいのですが、なぜゲートウェイがレポジトリを持つような形になってしまうのかが違和感でした。 ですが、それもそのはず。iOSでは基本的にビューコントローラが参照元となり、 プレゼンターやユースケース、ゲートウェイ、レポジトリの順に参照されているからです。 参照元と参照先の関係は以下の図のようになると思います。
これは恐らく、ビューコントローラが破棄された際、 その画面を構成するインスタンスも同様に破棄されて欲しいためだと思います。 ですが、クリーンアーキテクチャの依存性の方向と参照の方向が異なるため、 このような違和感を抱くことになったんですね。
(もっと上手にやれば参照の方向なども自然な設計になったりするんでしょうか?)
ユースケースとはどのようなもので何を実装するか?
ユースケースとはどのようなものでしょうか? 本家のブログや書籍には「アプリケーション固有のビジネスルール」とありますが、 APIから取得したデータをビューに表示するだけのシンプルなアプリの場合、 恥ずかしながら何がユースケースに相当するのかがイマイチ想像できませんでした。
こんなとき頼りになるのがグーグル検索です。 そこで「ユースケース」と検索しますと、様々なユースケース図が出てきて気づきました。 iOSアプリケーションにおいては純粋にユーザーのアクションで良いのではないか?と。 例えば、皆さんが知ってるTwitterアプリなどの場合、以下のようになるかと思います。
Twitterアプリケーションでなくとも、多くのアプリケーションにおいて 「お気に入り」や「再読み込み」、「追加読み込み」などの機能があると思います。 これらユーザーのアクションをユースケースとするのがiOSアプリケーションにとって 自然なのではないでしょうか。
(クリーンアーキテクチャの「ユースケース」にとらわれて広義での「ユースケース」がすっぽり抜け落ちてたのは内緒です)
エンティティとRxSwift
皆さんRxSwift使っていますでしょうか? タイトルに入れたくせにこれまで全く触れられてこなかったRxSwiftについてですが、個人的にはとても好きなライブラリです。 さて、クリーンアーキテクチャを用いた実装でRxSwiftをどのように使うかですが、意外とエンティティと相性が良いのではないでしょうか。 (ビュー層はこれまでと同様の使い方をすると思います。また必ずしもエンティティにしなければならないわけではないと思います)
例えば、テーブルビューやコレクションビューにて1つ1つのセルを構成するデータをSomeData
として、
そのデータの配列を SomeDataList
とした場合、以下のような実装になります。
// SomeData.swift struct SomeData { let title: String let description: String }
(以下の SomeDataList
ではBehaviorRelay
を用いていますのでRxCocoa
のインポートが必要です。)
// SomeDataList.swift import RxSwift import RxCocoa class SomeDataList { let list: BehaviorRelay<[SomeData]> var stream: Observable<[SomeData]> { return list.asObservable() } init(value: [SomeData] = []) { list = BehaviorRelay(value: value) } func set(value: [SomeData]) { list.accept(value) } func add(value: [SomeData]) { list.accept(list.value + value) } }
このように Observable
で観測可能な状態にしてユースケースからプレゼンターへ、
そしてプレゼンターで Driver
にしてからビューコントローラへ渡していくことで
ビュー層では観測するだけですむのではないかと思います。
詳細な部分は違うかもしれませんが、目的やイメージとしてはfluxに近いかもしれません。
またエンティティ(SomeDataList
)が扱う[SomeData]
ですが、これは最初の構成図におけるdata
に相当します。
クリーンアーキテクチャでは「境界線を超えるデータは単純なデータ構造で構成されている」とあるので、
struct
で定義した単純なデータ構造の配列である[SomeData]
は境界線を超えても良いのではないかと思いました。
最後に、最初の構成図でエンティティがユースケースをまたがっている部分についてですが、 同一のエンティティを異なるユースケースで使うパターンがあるかもしれない、と思ったからです。 ログイン・ログアウトのような場合はユースケースを再利用したら良いかもしれませんが、 上記のようなデータの再読み込みと追加読み込みなどは、ユーザーのアクションは異なりますが、 扱うエンティティは同じ方が都合が良いと思います。
ただ、データの再読み込みと追加読み込みに関しては、 ゲートウェイやレポジトリも同じ方が都合が良いため、 今のところ「ロード」という1つのユースケースに集約しています。 (もしかしたらそのようなエンティティは存在しない・・・!?)
試してみた感想
まだ直面していないので自信を持って言えないですが、 利点としてはやはり「仕様変更に強そう」ではないでしょうか。 ですが、サービスそのものが開発途上のiOSアプリケーションにおいては、 中心のドメインの仕様変更も少なくないのが少々怖いところです。
逆に欠点ですが、やはり「ファイル数が多い」と「理解が難しい」です。 「理解が難しい」は勉強しろと言われればそれまでかもしれませんが、 チーム開発を行う上では全員に理解してもらわないといけないので 勉強会を行うなどしないといけないと思います。
まとめ
いかがでしたでしょうか? 正直、まだまだ実装している最中なので何か不都合が出てこないか、 そもそも自分のクリーンアーキテクチャの理解が間違っていないか毎日ドキドキしています。 また、今回お見せした例は「こうあるべきだ」とか「こうしないといけない」というものでもないです。 ですが、クリーンアーキテクチャを勉強を通して「こうしたらもっと仕様の変更に強くなりそうだ」というのが 以前より多くなったと思いますので、(プロダクトに導入するかはともかく)一度本家のブログだけでも 読んでみてはいかがでしょうか。