別のアーキテクチャ?
近年、iOSプラットフォーム用のアプリケーションを作成するための代替アーキテクチャのトピックが注目を集めています。 MVP、MVVM、VIPERとして知られる一部の有力者は、すでに特別名誉委員会に参加しています。 そしてそれらに加えて、それほど一般的ではない他の多くがあります。
強者の間では、私の意見では、すべての場合に普遍的な錠剤はありません:
- 静的なデータセットを使用していくつかの小さな画面を作成する必要がある場合、完全なVIPERを導入するのは非常に高価です。
- リアクティブアプローチが気に入らない場合は、高い確率でMVVMが通過します。
- 大規模なプロジェクトで大規模な問題が発生した場合、MVCはおそらく適切ではありません。
複数のアーキテクチャを使用するオプションがあります。多くのアーキテクチャでは、程度を変えることで他のアーキテクチャと組み合わせることができますが、少なくとも3つの理由であまり便利ではありません。
- モジュールが大きくなると、別のアーキテクチャに変換する必要がある場合があります。
- モジュールに変更を加えるときは、まずどのアーキテクチャが使用されているか、そしてそこでモジュールを変更する必要があるかどうかを考慮する必要があります。
- コードアダプターを追加して、異なるアーキテクチャのモジュールを共有する必要があります。これは、コードが両方とも同時にネイティブになる可能性が低いためです。
そして過去4年間、多くのプロジェクト(銀行セクターからのいくつかのプロジェクト、いくつかの異機種混合のカスタムプロジェクト、および自分自身のいくつかのアプリケーションとゲームの両方)に直面して、私は自分でアーキテクチャアプローチを形成しました。私が始めているプロジェクト。
これまでのところ、彼は私に失敗していません。 同時に、私は開拓者だとは思いません。確かに、多くの人がすでに同様のアプローチを使用しています。 しかし、私が個人的に遭遇したプロジェクトは建築に関してかなり難しいので、自分の考えを共有したいと思いました。
シルバーについて簡単に
このアーキテクチャバリアントの形成では、いくつかの重要な側面が考慮されました。
- 単純なモジュールと複雑なモジュールの両方に適用することも同様に簡単です。
- 必要に応じて、テストを幅広くカバーできる必要があります。
- ビューは部分的にアクティブであり、複雑なロジックと通信できる場合がありますが、その内部に実装を含めるべきではありません。
- 存在の事実のためにInteractorでエンティティを生成しないために、 Viewは必要に応じてサービスと直接通信できます-特定のモジュールに関連付けられていないロジック。
- iOS UIのライフサイクルでは、中心的なリンクはViewController(View)です 。これはメモリ管理を簡素化するために使用する必要があります。
要約すると:
- Viewは、それ自体をシンコントローラーにし、必要に応じてInteractor 、 ルーター 、その他のサービスと通信します。
- 依存関係はServiceLocatorを通じて登録されます。
- Routerは外部からモジュールとの通信を行いますが、メモリ管理はViewに基づいています 。
アーキテクチャの主要部分:
- 各モジュールは最上位のInteractor 、 Router 、 Viewにあります 。
- ストレージおよび処理用のデータは、個別の共通エンティティ層です。
- 依存関係はServiceLocatorを通過します。
私は条件付きでそれをシルバーと呼びます:最初の文字。
例による銀
地理に関する独自の知識を期待して、自分たちが覚えている国と都市のリストを保持する小さなデモアプリケーションを収集します。
まず、モジュールの公開プレゼンテーションを見てみましょう。 このフレーズでは、モジュールは制御可能な集合イメージを表し、そのステータスは画面に表示できます。 したがって、どのモジュールにも2つの公開部分があります。
- ルーター 。モジュールを管理し、他のモジュールとやり取りできます。
- ViewController 。モジュールの視覚的表現を表示できます。
protocol IBaseRouter: class { var viewController: UIViewController { get } } struct Module<RT> { let router: RT let viewController: UIViewController }
これにより、 ViewControllerが構造の別のプロパティに繰り返し接続されている場合、それらを繰り返した理由が尋ねられる場合があります。
その理由は、最も単純なメモリ管理を確保するために、 ViewControllerがモジュールの残りの部分と強力に接続しているという事実に重点が移っているということです。現在の画面から戻ると、 ViewControllerはUIKit階層から削除され、モジュール全体。
同じ理由で、親モジュールから、子ルーターとの通信は、必要に応じて弱められます。
そのため、メモリを詰まらせないために、初めてViewControllerが作成されるのは、アクセスされた瞬間のみです。 したがって、実行可能なモジュールを表示するには、そのViewControllerを使用する必要があります。 ただし、制御できるようにするには、 ルーターと通信する必要があります。
モジュールファクトリからルーターを取得すると、モジュールへの強力なリンクがなくなり、次のコード行で既に破棄されます。 また、工場からViewControllerを取得した場合、モジュールを制御および構成することはできません。
この問題は、モジュールが作成されたときに満たされているモジュール構造によって解決され、 ルーターとViewControllerへの両方の強力なリンクを一時的に保持することができます。 その結果、構造がローカルスコープでまだ生きている間に、 ルーターを弱いリンクに保存し、UIKitが強いリンクを保持する画面にViewControllerを表示できます 。
モジュール作成の例 func InputModuleAssembly(title: String, placeholder: String, doneButton: String) -> Module<IInputRouter> { let router = InputRouter(title: title, placeholder: placeholder, doneButton: doneButton) return Module<IInputRouter>(router: router, viewController: router.viewController) }
モジュールの使用例 private func presentCountryInput() { let module = InputModuleAssembly(title: "Add city", placeholder: "Country", doneButton: "Next") self.countryInputRouter = module.router module.router.configure( doneHandler: { [unowned self] country in self.interactor.setCountry(country) self.presentNameInput() } ) internalViewController?.viewControllers = [module.viewController] }
一般的に、 ルーターは次の目的で必要です。
- モジュールの構成に必要な着信パラメーターを受け入れます(より頻繁に-コンストラクターを使用)。
- ユーザーが何らかのアクションを実行したことをモジュールが報告できるように、必要なコールバックを受け入れます。
- ViewControllerの受信を手配します。
- 有用であれば、 ルーターの子モジュールを保存します。
ルーターの例 protocol IInputRouter: IBaseRouter { func configure(doneHandler: @escaping (String) -> ()) } final class InputRouter: IInputRouter { private let title: String private let placeholder: String private let doneButton: String let interactor: IInputInteractor private weak var internalViewController: IInputViewController? init(title: String, placeholder: String, doneButton: String) { self.title = title self.placeholder = placeholder self.doneButton = doneButton interactor = InputInteractor() } var viewController: UIViewController { if let _ = internalViewController { return internalViewController as! UIViewController } else { let vc = InputViewController(title: title, placeholder: placeholder, doneButton: doneButton) vc.router = self vc.interactor = interactor internalViewController = vc interactor.view = vc return vc } } func configure(doneHandler: @escaping (String) -> ()) { internalViewController?.doneHandler = doneHandler } }
モジュールで複数のアクションを実行できる場合、構成メソッドにはすべての可能なコールバックを含めることができます。 これにより、開発プロセス中に新しいコールバックを追加する場合に呼び出しを登録することを忘れないでください。
// callback, // , // . func configure(cancelHandler: @escaping () -> (), doneHandler: @escaping (String) -> ()) // callback , // . func configure(cancelHandler: @escaping () -> ()) func configure(doneHandler: @escaping (String) -> ())
まったく同じ方法で、保存されたモジュールの形で、アプリケーションの最初の部分を表すことができます。
class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? private weak var rootRouter: IRootRouter! func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { let window = UIWindow(frame: UIScreen.main.bounds) self.window = window let module = RootModuleAssembly(window: window) rootRouter = module.router window.rootViewController = module.viewController window.makeKeyAndVisible() return true } }
依存関係は、 RootRouterで構成されているServiceLocatorから取得されます (ただし、明確にするためにRootInteractorに移動する価値があるかもしれません)。それに関連する2つの主なニュアンスがあります。
- その作成はルートモジュールで行われます。
- 再利用のためのサービスの準備は、それ自体の中で行われます。
SILVERの一部として、 ルートモジュールは常にその責任の範囲内にあるため、常にそこにあると想定されます。
- アプリケーションの状態に応じてルート画面を切り替える。
- ServiceLocator登録。
ServiceLocatorの例 struct ServiceLocator { let geoStorage: IGeoStorageService func prepareInjections() { prepareInjection(geoStorage) } } func inject<T>() -> T! { let key = String(describing: T.self) return injections[key] as? T } fileprivate func prepareInjection<T: Any>(_ injection: T) { let key = String(describing: T.self) injections[key] = injection }
ServiceLocatorの作成例 final class RootRouter: IRootRouter { // ... init(window: UIWindow) { let serviceLocator = ServiceLocator( geoStorage: GeoStorageService() ) serviceLocator.prepareInjections() }
ServiceLocatorの例 final class ListInteractor: IListInteractor { // ... private lazy var geoStorageService: IGeoStorageService = inject()