目次
- 概要
- 前置き
- はじめに
- DDDについて
- LayeredArchitectureについて
- OnionArchitectureについて
- OnionArchitectureの応用
- CleanArchitectureについて
- CleanArchitectureの応用
- 実装テクニック・ノウハウ
- 用語集
概要
複雑・肥大化するwebアプリケーションコードの寿命を伸ばす&筋の良い作りにするために、代表的なアーキテクチャの概念をまとめてみる。
特に、業務でも使うことの多いDjangoをターゲットとして、CleanArchitectureを使った場合の構成を模索してみた備忘録。
前置き
- ソフトウェアの寿命って?
- アップデートについていけなくなる。保守・開発ができなくなる。などが原因で使われなくなった時
- 寿命の短いソフトウェアって?
- 複雑怪奇なコードで、追加開発・修正のコスト大 → 誰も保守・開発できなくなり廃棄
- 特定の人しか知らない構造・コードになっていて、後から追うのが辛い → 作り直した方が早いので廃棄
- 寿命を伸ばすためには?
- 仕様変更や技術のアップデートに耐えられる柔軟性を持たせる
- デファクトスタンダードなアーキテクチャに則って、同じ文脈で保守・開発できる絶対数を増やす
はじめに
はじめに、導入としてDDDの概念をチョーざっくり説明する。
次に、アーキテクチャとして、LayeredArchitectureの説明を行う。
また、LayeredArchitectureをベースに、OnionArchitectureの概念と、用語・構造を説明する。
OnionArchitectureの応用として、既存のMVC型WebアプリケーションフレームワークとOnionArchitectureを組み合わせた場合を説明する。
そして、CleanArchitectureの概念と、用語・構造を説明する。
CleanArchitectureの応用として、既存のMVC型WebアプリケーションフレームワークとCleanArchitectureを組み合わせた場合を説明する。
補足
実装テクニック・ノウハウと、用語集も記述している。
DDDについて
ドメイン駆動設計(Domain-Driven Design)とは
EricEvansのドメイン駆動に関するdocumentは、ここを参照いただければと思うので、詳細は割愛。
ざっくり自分の理解をまとめると、
ドメインモデリングのノウハウやベストプラクティスを集大成した、1つの設計 思想・哲学。TDDなどのような開発プロセスとは違うよ。
- 「ドメイン」とは何か?
- ソフトウェア化する対象のことで 「A sphere of knowledge, influence, or activity.」 と定義している。つまり、「ソフトウェア化して解決したい対象(=問題/課題)」が持つ「業務内容や知識」のこと。
- 「ドメインモデリング」とは何か?
- ソフトウェア化する対象の定義・設計
- 「ドメインモデリング」の価値とは何か?
- ドメインモデルが適しているかという観点で開発が回るので、ソフトウェアのコア機能を、コンパクト かつ 短いサイクルでの改善ができる
- ドメイン知識が、所定のモジュールに集約される(= 凝集 )ので、コードが散らばらずに保守・開発がしやすくなる
つまり、
ソフトウェア化したい現実の対象を、「ドメイン」としてソフトウェアの世界に落とし込むという思想
LayeredArchitectureについて
LayeredArchitectureとは?
DDDにて紹介されたドメインを隔離するためのアプリケーションアーキテクチャ
原則として、上から下に向けて依存関係になっていて、逆方向への依存はNG
LayeredArchitecture構成要素
-
UI/Presentation
- ユーザや別システムへの情報表示
- ユーザや別システムからのコマンド受付
- 画面構成やデザイン変更による仕様変更が頻繁に発生し得る不安定な層でもある
-
Application
- UIから呼び出されるアプリケーションのロジックが定義される
- アプリケーションとしてのドメイン知識はここには詰め込まないで、UIからデータを受け取ってパースしたり、UI向けのデータ加工を行う層
-
Domain
- Applicationから呼び出されるモジュール。
- ソフトウェアとしてのドメイン知識をモジュールとして提供する層。
-
Infrastructure
- DBや外部サービス(File Access, Access, ORM, etc...)にアクセスして永続化を担当する層。
- 一般的な技術要素が詰め込まれている
- 技術要素に引きづられるので、ここも頻繁に変動が起こり得る不安定な層。
LayeredArchitectureの特徴
- Domainで、ドメインロジックを提供することで、ドメイン知識を所定のモジュールに押し込めている。
- Applicationで、アプリケーション固有のデータ加工や、ドメインロジックの制御を提供することで、ドメインに影響を与えずにロジックを組み立てることが可能になる。
- 基本的に下の階層のモジュール呼び出しのみ可能としている。
- コードの再テストや修正による影響は、依存先である 上位の層 のみとなる。
LayeredArchitectureの弱点
- Infrastructure層が上位層から依存されていることが問題。
- Infrastructureのモジュールの修正や置き換えが発生した場合、それを参照しているDomain、さらにはApplication/UIまで影響が波及していく。つまり、Domain層より上位の層の修正対応のほか、テストの追加・修正から再実行まで行う必要が出てくる。
- 例えば、MySQLモジュールのバージョンアップや、MySQLからCloud Saas Serviceへの移行による影響が、ドメインロジックまで波及する。(ドメインロジックに波及するということはアプリケーション・UIもテストが必要になる)ドメインロジックやアプリケーションロジックが膨大になればなるほど、この影響は辛いものとなる。。。
OnionArchitectureについて
参考
- https://qiita.com/little_hand_s/items/2040fba15d90b93fc124
- https://qiita.com/little_hand_s/items/ebb4284afeea0e8cc752
- https://qiita.com/cocoa-maemae/items/e3f2eabbe0877c2af8d0
OnionArchitectureとは?
LayeredArchitectureをベースに、「テスト」や「インフラ(DB)」等の外部サービスと技術要素との結合を疎にして、より「ドメイン」の実装に着目したアーキテクチャ。
OnionArchitecture構成要素
-
UserInterface/Presentation
- ユーザや別システムへの情報表示
- ユーザや別システムからの入力(リクエストやコマンド入力)受付
- 画面構成やデザイン変更による仕様変更が頻繁に発生し得る不安定な層でもある
-
Infrastructure
- DBや外部サービス(File Access, Access, ORM, etc...)にアクセスして永続化を担当する層。
- 技術要素に引きづられるので、ここも頻繁に変動が起こり得る不安定な層。
-
Tests
- テストコードモジュール。
- UIの変更に伴い、テスト項目も変動する不安定な層。
-
ApplicationService
- DomainServiceを組み合わせてアプリケーション実行に必要なロジックを搭載する。
- ソフトウェアとしてのドメイン知識はここには詰め込まないで、UIからデータを受け取ってパースしたり、UI向けのデータ加工を行う層。
- DomainService
-
DomainModel
- ドメイン知識に関連した状態と振る舞いを持つオブジェクト(EntityとValueObjectに代表されるもの)を配置する層。
- 他の層への依存関係を持たない。
OnionArchitectureの特徴
-
LayeredArchitectureの弱点であった、 Infrastructureへの Domainの依存を、 依存性逆転の原則 とDIにて解消している
つまり、以下図のように、InfraStructure層がDomainService層に依存するようになっていることがわかる。
-
Applicationロジック/Domainロジックが、外的要因(UI・インフラ)への依存関係をなくしたことにより、外的要因の影響を受けにくい(=疎結合)になった。つまり、外的要因の修正・変更があっても、ドメインモデルやServiceロジックの修正・テストは不要になった(= 最も重要なドメインに関する機能を担保できるようになる。
OnionArchitectureの応用
Djangoとの融合
Djangoのアーキテクチャに対して、OnionArchitectureを適用した簡易例を上記図で表している。
-
UI(PresentationLayer)
- MVCにおけるView/Controllerがここに存在。
- 責務としては、入力値のvalidation程度。
- エンドポイントの提供。
- ApplicationService/DomainService/DomainModelの呼び出し・組み立てを行う。
- Controllerのhelperが存在。
- Controllerの共通処理を提供。
- MVCにおけるView/Controllerがここに存在。
-
Infrastructuire
- MVCにおけるModelがここに存在。
- ORMを使ったモデルが定義され、DBへのアクセス・制御を行っている。
- Domainロジックで扱うデータ型でwrapされたRepositoryオブジェクトの具象クラスが存在する。
- Modelもここでwrapして、Repositoryとしてドメインロジック内で扱えるようにする。
- IOを直接制御するロジックもここでwrapして、Repositoryとしてドメインロジック内で扱えるようにする。
- MVCにおけるModelがここに存在。
-
ApplicationService
- アプリケーション向けのデータ加工やDomainService/DomainModelの呼び出しを行う
- アプリケーション固有の表示方法やvalidationなどのロジックはここに押し込める。
-
DomainService
- ドメインロジックの組み立てを行う。
- InfrastructureのInterfaceクラス(=Repository)を定義する。
-
DomainModel
- ValueObjectおよびEntityが定義される。
OnionArchitectureでのDjangoエンドポイント処理の流れ
- UIから飛んできたリクエストは、ApplicationServiceに飛ばされる。
- ApplicatinServiceでUIからデータを受け取り、DomainServiceで処理できるデータ形式に加工。
- DomainServiceでApplicatinServiceからデータを受け取る。
- 受け取ったデータを使ってDomainModelを呼び出し、ドメインロジックを組み立てる。
- RepositoryとDIで渡されたRepositoryの具象クラスを使って、データの永続化処理を実行
- DomainModelでDomainServiceから要求されたドメインモデルを返す。
- ApplicationServiceでDomainServiceから返り値を受け取る。受け取ったデータは、UIで表示できるデータ形式に加工。
- UIでApplicationServiceからの返り値を受け取り、UIに反映させる。
CleanArchitectureについて
参考
- https://qiita.com/nrslib/items/a5f902c4defc83bd46b8
- https://qiita.com/kz_12/items/bc79102247b86626fc72
- https://github.com/nrslib/CleanArchitecture/tree/master/CleanArchitectureSample
- https://nrslib.com/clean-ddd-entity/
CleanArchitectureとは?
OnionArchitectureなどのDDDベースで発案されたアーキテクチャの包括的な概念・構成を定義したもの。
さらに、具体的なクラス構成案や、依存性逆転のためのオブジェクト構成まで言及している。
CleanArchitecture構成要素
-
Frameworks & Drivers(=External Interfaces)
- 概要
- 技術的要素(DBやメールシステムなど)やUIそのものが存在し、外的要因で修正・変更が走りやすい層。
- 要素
- DB
- DBそのものや、DBを取り扱うミドルウェア・フレームワークのコードが存在する層。
- UI
- フロントのUIコードが存在する層。
- DB
- 概要
-
Interface Adapters
- 概要
- 入力、永続化、表示を担当するオブジェクトが所属
- External Interfacesとの接点
- 要素
- Gateways
- データ永続化の実行ロジックを担当する層
- Controller
- UseCasesのIF向けにデータ加工を担当する層
- UseCasesを呼び出す層
- Presenter
- 出力表示のためにデータ加工を担当する層
- Gateways
- 概要
-
Application Business Rules(=UseCases)
- 概要
- ソフトウェアとしてのドメイン知識はここには詰め込まない
- Controllerからデータを受け取ってドメインロジックを組み立てる
- PresenterのIFを介して、出力データを取得する
- Entitiesを組み合わせて制御する。
- 要素
- InputBoundary
- Controllerから参照する、InteractorのIFクラス
- UseCasesのロジック変更をControllerに波及させないために用意
- OutputBoundary
- Interactorから参照する、PresenterのIFクラス
- Presenterのロジック変更をInteractorに波及させないために用意
- Interactor
- Contorllerから呼び出される、ロジック組み立てを行うクラス
- Controllerから参照されるInteractorのIFをInputBoundaryとして用意する
- Entitiesを参照してロジックを組み立てる
- InputData
- ControllerからInteractorに渡すデータ構造
- OutputData
- InteractorからPresenterに渡すデータ構造
- InputBoundary
- 概要
-
Enterprise Business Rules(=Entities)
- 概要
- DDDにおけるEntityとは意味合いが異なるので注意!
- ドメイン知識に関連した状態と振る舞いを持つオブジェクト(EntityとValueObjectに代表されるもの)を配置する層。
- OnionArchitectureにおけるDomainModelだけでなく、DomainServiceも含まれ、ドメインルールを包括的に提供する層である。
- 他の層への依存関係を持たない。
- 概要
CleanArchitectureの具体的なモジュール構成とその成り立ち
上記はCleanArchitectureの具体的なモジュール構成を表した図である。 このモジュール構成とOnionArchitectureを照らし合わせて、構成のイメージを掴んでみる。OnionArchitectureを元に、以下の構造を用いる
InfrastructureからDomainServiceに伸びる依存関係は、DomainServiceが内包する、Repositoryを利用することを意味している。
DomainServiceからDomainModelに伸びる依存関係は、DomainServiceが内包する、Serviceを介してDomainModelを操作することを意味している。
EntitiesとDataAccessInterface
DomainModelを以下責務を持つオブジェクトとしてEntitiesを再定義する
- Entities
- プロダクトのコアとなるクラス
- EntityおよびValueObject
DomainServiceには、以下の二つの責務が存在する。責務の単一化のために、それぞれを分離する。
ここで、ApplicationServiceはDomainServiceやEntitesを参照して、アプリケーションの中核となるオブジェクトを取り扱う。また、DataAccessInterfaceを介して、データの永続化処理を取り扱うこととなる。
- DomainService
- Entitiesを用いてビジネスロジックを組み立てるクラス
- DataAccessInterface
- InfrastructureのInterfaceクラス(=Repository)
- ApplicationServiceからInfrastructureに相当する永続化ロジックを扱う際、依存性逆転の原則に従うことで生まれる。
DataAccessとInfrastructure
Infrastructureの中から以下責務を切り出す。
ここで、DataAccessは、DataAccessInterfaceを参照して具象クラスを実装する。また、Databaseを参照して、実際のIO操作を取り扱う。
- Database(Infrastructure)
- 実際のDBやIO操作を担う。
- ORM等のDBのwrapperモジュールもここに相当すると考える。
- DataAccess
- Infrastructureの機能をwrapして、ドメインロジックで扱う事のできるモジュールとして提供するクラス。
ApplicationServiceとInputBoundary
今までの図からは、UIからApplicationServiceに向けて依存関係が存在することがわかる。
しかし、UIはフロントエンドの開発、ApplicationServiceはバックエンドの開発として切り分けて実装・テストすることも多い。そこで、ApplicationServiceの拡張と修正によるUIへの影響低減を実現させたい。
ここで、開放閉鎖の原則を用いて、ApplicationService側の変更に対して、UI側が柔軟にテストや拡張が可能な仕組みを取り入れる。
具体的には、ApplicationServiceの実行ロジックをInterfaceクラス(=InputBoundary)としてUI側に提供する。UIからはInterfaceが備えるメソッドを使ってApplicationServiceを利用することで、Interfaceを満たしている限り、ApplicationServiceに影響を受けなくしている。
ここで現れたモジュールは以下の通り
- InputBoundary
- UIからApplicationServiceを呼び出す際に、開放閉鎖の原則より、修正の影響を受けないようために用意されるApplicationServiceのInterfaceクラス。
ControllerとInputData
UIには、ユーザからの入力を受け付ける処理、InputBoundaryを呼び出すため処理、InputBoundary向けに入力データのバリデーションやデータ加工を行う処理、出力形式に合わせてデータ加工する処理、Viewとして表示処理を担当する処理などの複数の責務が混在している。
そこで、まずはUIを以下の責務で分割する。
ここで、ControllerはInputDataを呼び出して、データ構造を作る。また、InputBoundaryを呼び出して、InputDataで定義したデータを渡して、ApplicationServiceを実行する。
そして、ControllerからUIを呼び出して、ApplicationService実行結果のデータの加工および出力処理を行う。
- Controller
- ユーザからの入力を受け付けるクラス
- アプリケーションロジックが実行できるか、入力値のバリデーション処理を行う
- InputBoundaryの呼び出し
- InputData
- InputBoundaryの呼び出し時に引数として渡すデータ構造またはクラス
- InputDataを用いてInputBoundaryはControllerからの値を解釈・利用する
ViewとPresenter
UI(Presentation)自体に、出力形式に合わせてデータ加工する処理とViewとして表示処理を担当する処理が混在している。
そこで、UI(presentation)を以下の責務で分割した。
ここで、ControllerはPresenterを呼び出し、ApplicationServiceの結果を出力したいフォーマットに加工する。
そして、PresenterはViewを呼び出し、出力処理を行う。
- Presenter
- ApplicationServiceの実行結果を出力したいフォーマットに合わせて加工するクラス
- View
- MVCにおけるview処理を行うクラス
- 出力や描画処理を担う
OutputBoundaryとOutputData
このままでは、Controllerが、ApplicationServiceとPresenterを呼ぶという2つの責務が発生している。そこで、PresenterおよびViewといった出力処理をControllerから剥がして、ApplicationServiceから呼び出せるように、以下の責務を分割する。
この時、ApplicationServiceからPresenterへの依存関係を結ばせないために、依存性逆転の原則に従って、PresenterのInterfaceクラス(=OutputBoundary)を用意している。
ここで、PresenterはOutputBoundaryを呼び出して、データの加工処理の具象クラスを実装する。
次に、ApplicationServiceはOutputDataを呼び出して、データ構造を作る。また、OutputBoundaryを呼び出して、OutputDataで定義したデータを渡して、Presenterを実行する。
- OutputBoundary
- PresenterのInterfaceクラス
- ApplicationServiceから参照させるために利用
- OutputData
- OutputBoundaryの呼び出し時に引数として渡すデータ構造またはクラス
- OutputDataを用いてOutputBoundaryはApplicationServiceからの値を解釈・利用する
UseCaseInteractor
今までの図より、CleanArchitectureで提案されているモジュール構造が、既存のOnionArchitectureをベースに、SOLIDの原則に従って変形していくとほぼ一致する形になることがわかる。
ここで、ApplicationServiceは、以下CleanArchitectureで提案されているUseCaseInteractorと同等の位置付けであると解釈できる
そこで、ApplicationServiceをUseCaseInteractorと再定義して、以下の図のようになる。
このInteractorは、基本的に外の層とのやりとりは全てInterfaceクラスを介して行っている。そのため、外の層への依存性をなくすことができているのである。
ここで現れたモジュールの責務は以下の通り
- UseCaseInteractor
- Controllerから、InputBoundaryを介して使われる
- Controllerから、InputData型のデータを受け取る
- DomainServiceを介して、Entitiesを取り扱う
- DataAccessInterfaceを介して、データの永続化処理を取り扱う
- OutputDataを呼び出して、処理結果をデータ構造に押し込める
- OutputBoundaryを呼び出して、出力したいデータフォーマットに合わせた加工を行う
[補足]ViewModelとView
PresenterとViewの関係については深く説明されていないが、CleanArchitecturecではViewModelを利用している図が現れる。
ここでは、Presenterから、ViewModelを参照し、ViewModelを加工したデータを用いて変更する。
変更されたViewModelは、Viewからも参照・バインディングされ、Viewの処理が行われて出力につながる。
ただし、このスタイルは、用いるフレームワークによってサポートされているかどうかによっても利用可否が変わってくる。
また、Webアプリケーションの開発の場などでは、MVC2をベースとしたFrameworkを採用していることが多いが、CleanArchitectureではMVC1をベースにしているため、若干のPresenterとView、Controllerの関わり方の違いがある。
そのため、利用するフレームワークによって、柔軟に設計し直す必要がある。
CleanArchitectureの特徴
- 外側から内側に向けて依存方向を向けることを原則としている。
- DDDのEntityとは異なり、EntitiesではDomainServiceとDomainModelを含む、ドメインロジックを提供する責務がある。
- OnionArchitectureと同様、Infrastructureの要素(=InterfaceAdapterのGateways)は、Repositoryの具象クラスとなる。Repositoryクラスは、UseCasesにて提供される。
- UIとUseCaseInteractorとの関係について、開放閉鎖の原則と依存性逆転の原則をベースに、入力データの加工を担当するInputBoundaryと出力データ向けの加工を担当するOutputBoundaryというInterfaceクラスが存在するようになった。
CleanArchitectureの応用
Djangoとの融合
Djangoに対して、CleanArchitectureを適用した簡易例を上記図で表している。
Djangoベースで考えた際に、MVC2をベースとしているため、ControllerとPresenter、Viewの関わり方がCleanArchitectureの基本から変わっている。
変更点は以下の通り
- Controllerの責務修正
- UseCaseInteractorの実行結果をOutputData型で受け取る
- Presenterを呼び出して、OutputData型のデータを与える
- PresenterでViewModelの変更を行い、Viewに値を反映させる
- OutputBoundaryの削除
- Presenterの呼び出しが、InterfaceAdapter層で閉じるため、UseCase層にInterfaceクラスが不要になった。結果、OutputBoundaryは削除した
実装テクニック・ノウハウ
依存性逆転の原則
抽象に依存せよ
依存する・されるの関係は、依存される側に変更があると、依存している側にも影響を及ぼす。(=importしているモジュールに変更があると、そのモジュールを利用しているコードも修正・テストが必要になる)
DomainがInfrastructureに依存している構成
上記例は、Domain層のItemクラスとInfrastructure層のItemRepositoryクラス間の依存関係を表している。具体的なクラス実装例は以下の通り。
// Itemオブジェクトを表すDomein層のクラス
#include<iostream>
#include<string>
#include<ItemRepository> // ItemRepositoryへの依存がある状態
using namespace std;
public class Item{
private int itemID=0;
private string itemName="";
public Item(int id, string name){
itemID = id;
itemName = name;
}
public void SaveItem(){
ItemRepository repo = new ItemRepository(); // ItemRepositoryを参照してインスタンス化
repo.save(this.itemID, this.itemName);
}
}
// ItemRepositoryとして、DBを介して永続化を行うInfrastructure層のクラス
#include<iostream>
#include<string>
using namespace std;
public class ItemRepository{
public void save(int id, string name){
sql::mysql::MySQL_Driver *driver = sql::mysql::get_mysql_driver_instance();
// MySQLのInsert処理
// ........
}
}
具体例より、ItemクラスではItemRepositoryを参照しており、ItemRepositoryの実装に変更・修正があった場合はItemクラスも修正・テストが必要になる。
そこで、InterfaceClassを介して、依存の方向を逆向きに変えるテクニックが依存性逆転の原則である。
依存性を逆転さた構成(DIなし)
上記例は、Domain層の中にInterfaceクラスとしてItemRepositoryクラスを用意し、Interfaceの中に実装クラスとしてItemRepositoryImplementクラスを用意した上でのクラス間依存関係を表している。具体的なクラス実装例は以下の通り
#include<iostream>
#include<string>
#include<IItemRepository> // IItemRepositoryへの依存がある状態
#include<ItemRepositoryImplement> // ItemRepositoryImplementへの依存がある状態
using namespace std;
public class Item{
private int itemID=0;
private string itemName="";
public Item(int id, string name){
itemID = id;
itemName = name;
}
public void SaveItem(){
IItemRepository repo = new ItemRepositoryImplement(); // IItemRepository/ItemRepositoryImplementを参照してインスタンス化
repo.save(this.itemID, this.itemName);
}
}
// RepositoryのInterfaceを表すDomain層のクラス
#include<iostream>
#include<string>
using namespace std;
public class IItemRepository{
// saveメソッドの純粋仮想関数
public virtual void save(int id, string name) = 0;
}
// Repositoryの具象クラスを表すInfrastructure層のクラス
#include<iostream>
#include<string>
#include<IItemRepository>
using namespace std;
public class ItemRepositoryImplement: public IItemRepository{
public void save(int id, string name){ // 純粋仮想関数の具象化
sql::mysql::MySQL_Driver *driver = sql::mysql::get_mysql_driver_instance();
// MySQLのInsert処理
// ........
}
}
具体例より、依然としてItemクラスの中でItemRepositoryImplementクラスを判別して呼び出しており、参照関係が消えていないことがわかる。
そこで、DIを利用して、Itemクラスの呼び出し元から、Interfaceクラスと具象クラスの組み合わせを渡して、Itemクラスの中で具象クラスを参照しないテクニックを使う必要がある。
依存性を逆転さた構成(DIあり)
上記例は、DIを使った場合のクラス間の依存関係を表している。具体的なクラス実装例は以下の通り。// Itemクラスを呼び出す上位モジュール
#include<iostream>
#include<string>
#include<Hypodermic/Hypodermic.h> //DIコンテナのためのライブラリ
#include<Item> // Itemへの依存がある状態
#include<IItemRepository> // IItemRepositoryへの依存がある状態
#include<ItemRepositoryImplement> // ItemRepositoryImplementへの依存がある状態
using namespace std;
public void Main(){
// DIコンテナの定義
Hypodermic::ContainerBuilder builder;
builder.registerType< ItemRepositoryImplement >()
.as< IItemRepository >();
m_container = builder.build();
// Itemのインスタンス化
repo = m_container->resolve< IItemRepository >()
item = new Item(1, "my item", repo) // repoを渡す
item.SaveItem(); // infrastructure層のロジックが実行される
}
// Itemオブジェクトを表すDomein層のクラス
#include<iostream>
#include<string>
#include<IItemRepository> // IItemRepositoryへの依存がある状態
using namespace std;
public class Item{
private int itemID=0;
private string itemName="";
IItemRepository itemRepo;
public Item(int id, string name, IItemRepository repo){
itemID = id;
itemName = name;
itemRepo = repo
}
public void SaveItem(){
itemRepo.save(this.itemID, this.itemName);
}
}
// RepositoryのInterfaceを表すDomain層のクラス
#include<iostream>
#include<string>
using namespace std;
public class IItemRepository{
// saveメソッドの純粋仮想関数
public virtual void save(int id, string name) = 0;
}
// Repositoryの具象クラスを表すInfrastructure層のクラス
#include<iostream>
#include<string>
#include<IItemRepository>
using namespace std;
public class ItemRepositoryImplement: public IItemRepository{
public void save(int id, string name){ // 純粋仮想関数の具象化
sql::mysql::MySQL_Driver *driver = sql::mysql::get_mysql_driver_instance();
// MySQLのInsert処理
// ........
}
}
具体例より、Interfaceクラスと具象クラスの対比表を作ってItemクラスの呼び出し時に渡していることがわかる(DIコンテナ)。
Itemクラスでは、Interfaceクラス型を使って処理を定義可能であるため、具象クラスが何かを知らなくても良い。つまり、Infrastructure層への参照がなくなったことがわかる。
DI
参考
説明
あるコンポーネントAが利用している別のコンポーネントBを外部から注入することにより、コンポーネントAがコンポーネントBの実装との依存関係を排除するデザインパターン。
あるモジュールA内でモジュールBを参照しないように、Aの呼び出し時にBのインスタンスを渡す(注入)ということ。
この時、受け取るインスタンスの型は、Interfaceクラスを利用することで、Aの中でBのメソッドにアクセスする際も、具象クラスの実装内容に影響を受けることがなくなる
以下は、DIを利用しない例。
// メインプログラム
include<FooClient>
class Program{
public void Main(){
FooClient client = new FooClient();
client.Execute();
}
}
// FooComponentを利用する機能
include<FooComponent>
class FooClient{
public FooClient(){
}
public void Execute(){
FooComponent component = new FooComponent();
component.Execute();
}
}
// 機能Fooを提供するコンポーネント
class FooComponent{
public void Execute(){
// ...なにか処理を行う
}
}
上記例では、FooComponentに修正があった際にFooClientの修正、再ビルド、再テストが必要になる。
そこで、FooComponentのインスタンスをFooClientに渡すことで、FooComponentの中身を知らなくても良いようにする。
以下がその例。
// メインプログラム
include<FooClient>
include<FooComponent>
class Program{
public void Main(){
FooClient client = new FooClient(new FooComponent());
client.Execute();
}
}
// FooComponentを利用する機能
include<IComponent> // Interface以外の参照がなくなった!
class FooClient{
IComponent component;
public FooClient(IComponent component){
this.component = component;
}
public void Execute(){
component.Execute();
}
}
// ComponentのInterface
class IComponent{
public virtual void Execute() = 0;
}
// 機能Fooを提供するコンポーネント
include<IComponent>
class FooComponent: public IComponent{
public void Execute(){
// ...なにか処理を行う
}
}
上記例では、FooComponentに修正があった場合でも、FooClientではFooComponentに依存していないため、修正・テストが不要になる(Interface自体に変更がない限り)
このように、インスタンスを実際に渡して依存性を排除するテクニックがDIである。
開放閉鎖の原則
拡張に対して開かれている(Open)
あるモジュールが拡張可能である場合、そのモジュールは拡張に対して開かれている(Open)と言う。
修正に対して閉じている(Closed)
あるモジュールが修正に対してソースコードに影響を受けない場合、そのモジュールは修正に対して閉じている(Closed)と言う。
つまり、 コードを修正せずに追加によって拡張可能にするテクニック である。
以下例を参考にまずい箇所を説明する。
// 呼び出し元オブジェクト
#include<iostream>
#include<string>
#include<Engine>
using namespace std;
class Car{
public void RunEngine(){
// Engineのインスタンス化
Engine engine = new Engine();
// ガソリンエンジンの起動
engine.runGasEngine();
// ハイブリッドエンジンの起動
engine.runHybridEngine();
// 他のエンジンがあればここに延々と続いていく。。。
}
}
// 呼び出し先オブジェクト
#include<iostream>
#include<string>
using namespace std;
class Engine{
public void runGasEngine(){
// ガソリンエンジン起動処理
}
public void runHybridEngine(){
// ハイブリッドエンジン起動処理
}
// 他のエンジンが追加されたらここを修正
}
例では、参照元のCarオブジェクトでエンジンを追加してEngineロジックを実行したい場合、参照先のEngineクラスとそのテストコードの修正が必要となる。
つまり、Engineクラスは修正に対して閉じていないということになる。
そこで、上記例を開放閉鎖の法則に従って修正したコードを以下に示す。
// 呼び出し元オブジェクト
#include<iostream>
#include<string>
#include<EngineRun>
#include<GasEngine>
#include<HybridEngine>
using namespace std;
class Car{
public void RunEngine(){
RunEngine runEngine = new RunEngine();
// ガソリンエンジンの起動
runEngine.run(new GasEngine());
// ハイブリッドエンジンの起動
runEngine.run(new runHybridEngine())
}
}
// 呼び出し先のロジック実行オブジェクト
#include<iostream>
#include<string>
#include<IEngine>
using namespace std;
class RunEngine{
public void run(IEngine engine){
engine.run();
}
}
// 実行ロジックのInterfaceクラス
#include<iostream>
#include<string>
using namespace std;
class IEngine{
// エンジン実行の純粋仮想関数
public virtual void run() = 0;
}
// 実行ロジックの具象クラス
#include<iostream>
#include<string>
#include<IEngine>
class GasEngine: public IEngine{
public void run(){
// ガソリンエンジンの起動処理
}
}
#include<iostream>
#include<string>
#include<IEngine>
class HybridEngine: public IEngine{
public void run(){
// ハイブリッドエンジンの起動処理
}
}
開放閉鎖の原則を実現するためには、はじめに、出し分けるロジック(GasEngine/HybridEngineクラス)のInterfaceクラスを用意し、出し分けたいロジックは具象クラスとして実装する。
また、Interfaceを介して、ロジックを実際に実行するクラス(RunEngineクラス)を実装する。
呼び出し元(Carクラス)からは、Interfaceクラスの具象クラスをインスタンス化し、RunEngineクラスに渡している。
結果、新しいEngineクラス(例えばHydroEngineクラス)が追加されたとしても、Interfaceを介しているだけなので、呼び出し先のRunEngine/GasEngine/HybridEngineクラスは修正が不要となる。
変わりに、 Interfaceを継承した具象クラス(HydroEngineクラス)を追加し、呼び出し先に渡すだけでロジックの修正が済む。
これが開放閉鎖の原則の一例である。
用語集
- 依存
- 要素Aを実装する際に、要素Bを読み込まなければ実現できないような場合(≒import/require/include/etc..)、要素Aは要素Bに依存している。依存の方向は、「要素A → 要素B」
- ドメインロジック
- ドメイン知識を提供するためのモジュール・クラス・メソッド。
- Entity
- Entityは識別子(=ID)によって同一性を示す。
- オブジェクト自体のフィールドの変更を許容する。
- つまり、オブジェクト一つ一つが意味を持ち、一意性を持つ。そして、そのオブジェクトが持つ情報が書き換わることを許容し、書き換わったとしても同じオブジェクトであることを表している。
- ValueObject
- ValueObjectは保持する情報が同一であれば同一とみなす。
- オブジェクトが保持するフィールドが変わることを許容しない。
- つまり、オブジェクト一つ一つが不変なデータの塊であり、それらフィールドが同一であれば、複数のオブジェクトが存在しても全て同じオブジェクトとみなす事ができる。