Kyome流SwiftUI×AppKitでの個人開発アーキテクチャ
個人開発しているmacOSアプリのメンテナンス性を向上するためにアーキテクチャの導入を進めてきました[1]が、ようやくある程度納得のいく形になったので文書化します。
アーキテクチャを導入するモチベーション
- 機能追加の手順をテンプレート化し、思考のリソースを抑えつつ品質を担保したい。
- SwiftUIのプレビューやテストのモックなどで依存性の注入ができるようにしたい。
- OSSで公開しているものもあるので、第三者がコントリビューションしやすいコードにしたい。
要件
- あくまで基本は個人開発向けのため、複雑でないこと
- 特別なフレームワークやライブラリを必要としないこと
アーキテクチャの概要
依存の方向を一方向にするように心がけたレイヤードなアーキテクチャです。
大きくData
・Domain
・Presentation
の3つの層で構成します。
- Data層:データの読み書きを担当
- Entity
- Repository
- Domain層:ビジネスロジックを担当
- AppModel
- Model
- ViewModel
- Presentation層:UIの提供を担当
- App
- View
各項目の詳細
Data層:Entity
struct
やenum
など全体で利用するデータの型を定義します。
ここでは複雑なビジネスロジックは書きません。
私はアプリ固有のエラーの種類もここで定義することにしました。
Data層:Repository
UserDefaults
やファイル(JSONやCSV)、データベースの読み書きを処理します。
また、データが更新されたことを通知する場合もあります。
Repository
はEntity層で定義したデータの型以外には依存しません。
命名規則として、〇〇Repository
のようにします。依存性の注入を実現するために、AnyObject
に準拠したprotocol
でインタフェースを定義し、それに準拠した〇〇RepositoryImpl
をclass
で定義しデータアクセスの処理を書きます。また、プレビュー用にダミーの処理を定義した〇〇RepositoryMock
もセットで用意します。
Domain層:AppModel
1つのアプリにつき1つだけ存在する特殊なModel
です。
アプリ起動直後・終了直前のイベントハンドリングをすることと、一部のRepository
とModel
、View
のインスタンスをシングルトンで保持することが責務です。それぞれのインスタンスの生成時に依存性の注入も行います。この責務以外のことはなるべく何もさせません。
命名規則として、アプリ名+AppModel
のようにします。ViewModel
を初期化する際にRepository
やModel
を渡すために、ObservableObject
に準拠したprotocol
でインタフェースを定義します。それに準拠した〇〇AppModelImpl
をclass
で定義します。
Domain層:Model
Presentation層
と直接関係のないビジネスロジックをここに書きます。
Model
はRepository
や別のModel
に依存することができます。ただし、AppModel
への依存と三つ巴のように循環するような依存は禁止します。
命名規則として、〇〇Model
のようにします。依存性の注入を実現するために、AnyObject
に準拠したprotocol
でインタフェースを定義し、それに準拠した〇〇ModelImpl
をclass
で定義しビジネスロジックを書きます。また、プレビュー用にダミーの処理を定義した〇〇ModelMock
もセットで用意します。
Domain層:ViewModel
View
に対応するModel
として、View
で表示すべきデータの手配とユーザーのアクションをModel
に伝達する役割を担当します。
ViewModel
はRepository
やModel
に依存することができます。ただし、AppModel
と別のViewModel
への依存は禁止します。(AppModel
のもつRepository
やModel
を受け取ることは許します。)
命名規則として、View名+Model
のようにします。依存性の注入を実現するために、AnyObject
またはObservableObject
に準拠したprotocol
でインタフェースを定義し、それに準拠したView名+ModelImpl
をclass
で定義しビジネスロジックを書きます。また、プレビュー用にダミーの処理を定義したView名+ModelMock
もセットで用意します。
また、macOS向けの特殊なViewModel
として、WindowModel
とMenuBarModel
も用意します。それぞれウインドウの管理とメニューバーの管理を担当します。
Presentation層:App
SwiftUI
ベースのアプリなら1つ存在しているアプリ名+App
の@main
なstruct
です。
AppModel
を保持し、WindowGroup
やWindow
、Settings
などウインドウの定義をします。
Repository
やModel
の受け渡しのために必要であれば、.environmentObject()
でView
にAppModel
を流します。
Presentation層:View
基本的にはSwiftUIのView
です。ウインドウ内のビューヒエラルキーの中で最も低いView
には対応するViewModel
を持たせます。(View
はViewModel
に依存することができます。)
Repository
やModel
をViewModel
に渡すために必要であれば、@EnvironmentObject
でAppModel
を持ちます。プレビューにはRepositoryMock
やModelMock
を使います。
常駐型アプリの場合は、macOS向けの特殊なView
としてMenuBar
を用意します。
データやイベントの伝達手段
基本的に依存の方向性に対して決まる。
- 依存先へは
func
で伝達する。 - 依存先からの伝達は
Combine
で購読して受け取る。
例外的にAppModel
からModel
への伝達は依存の方向性を無視してfunc
を用いる。
実装例
-
ほとんどのアプリがアーキテクチャのことをほとんど知らない学生時代に作ったもので、まともな設計ではありませんでした。 ↩︎
Discussion