VOとエンティティの関係
あるテーブル用に値が集まると、レコード(=エンティティ)になります
エンティティとモデル、リポジトリの関係
複数のテーブルのレコードをミキシングして「集合」を作るとリポジトリになります
リポジトリとサービスの関係
「業務」をミキシングして「集合」をつくるとサービスになります。コントローラからサービス単位でロジックを作ると「ファットコントローラ」を防ぐことができます
ファクトリ
開発における一般的な「ファクトリ」とは、複雑になりがちなオブジェクト群を簡単に組み立て、呼び出し側に対して内部構造を隠して生成する仕組みのことを指します。これに対して、DDDのファクトリでは「ユビキタス言語を用いて集約をシンプルに生成する」という意味合いが強くなります。そのためDDDのファクトリとしては、下記のようなシナリオが中心となります。
model()
DDD用語
用語 | 意味 | 説明とか |
---|---|---|
ドメイン | 業務領域・業務フロー | 使用例:BLENDは校務支援ドメインのサービスだ。成績管理ドメインの概念に「生徒コース」というものがある。 |
ドメインレイヤー(アーキテクチャ) | アプリケーションアーキテクチャ(システムの全体構成)の中でドメインロジックを持つクラス群のこと | ドメインレイヤーには、「ドメインモデル」、「ドメインサービス」、「仕様クラス」といった業務要件をカプセル化したクラスがあります。これらは特定の画面や、データストア、APIなどに依存しない独立した存在となっています。 |
ドメインモデル (ドメインレイヤー) | 業務モデル | 説明: あるドメインの構成する概念をソフトウェアで扱うため、役割毎に特化したもの。 その概念がある役割を担うときの業務要件がカプセル化されている。 基本的に特定の画面や、データストア、APIなどに依存しない ※「概念として扱えるユビキタス言語の業務要件をカプセル化したクラス」とも表現できる ※要するに成績って値変更する時こんな制約あるよね、みたいなのを全部モデルに書くということ 利点: ・実装が業務に寄り添っているため、業務の変化に追随することができる(システムは日々進化する) ・「この概念はこういうものだ」という定義が一箇所に集約されている為、ドキュメント性が高い ・定義の中には制約も含まれる為(特定の使い方をしなければエラーが出る等)、潜在的なバグが生まれにくい。 欠点: ・適当に要件定義すると、各機能はドメインモデルに依存するという関係性から全体がおかしくなる。 ・実装者の業務知識を詰め込むような作り方をするため、実装コストが高い(深い検討や勉強が必須) ・他に依存しないという制約から疎結合かつ凝集度の高い実装が常に求められるため実装コストが高い |
Entity(エンティティ ) (ドメインレイヤー) 実装例(Entity) | ドメインモデルの種類 主キーで自身の唯一性を担保するドメインモデル 可変クラス(リプレイスでは不変クラスとして扱ってる) | Students Classのオブジェクトである山田太郎(id=1)と山田太郎(id=2)は名前こそ同じだが、 id=主キーによってそれぞれが唯一の存在であることを表している。 おすすめ記事: DDDとORMのEntityを混同しないための考え方 DDD基礎解説:Entity、ValueObjectってなんなんだ – little hands’ lab |
Value Object(値オブジェクト) (ドメインレイヤー) 実装例(ValueObject) | ドメインモデルの種類 ある値をドメインモデルとして切り出したもの 唯一性ははなく、値が同じならそれらは皆同じ存在 そのシステム独自の型定義 不変クラス | よくある「school_id」は単純なintとして定義されていますが、実際色々な用件がありますよね。 ・セッションのschool_idと一致していなければならない ・nullは許可しない ・0以下にはならない だから都度チェックなどしている訳ですが、例えばこんなクラスのオブジェクトを作って、 ======================= class SchoolId{ $value; public function __constructor($value){ if($this->login_data[“school_id”] != $value) //エラー if(!is_int($value)) //エラー if($value <= 0) //エラー $this->value = $value; } } ======================= 各オブジェクトに組み込んでやります。 ======================= class Grades{ $school_id; $exam_score; public function __constructor( School_id $school_id, Int $exam_score ){ $this->school_id = $school_id; $this->exam_score = $exam_score; } } ======================= この時Gradesオブジェクトの「school_id」はInt型ではなく、SchoolId型になっています SchoolId型になっているということは、その値はschool_idの業務要件を全て満たしているということで、 もっと言えばおかしな値をセットできない作りになっているということです。 値をオブジェクト化することで、独自の型を定義し、業務要件をカプセル化する それが値オブジェクトです。 おすすめ記事: 設計要件をギッチギチに詰めたValueObjectで低凝集クラスを爆殺する – Qiita |
不変クラス | オブジェクト生成後に、その値を変更することができないクラスのこと | 更新処理などでよくある ======================= $old_data = getOldata(); $old_data->name = $post[“name”]; dataSave($old_data) ======================= という流れを、 ======================= $old_data = getOldata(); $new_data = $old_data->changeName($post[“name”]); dataSave($new_data) ======================= という感じで書くということです。 changeName()内部ではold_dataの同じクラスのオブジェクトをnewし直してます。 constructorで制約をかけた上で(この値は0以下にはならない等) こういった作りにすると、常に想定内のデータしか持っていないということが担保されます。 新規参入者など業務知識に疎い人が誤ってあり得ないデータを作ってしまうなんてことが防げます。 ※毎回必ずconstructorという共通のフィルタに値を通さざるを得ない作りということです。 おすすめ記事: 持続可能なソフトウェアのコーディング(4)不変クラス | NHN Cloud Meetup |
Aggregates(集約) (ドメインレイヤー) ⇨実装例のEntityは集約ルートになってる 実装例(Entity) | 簡単に言えば複合オブジェクトのことです。 (複数のオブジェクトを持つオブジェクト) エンティティと値オブジェクトを合体させて作ります。 またトランザクション整合性の単位で作られます。 | これとこれは同時に更新しなければというデータ整合性を考えるときには、トランザクションを貼りますよね。 集約というのはそのトランザクションの単位で各オブジェクトを合体させて、 その単位でしか保存更新させないように制約をかけることで、トランザクション整合性を必ず守らせるという仕組みのことです。 集約は集約ルートと呼ばれる親玉オブジェクトみたいなエンティティがいて、 その親玉オブジェクトがトランザクション整合性の観点から関連のあるオブジェクトを自身のプロパティとして保持するという形で構成されます。 下記のような感じです。 ======================= //集約ルート class SchoolField extends Entity{ SchoolFieldId $id; SchoolId $school_id; String $name; SchoolFieldChoice $choice; } //集約ルートSchoolFieldのメンバオブジェクト class SchoolFieldChoice extends ValueObject{ $name; $value; } $SchoolField = new SchoolField(); saveData($SchoolField) ======================= 例えば、「1つのschool_fieldは同じ選択肢を重複して持たない」という制約があったとします。 school_field_choiceを単独で保存できるように作ると、チェック方法としてはdbを見にいくとかが考えられると思いますが、これを書くかどうかは実装者次第です(チェックしないで保存という実装も可能ですよね) 要するに制約が破壊される可能性があります。 一方こうした集約オブジェクトを作り、constructorの時点で重複チェックをかけておくと、その後はこのオブジェクトを経由したロジックを書く限り必ずその制約が守られるようになります。 常に整合性の単位でまとめて保存更新をさせることにより、データの整合性を保つ それが集約です。 デメリットとしては一括更新なのでsqlパフォーマンスを最適化できないのですが、 なるべく小さい集約をたくさん作るという方針で進めることでこれはいくらか軽減できます。 |
トランザクション整合性 結果整合性 (DDDとは直接関係ないのでまとめちゃう) | データ整合性の種類 | トランザクション整合性: トランザクションを張る必要のある即時に整合性が求められるようなデータの関係性 ※決済処理など、即時に整合性を合わせないと巻き返せないレベルでその後の処理がおかしくなる場合に必要 結果整合性: 整合性は必要だけど、即時ではなく結果的に合えばいいというレベルの整合性 ※授業には生徒が必要だけど、生徒がいないからといってデータは破綻しない |
ドメインサービス (ドメインレイヤー) | 複数の集約をまたがないと実行できないドメインロジックを書く際に利用する。 言い方を変えると、 「複数集約の処理を連続的に行うこと = 1つの業務」みたいなロジックを書くときに使う | A集約データセット => A集約データ加工 => A集約データ保存 のような処理なら1つの集約で完結しますが、 A集約データセット => A集約データ保存 => A集約データ情報をB集約データセット => B集約データ保存 のように1集約で完結しないロジックもあります。 grades と grades_parts みたいですね(ちょっと違いますが…)後者の場合、 B集約データ保存をする為にはAデータをまず保存しなければならない という業務要件を崩されないようにドメインサービスにカプセル化します。controllerにベタ書きすると、その仕様を知らない人がめちゃくちゃな実装をしてしまう可能性があったり、業務要件がコードから拾いづらくなるためです。 |
仕様クラス (ドメインレイヤー) 実装例(Specify) | 複数の集約をまたがないと検証できない整合性をチェックするために利用する。 ※本当はもう少し使い方があるけど、今は必要ないかも | |
Repository(レポジトリ) (インフラレイヤー) (ドメインレイヤー) 実装例(Repository) | 集約単位で取得・保存・更新を行う | 主キーなどを渡してデータを取得・集約オブジェクト型に再構成して返したり、 集約オブジェクトを渡して保存・更新したりする select系はCI modelのものを呼び出して使っていますが、 save, update系はベタ書きにしています(別の箇所から保存・更新させないため) |
表明(Assertion or Assert) ※DDDというより契約プログラミングかな | 契約プログラミングにおける前提条件のこと リプレイスでは主にドメインモデル生成時の絶対的な前提条件として使っています。 意味不明なデータは許しません。 | 表明はバリデーションに似ています。 ========================= Assert::NotNullAndNumeric($hr_grade, ‘利用学年’); public static function NotNullAndNumeric($value, $validation_target_str){ if(!is_numeric($value) || strlen($value) == 0){ throw new AssertException(“{$validation_target_str}は数字で入力してください。”); } } ========================= 違いとしては、絶対に引っかからない前提で実装されているという点です。 ご覧の通りexceptionを投げています。 普通のバリデーションにしてはやりすぎですよね。ただ絶対に来ない想定なのでいいのです。 むしろプログラムを落とさないととんでもないバグデータが生まれます。 通常は開発環境でのみ動作させて本番では動かさないという使い方をするらしいのですが、 BLENDは割と検証が甘いことが多いのでCI環境とテストコードが整備されるまではこれでいいかなと思います。 |
実装例(Entity)
<?php
namespace domain\model;
use domain\model\Entity;
use domain\model\Assert;
use domain\model\SchoolId;
use domain\model\SchoolGradeCurriculum\CurriculumId;
use domain\model\GradeEvaluateItem\m_SaveTime as SaveTime;
use domain\model\GradeEvaluateItem\m_DisplayTime as DisplayTime;
use domain\model\GradeEvaluateItem\m_GradesColumn as GradesColumn;
use domain\model\GradeEvaluateItem\m_InputType as InputType;
use domain\model\GradeEvaluateItem\m_GradeEvaluateChoice as EvaluateChoice;
use domain\model\GradeEvaluateItem\Methods;
use domain\model\GradeEvaluateItem\GradeEvaluateItemId;
class GradeEvaluateItem extends Entity {
use Methods;
protected $id;
protected $school_id;
protected $curriculum_id;
protected $hr_grade;
protected $save_time;
protected $display_time;
protected $name;
protected $grades_column;
protected $input_type;
protected $memo;
protected $year;
protected $choices;
public function __construct(
GradeEvaluateItemId $id,
SchoolId $school_id,
CurriculumId $curriculum_id,
$hr_grade,
SaveTime $save_time,
DisplayTime $display_time,
$name,
GradesColumn $grades_column,
InputType $input_type,
$memo,
$year,
$choices=[]
) {
parent::__construct();
//前提条件チェック
Assert::NotNullAndNumeric($hr_grade, '利用学年');
Assert::NotNullAndString($name, '名称');
Assert::NotNullAndNumeric($year, '年度');
Assert::UniqueRule($this->NecessaryChoice($input_type, $choices), '入力種類が「選択肢入力」の場合は選択肢が必要です');
Assert::UniqueRule($this->NotNecessaryChoice($input_type, $choices), '入力種類が「選択肢入力」以外の場合は選択肢が不要です');
if(!empty($choices)){
Assert::MatchClassRecursive($choices, '評価項目選択肢', EvaluateChoice::ClassName());
Assert::SameObjsNotDuplication($choices,'評価項目選択肢');
}
$this->id = $id;
$this->school_id = $school_id;
$this->curriculum_id = $curriculum_id;
$this->name = $name;
$this->hr_grade = $hr_grade;
$this->save_time = $save_time;
$this->display_time = $display_time;
$this->grades_column = $grades_column;
$this->input_type = $input_type;
$this->year = $year;
$this->memo = $memo;
$this->choices = $choices;
}
//再構成
public function reBuild(
$name,
$memo,
$choices
){
//これは後ほど条件つきにしたい。まだ使われていなければ的な。
Assert::UniqueRule($this->cantRemoveChoiceAfterCreate($choices), '登録後に選択肢を減らすことはできません');
return new static(
$this->id,
$this->school_id,
$this->curriculum_id,
$this->hr_grade,
$this->save_time,
$this->display_time,
$name,
$this->grades_column,
$this->input_type,
$memo,
$this->year,
$choices
);
}
}
実装例(ValueObject)
<?php
namespace domain\model;
use domain\model\ValueObject;
use custom_exception\IllegalException;
//今の形に合わせる関係上仕方ないが、
//本来はLoginData classのインスタンスを引数に渡すべき
//内部でsessionデータを取得すべきではない
class SchoolId extends ValueObject{
protected $school_id;
public function __construct($school_id) {
$CI = & get_instance();
if(empty($school_id)){
throw new IllegalException("school_id={$CI->login_data['school_id']}の学校アクセス時にschool_idの指定がないデータが作成されようとしました");
}
if($school_id != $CI->login_data['school_id']){
throw new IllegalException("school_id={$CI->login_data['school_id']}の学校アクセス時にschool_id={$school_id}の学校データが呼び出されました。");
}
$this->school_id = $school_id;
}
public function value(){
return $this->school_id;
}
}
実装例(Specify)
現状うねうねドメインサービスに書いてあるdbアクセスを伴う整合性チェックも
こうして判定できるのが理想ですね。
ここ正直Assertionで固く弾くというよりerrorBagに詰めて画面側に返す感じにしちゃってます。
なんか2か所で似たようなdbアクセスするのは正直微妙かなと思って…
それにAssertionはシステム的な意味の側面で考えると、開発バグor攻撃に対して張っているものなので、
こういうdbにアクセスして初めて通っているか確認する系のルールだとerrorBagでいいのではないかなと思いました。(だってバグってなくても攻撃してなくても引っ掛かっちゃうよね?)
<?php
namespace domain\model\SchoolGradeCurriculum;
use repository\grade_setting\SchoolGradeCurriculumRepository;
use domain\model\SchoolGradeCurriculum;
class Specify{
public function __construct() {
}
//カリキュラムは別のカリキュラムが持つhrコースをメンバーとして保有することができない
//このロジックでは、「別のカリキュラムと既に関係性があるhrコースをSchoolGradeCurriculumが保有していないか」を判定している
//ドメインルールを正しく表現するという意味ではcourse_idを渡して検証するのが正しい姿な気もするけど、その都度sqlを発行するとパフォーマンス的に問題なので妥協している
//もしくはschoolGradeCurriculumにCurriculumCoursesを組み込む前に、CurriculumCoursesを渡して検証してする、という方法でもいいかもしれない
//(むしろそっちの方がいいのかな。course_idsを渡して、course_idsの整合性をチェックしているし)
public static function isNotCourseRelationedByOtherCurriculum(SchoolGradeCurriculum $schoolGradeCurriculum){
$relationable_courses = SchoolGradeCurriculumRepository::Create()->getRelationableCourse(
$schoolGradeCurriculum->school_id(),
$schoolGradeCurriculum->year(),
$schoolGradeCurriculum->id()
);
$relationable_course_ids = array_column($relationable_courses, 'id');
foreach($schoolGradeCurriculum->courses() as $course){
if(!in_array($course->course_id(), $relationable_course_ids)){
return false;
}
}
return true;
}
}
実装例(Repository)
<?php
namespace repository\grade_setting;
use repository\Repository;
use domain\model\UniqueId;
use domain\model\SchoolId;
use domain\model\GradeEvaluateItem;
use domain\model\GradeEvaluateItem\m_GradeEvaluateChoice as GradeEvaluateChoice;
use domain\model\GradeEvaluateItem\m_SaveTime as SaveTime;
use domain\model\GradeEvaluateItem\m_DisplayTime as DisplayTime;
use domain\model\GradeEvaluateItem\m_GradesColumn as GradesColumn;
use domain\model\GradeEvaluateItem\m_InputType as InputType;
use domain\model\GradeEvaluateItem\GradeEvaluateItemId;
use domain\model\SchoolGradeCurriculum\CurriculumId;
class GradeEvaluateItemRepository extends Repository{
private $grade_evalueate;
public function __construct(GradeEvaluateItem $grade_evalueate) {
parent::__construct();
$this->CI->load->model('GradeEvalueateItem_m');
$this->grade_evalueate = $grade_evalueate;
}
//集約保存処理
public function store($teacher_id){
$this->CI->db->trans_begin();
//評価項目保存
(self::exist($this->grade_evalueate->id())) ? $this->updateEvaluateItem($teacher_id):$this->insertEvaluateItem($teacher_id);
//評価項目選択肢保存
if(!empty($this->grade_evalueate->choices())){
foreach($this->grade_evalueate->choices() as $choice){
(self::existChoice($choice->id())) ? $this->updateEvaluateChoice($choice, $teacher_id):$this->insertEvaluateChoice($choice, $teacher_id);
}
}
if ($this->CI->db->trans_status() === FALSE){
$this->CI->db->trans_rollback();
}else{
$this->CI->db->trans_commit();
}
}
コメント