DDDにおけるEntityについて

IDDD本を読んだりしてEntityのイメージが固まってきたので書き残します。
sample codeのコメントを詳細に説明していきます。

sample code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<?php

// エンティティ
class User {

private $id;
private $name;
private $age;
private $gender;
private $email;
private $address;

// すべてのプロパティの値をコンストラクタで設定します。
private function __construct($id, $name, $age, ?Gender $gender, $email, $address)
{
// セッターを利用してプロパティに値を格納します。
$this->setId($id);
$this->setName($name);
$this->editAgeGender($age, $gender);
$this->setEmail($email);
$this->setAddress($address);
}

// セッターでバリデートします。
public function setId($id)
{
if ($this->id) {
throw new DomainException('idの変更はできません。');
}
$this->id = $id;
}

public function setName($name)
{
$this->name = $name;
}

public function setEmail($email)
{
$this->email = $email;
}

public function setAddress($address)
{
$this->address = $address;
}

public function getGender()
{
return $this->gender;
}

// プロフィール編集の時に名前と年齢を変更できるという振る舞いです。
public function editProfile($name, $age)
{
$this->setName($name);
$this->editAgeGender($age, $this->getGender());
}

// 自身を作成するファクトリーメソッドを持ちます。
// 会員登録時のユーザーを作成するという振る舞いです。
public static function memberEntry ($id, $name, $email)
{
return new self($id, $name, null, null, $email, null);
}

// エンティティ全体の状態のバリデートを行う、これを呼び出すのはドメインサービス
public validate(ValidationNotificationHandler $handler)
{
(new UserValidator($this, $handler)).validate();
}
}

// Validatorは再利用可能な抽象バリデーター
// エンティティ個別のバリデーターにエンティティ全体のバリデートロジックをもたせます。
class UserValidator extends Validator{

private $user;

public function __construct(User $user, ValidationNotificationHandler $handler)
{
parent::__construct($handler);
$this->user = $user;
}

public function valiate()
{
if ($this->hasWarpedAgeGender()) {
$this->notificationHandler()->handleError('年齢と性別の関係が不正です。');
}
}

// 年齢と性別の関係のバリデートロジック
public function hasWarpedAgeGender()
{
if ($this->age >= 30 && $this->age < 40 && $gender === Gender::MAN) {
return true;
}
if ($this->age >= 20 && $this->age < 30 && $gender === Gender::WOMAN) {
return true;
}

return false;
}
}

原則

Entityは識別子を持って一意であり永続化されます。

Entityの振る舞いとは

多くは下記の二点のことを指します。
・自身の状態を変更するメソッド
・自身を作成するファクトリメソッド
※デザインパターンのファクトリメソッドのことではありません。

例えば sample code での振る舞いは、
・プロフィール編集の時に名前と年齢を変更できる。 (自身の状態を変更するメソッド )
・会員登録できる。 (自身を作成するファクトリメソッド)
という振る舞いをエンティティが持つことになります。

すべてのプロパティの値をコンストラクタで設定します

不完全な状態のインスタンスを作成しないようにコンストラクタですべてのプロパティを設定できるべきです。
下記のアンチパターンのようにコンストラクタで状態が完結せずにセッターでセットしてインスタンスの状態が完成するべきではありません。

1
2
3
// アンチパターン
$user = new User($id, $name);
$user->setAge(30);

もちろんインスタンスの状態が完成している物に対して、変更する目的でセッターを利用するのは問題ありません。

セッターでバリデーションを行い、プロパティに値を格納します。

セッターでバリデーションを行うことにより。正当な値がプロパティに格納されます。
コンストラクタでバリデーションは行いません、ミュータブルなエンティティにとって状態を変更する時にコンストラクタのバリデーションを利用できないからです。

複数の項目に渡るバリデーションは、オブジェクト全体を遅延バリデートする

