
前回SOLID原則というものを整理してみました。もう少しデザインに落とすには各構成を考える必要があります。
ドメイン駆動設計(DDD)という考え方があると知り、サラリと表面上を読んでヘキサゴナルアーキテクチャがとっつきやすそうだったので、それを元に現在のコードを再構成してみようと思います。
ですが、いきなり変更すると訳がわからなくなりそうなのでサンプルコードを作ってみようと思います。
自動車を運転する場合を考えて見ます。
各自動車製造会社は各々で開発して自動車を販売していますが、その操作はハンドル操作、アクセルとブレーキのペダリングと共通しています。
この共通な操作が抽象化された部分で運転手から見ると正に自動車のインターフェイスとなります。
操作方法だけ知っていれば自動車の細かな実装を知らなくても運転が可能となります。
プログラム寄りにもう少しブレークダウンしてみると先程の図のそれぞれはDriver, Interface, Carというように定義できてます。ログ出力用にStateとUiも追加しました。
Driver内の「ハンドルを左へ」などは関数やメソッドにあたります。それらを呼び出すことでInterfaceを通じてCarのSteeringなどの機能を呼び出します。
Rustの場合は、Interfaceはtraitとして定義されて実装部分がCarになるでしょう。
DriverはInterfaceのみを通じてCarにアクセスします。Carの実装に変更があってもInterfaceは変わらないのでDriverに影響はないはずです。
これで依存性逆転ができていると思います。
ヘキサゴナルアーキテクチャを軽く読んだ限りなので正しい理解ではないと思いますが、まるでサーバ間のデータのやり取りのような感じに思えたのでコレをネットワーク図のようにしてみました。
AppがDriverやCarなどをすべてインスタンスとして持っているとして、上段のDriverから入力が始まり、Interfaceを通してCarに情報を伝達。
AppがCarの処理結果をStateに溜め込んでUiでログ出力、というようなかたちです。
最初は上段を入力系、中断を処理系、下段を出力系などと分けようと思ったのですがどのカテゴリにも入らないようなものもあるので一つのネットワークとして捉えていただければ。
ただ、データフローが分かりづらいのでもう一つ図を作ってみました。
シーケンス図にすると、new()でインスタンスを作るタイミングとそのライフタイムがわかりやすく表現できたのでRust向きな気がします。
矢印の上に書かれているのが伝達用オブジェクトになり、ここも抽象化することで依存をしないように注意する箇所でしょう。
AppやDriverなどの単位で分割していけばよさそうなので実装に移ろうと思いますが、Rustでは分割方法としてモジュールとクレートがあります。
どちらかを選択しなければなりません。しかし、どのような違いがあるのかいまいちわからなかったので両方のやり方で実装してみようと思います。
Rustのモジュールはファイルシステムのツリー構造がそのまま反映されるようなイメージになります。
クレートあるいはモジュールのルートはmain.rsあるいはlib.rsになります。
このファイルの中でmod carのように書くとcar.rsファイルがモジュールとして読み込まれます。
car.rsの中でさらにmod test のように書くと、これがサブフォルダ扱いとなり、carフォルダの中のtest.rsがモジュールとして読み込まれます。
これらを使うためには、crate::car::test::Testといったようなパス形式のようなかたちで指定します。
これらを簡略化して使うためには、useを使って名前空間に持ち込む必要があり、use crate::car::test::Testのように書くと以降は単にTestとするだけで認識されます。
このパス形式はcrate::car::test::Testのようなモジュールのルートからの絶対パス的表記やuse super::Appのような相対パス的な表記のどちらも可能です。
実装の詳細はexamples/solid_dddのなかのsolid_mod01をご覧ください。
コードとしては同じですがよりファイル分割をしたものをsolid_mod02として作ってみました。
多数のuse文やstruct単位にファイルを分割しています。このようにした場合、pub useを使ったre-exportが便利です。
クレートで分割する場合は、プロジェクトフォルダの中に個別にcargo new –lib car のように新しいプロジェクトフォルダを作って、それぞれを作成します。
扱いとしてcrates.ioで外部クレートをdependenciesに記述することと同じように個々の内部クレートを記述します。
それとは別に、大元のCargo.tomlでworkspaceセクションを使うとバージョンなどを一元管理できるようになるので便利です(個々の内部Cargo.tomlは大元のCargo.tomlのworkspaceセクションを参照するように記述が必要)。
実装の詳細はexamples/solid_dddのなかのsolid_crate01をご覧ください。
モジュール分割からクレート分割に移行する場合、アクセス権がより厳しくなるので注意が必要です。今回は特に引っかからなかったですが所有権も注意が必要だと思います。
コードとしてはmod文がなくなってuse文の記述が少し変わる程度ですが、ビルドの観点で見るとコンパイル時間の短縮が見込まれるます。
今回作ったサンプル程度ではあまり短縮の恩恵は感じられませんでしたが、内部クレート単位でビルドもできるので並列で開発、単体テストもできますので大規模プロジェクト向けの機能かな、と思います。
コードとしては同じですがよりファイル分割をしたものをsolid_crate02として作ってみました。
solid_mod02と同様のアプローチです。
簡単ですがクレート分割を使ってみた感想です。
| モジュール分割 | クレート分割 | |
|---|---|---|
| 分割方法 | mod文 | 内部クレート、workspace |
| ビルド | ファイル単位だが全体のコンパイルが必要 | 内部クレート単位で可能 並行コンパイルで全体ビルドの時間短縮の可能性 |
| テスト | 従来と同じ | 内部クレート単位で可能 |
| デメリット | サブモジュールを深くすると面倒かも | Cargo.tomlが複数作成されてしまう。workspaceで管理できるが記述量が増える。 |
| その他 | – | crates.ioに公開するとき、内部クレートも公開するのでネーミングに気をつける必要がある |
DDD自体の考え方はまだ理解できていない部分もありますが、コンポーネント間の境界が明確化されてより疎結合になる印象です。
テストも書きやすい気がします。全体的な記述量は増える気がします。
第2部 TOC
GitHubにコードをアップロードしています。0.1.1バージョン
コードのコメントに書かれているfirst_stepなどをcargoコマンドに渡すと実行できます。
# Example
$ cargo run --example first_step
Code language: Bash (bash)