MV(R)Pアーキテクチャを採用していたよ。 http://adarapata.hatenablog.com/entry/2018/08/16/001708
アーキテクチャを決めて早三か月、そろそろ良いところと改善できそうなところが見えてきたので文字に起こしてまとめておきたい所存
プロダクトで言うEntityは基本的に2パターンだと考えています
- マスターデータをシリアライズするためIInitializeインタフェースを実装したクラス
- 通信結果を受け取るためジェネレータで自動生成されたクラス
これらはログイン時に一気に確保することが多いので、このEntity群を保持するクラスが必要になります。 アーキテクチャによってはDataStoreなどを用意することもありますが、プロダクトではRepositoryが保持しています。 保持する都合上、Repositoryは恒久的に存在してほしいので MasterRepositoryInstaller と UserRepositoryInstaller で一気にバインドするというやり方を取っています。
Repositoryは何を返すべきでしょうか。今までは特に明確な規約を決めていなかったのでまちまちでした。Entityを返すこともあれば、Viewにとってもっと使いやすいクラスに変換して返すというメソッドもありました。しかしこの 使いやすいクラスに変換して返す
はRepositoryにとってはアンチパターンではないかという考えに至りました。悲しい。
例えばアイテムの例で考えてみましょう。 アイテムの名前や種類など素朴なデータはUserItemが持っているとします。 アイテム一覧画面で表示したい情報を取るUseCaseは次のコードになるでしょう。
public class GetAllItemUseCase {
public List<ViewItem> Run() {
List<ViewItem> items = _itemRepository.GetAll();
return items;
}
}
public class ItemRepository {
public List<ViewItem> GetAll() {
return entities.Select(item => new ViewItem(item));
}
}
だいぶ端折りましたが、保持しているRepositoryが変換してシュッと返す。UseCaseは楽です。PresenterはUseCaseからもらったものを表示するだけです。
しかし、アイテムというのは性質上様々なものに紐づきます。キャラクタの強化アイテムはキャラのレベリング情報と結びつき、スキルツリー素材アイテムはキャラのスキルツリー情報と関連があるでしょう。
こんどは特定のスキルツリーノードの習得に必要な素材アイテム情報を表示したくなりました。ここでは次の三つが必要とします
- キャラクタのパラメータ情報
- ノードを習得することによる効果情報
- ノードの習得に必要な素材情報
- 素材アイテムの名前・画像
これらは全部別Entityなので4種類のEntity、4種類のRepositoryが存在します
先ほどのノリで、Repositoryが使いやすい形に返すと仮定するとこうなります。
public class GetNodeDetailUseCase {
SkillTreeNodeRepository _repository;
public ViewSkillTreeNodeDetail Run(int nodeId){
var detail = _repository.GetDetailByNodeId(nodeId);
return detail;
}
}
public class SkillTreeNodeRepository {
UserCharRepository _userCharRepo;
SkillTreeUseItemRepository _useItemRepository;
ItemRepository _itemRepository;
private List<SkillTreeNodeEntity> _entities;
public SkillTreeNodeRepository(UserCharRepository u, SkillTreeUseItemRepository g, ItemRepository i){}
public ViewSkillTreeNodeDetail GetDetailByNodeId(int nodeId) {
var node = _entities.First(e => e.id == nodeId);
var char = _userCharRepo.GetByCharId(node.charId);
var useItem = _useItemRepository.GetByNodeId(nodeId);
var item = _itemRepository.GetById(useItem.itemId);
return new ViewSkillTreeNodeDetail(node, char, useItem, item);
}
}
Repository in Repository が発生してしまいがちです。無理とはいいませんがテスト時に必要ないRepositoryのバインドが必要だったりとしんどいです。更に言うならプロダクトのRepositoryはデータソースの保持と永続化が目的であるためほかのリポジトリや使いたいオブジェクトまで面倒を見てあげる必要はありません。この調子で場面場面の表示用モデルを返すメソッドが生えると、いずれ神と化します。
表示用モデルを量産せずにすべてを詰め込んだ巨大なモデルを作る手もありますが、それは現状のMasterDataStoreやUserDataと同様の存在になってしまいます。
どちらにしても、 単一責任の原則 に反することになります。
RepositoryはEntityのCRUDのみに徹して、がっちゃんこはUseCaseがやるべきだと考えます。
public class GetNodeDetailUseCase {
SkillTreeNodeRepository _repository;
UserCharRepository _userCharRepo;
SkillTreeUseItemRepository _useItemRepository;
ItemRepository _itemRepository;
ViewSkillTreeNodeDetail.Factory _factory;
public ViewSkillTreeNodeDetail Run(int nodeId){
var node = _repository.GetByNodeId(e => e.id == nodeId);
var char = _userCharRepo.GetByCharId(node.charId);
var useItem = _useItemRepository.GetByNodeId(nodeId);
var item = _itemRepository.GetById(useItem.itemId);
return _factory.Create(node, char, useItem, item);
}
}
public class SkillTreeNodeRepository {
private List<SkillTreeNodeEntity> _entities;
public SkillTreeNodeEntity GetByNodeId(int nodeId) {
return _entities.First(e => e.id == nodeId);
}
}
public class ViewSkillTreeNodeDetail {
public class Factory : PlaceholderFactory<SkillTreeNodeEntity, UserCharEntity, UseItemEntity, ItemEntity, ViewSkillTreeNodeDetail>
}
上記のコードだと次の点が変わっています。
- 各RepositoryはEntityを条件に応じて返すだけにした
- UseCaseが関連するリポジトリすべてにアクセスしている
- 最終的な生成はZenject Factoryを使っている
まずRepositoryの責任がEntityの操作一本に絞られました。 ここに存在するのはドメインに即したデータ操作命令だけなのでシンプルになります
次に、ロジックがUseCaseに内包されることとなりました。これにより、ファクトリーのバインドなどはUseCaseを使うInstallerに移動するため「その場面で行われる処理」という区分けがやりやすくなります。
最後に生成処理をZenjectFactoryに移行しました。これにより、生成時に必要な依存関係をコンテナに任せられるようになったのでZenject管理の旨味を生かせます。
UseCaseを細かい単位で分割しているのはPresenterと強烈な結びつきを許可しているからです。よって違う画面でのUseCase再利用は原則できません。だからこそ、UseCaseはその画面において何をしたいのか、何が必要なのかというロジックをすべて内包しても良いと考えます。という理由からRepositoryは変換を行わず素朴に返し、どういうものが欲しいのかはUseCaseが処理するという関係性を保つとだいぶクリーンになるかと思います。
ということで、画面に必要なモデルはUseCaseがFactory使って生成するようにしましょう~
この「目的のオブジェクトに変換する」という方法は、アーキテクチャによっては Translator層
を設けるという形で解決することがあります。
https://speakerdeck.com/monry/lets-master-the-clean-architecture?slide=80
この層は割とFactoryとやることが似ていたりします。そしてプロダクトにおいてはFactoryを使った生成の運用がすでに浸透しているのでそちらを採用することとなりました