ちょっとしたネットワークリクエストを伴うツールを書く事があり、せっかくなのでRustでレイヤードアーキテクチャでやってみることにしました。
フォルダを分けてモジュールレベルで分離するのではなく、Cargoのworkspaceをつかって各レイヤーをcrateレベルで分離する事で各層の分離をより強化するというアプローチをとってみました。
Domain層の責務は、ドメイン層のレベルで利用するモデルのStructと、リポジトリのTrait、そしてData層での実装を意図したRepositoryのProviderのTraitです。
モデルについてもTraitで提供するというのも考えましたが、完全に独立したStructで定義し、この値を構成する責任はData層に委任するという形にしてみました。 このあたりは、Traitで提供するアプローチについてもやってみたいとおもいます。
Data層では、以下のようなRepositoryを実際に実装した値を提供する責務のProviderを実装する事によってApplication層で使えるように提供します。 AndroidアプリケーションではDaggerのようなDIコンテナのフレームワークがあるのですが、 RustではDIフレームワークはほとんどつかわれておらず、自分で実装するというのが一般的なようです。
一つのクレートの中でやる場合には、Genericsをつかって実現する事が一般的なようですが、今回はcrateレベルで分離をしてみているので
Box<dyn Trait>
パターンで隠蔽する事になり、このあたりは記述量がかなり増えて大変な印象がありました。
pub trait SampleModelRepositoryProvider { fn provide_sample_model_repository(&self) -> Box<dyn repository::SampleModelRepository>; }
これを実装したRepositoryを提供するためにData層で実装して、Data層からはこのDataModuleだけを外部に公開するようにします。
pub struct DataModule {} impl SampleModelRepositoryProvider for DataModule { fn provide_sample_model_repository(&self) -> Box<dyn SampleModelRepository> { return Box::new(SampleModelRepositoryImpl::new()); } }
このモジュールをApplication層から
let data_module = data::DataModule::new(); let repo = data_module.provide_sample_model_repository(); let model = repo.get_by_email_pass("my_email".to_owned(), "my_pass".to_owned()).awai; println!("{:?}", model);
こんな感じで利用する事で、Domain層で提供されているインタフェースの実装の詳細を知らずに利用する事ができました。