セッターを作成し、相互にバリデーションを行うことはできません。
また、制約に関連する項目すべてを引数にとり状態を変化させるメソッドを作成すると、制約の追加や変化に応じてメソッドの引数がどんどん多くなっていき、利用しずらい振る舞いになります。
制約に関連する項目の変更がすべて揃った状態か、それとも途中の中途半端な状態かというのはエンティティに判断させることは難しくなりますので、ドメインサービスにてバリデートの実行のタイミングを判断します。
エンティティにはどのバリデーターを利用するかをだけを決めさせて、エンティティ全体のバリデートロジックはバリデーターに担当させることで、エンティティの振る舞いの責務が埋もれてしまわないようにします。
(※矛盾するように感じるかもしれませんが、プロパティ個別のバリデートはエンティティのセッターで行います。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// アンチパターン 1

// 各項目ごとのセッターで複数の項目にわたるバリデートを行う。
// この例だと、女性で25歳の状態を男性で33歳に変更したい場合に
// 変更できなくなる
public function setAge($age)
{
if ($age >= 30 && $age < 40 && $this->gender === Gender::MAN) {
throw new DomainException('30代の男性しか登録できません');
}
if ($age >= 20 && $age < 30 && $this->gender === Gender::WOMAN) {
throw new DomainException('20代の女性しか登録できません');
}

$this->age = $age;
}

public function setGender($gender)
{
if ($this->age >= 30 && $this->age < 40 && $gender === Gender::MAN) {
throw new DomainException('30代の男性しか登録できません');
}
if ($this->age >= 20 && $this->age < 30 && $gender === Gender::WOMAN) {
throw new DomainException('20代の女性しか登録できません');
}

$this->gender = $gender;
}

// アンチパターン 2
// 複数の項目に渡るバリデーションはその項目数を引数にもったメソッドでプロパティにセットする。
// その後、男性は港区だけという制約が増えると振る舞いの引数が増えどんどん扱いにくいメソッドになっていきます。
public function editAgeGender($age, ?Gender $gender)
{
if ($this->age < 30 && $this->gender === Gender::MAN) {
throw new DomainException('30歳未満の男性は登録できません');
}

if ($this->age < 25 && $this->gender === Gender::WOMAN) {
throw new DomainException('25歳未満の女性は登録できません');
}

$this->age = $age;
$this->gender = $gender;
}

検索で不要な記事を除外する

検索を行う時に不要なものが表示されて目的の記事を探しづらい時に、不要な記事に一致するキーワードを -不要 キーワード とすることで
検索から除外するキーワードを設定できます。
例えば iPhone11の記事を探している時にiPhone8の記事が沢山でてきて、iPhone8を除外したい場合は、
iPhone11 -iPhone8 と検索すると、iPhone8が除外された記事がでてきます。

また検索エンジンで、サイト内検索を行う方法もあります。
site:サイトURLとおこなうことでサイト内検索ができます。
例えば amazon内でiphone11を探したい場合は下記のように、
iphone11 site:https://www.amazon.co.jp/
とすると、amazonサイト内でのiphone11の記事に絞り込むことができます。

Hexoテーマカスタマイズ

Hexoのサイドカラムにバナーを表示の仕方を探していました。
landscapeというテーマだと
themes > landscape > layout > _partial > sidebar.ejs
のファイルを編集すると表示できました。
テーマというディレクトリ構成がWordpressに似ていますね。

無料でブログを始める方法

サーバー料金やドメインの料金をかからない方法で、ブログを始める方法を探していたら。
github pageとhexoの組み合わせがよさそうだったので、試してみました。
大体は下記のurlに通りにすれば、簡単にはじめれました。
https://qiita.com/wawawa/items/1a2f174fb29c35302543
つまずいた点は

1
hexo deploy -g

のコマンドを打つと下記のエラーが出てきました。

1
ERROR Deployer not found: git

こちらは、blogディレクトリに移動してから

1
sudo npm install hexo-deployer-git --save

を再度実行することでエラーの解消ができました。