前回までの振り返り
第三回では、ドメイン駆動設計について、基本的なことを学びました。
今回は、ドメイン駆動設計勉強後に出た疑問点、「インフラ層のModelとドメイン層のModelを分ける意味はあるのか」を考えていきます。
前提
第三回で写経したコードを中心に考えます。
構成
app
├── Console
├── Domain
│ ├── Models
│ │ └── User.php
│ ├── Repositories
│ │ └── UserRepositoryInterface.php
│ └── Services
│ └── UserService.php
├── Exceptions
├── Http
│ ├── Controllers
│ │ └── UserController.php
│ ├── Middleware
│ └── Requests
├── Infrastructure
│ └── Eloquent
│ ├── Models
│ │ └── EloquentUser.php
│ └── Repositories
│ └── EloquentUserRepository.php
└── Providers
└── AppServiceProvider.php
インフラ層のModel
<?php
namespace App\Infrastructure\Eloquent\Models;
use Illuminate\Database\Eloquent\Model;
class EloquentUser extends Model
{
protected $table = 'users';
protected $fillable = [
'name',
'birthday',
];
}
ドメイン層のModel
<?php
namespace App\Domain\Models;
class User
{
public int $id;
public string $name;
public string $birthday;
public function __construct(int $id, string $name, string $birthday)
{
$this->id = $id;
$this->name = $name;
$this->birthday = $birthday;
}
public function calcAge()
{
this->birthdayを用いて年齢を計算
}
}
インフラ層のモデルでは、DB取得したデータをEloquentUserという名前のクラスに保存しています。
ドメイン層のモデルでは、EloquentUserのデータを、新たにUserクラスとして保持し、そのデータに対して行う操作が定義されています。
なぜこのような形にするのか
ドメイン駆動設計の勉強をして、EloquentUserに保存したデータを、Userクラスにわざわざ入れ替える意味がよくわからなかったので調べてみました。
理由1. データ構造がテーブルと同じになってしまうから
一つ目の理由が、データ構造がテーブルと同じになってしまうためです。
インフラ層のModelで以下のように値を保持しているとします。
class EloquentUser extends Model
{
protected $table = 'users';
protected $fillable = [
'name',
'birthday',
];
}
この時、name,birthdayはともにDBではVARCHAR型、つまりStringで渡されます。
そしてドメイン層のモデルに渡されて、以下の形で保存されます。
class User
{
public int $id;
public string $name;
public string $birthday;
public function __construct(int $id, string $name, string $birthday)
{
$this->id = $id;
$this->name = $name;
$this->birthday = $birthday;
}
この時、nameとbirthdayはstring型で渡ってきています。しかし、「birthdayは日付を表すデータであり、数字8桁出なければいけない」という制約があったとしたらどうでしょうか?
今の状態では、birthdayに不正な値が混入してしまう可能性があります。
そこで、birthdayの方を、日付を表す型、Dateに変換します。
class User
{
public int $id;
public string $name;
public Date $birthday;
public function __construct(int $id, string $name, string $birthday)
{
$this->id = $id;
$this->name = $name;
$this->birthday = Date.create($birthday); //string型をDate型に変換
}
このように、インフラ層のオブジェクトとは違うデータ構造を持たせたい時があるので、インフラ層とドメイン層でModelを分ける必要があります。
ドメイン駆動設計では、値オブジェクトと呼ばれる、値のルールをクラスとして表現したものを扱うことができます。上に挙げた例で言えば、「8桁の数字でなければいけない」という制約を持つDateが値オブジェクトにあたります。
プリミティブな型を使わず、値オブジェクトにすることで、ビジネスルールの適用をやりやすくなります。
理由2. 単一責任の原則
二つ目の理由は、単一責任の原則です。
SOLIDの「S」、クラスは単一の責任を持つべきという原則です。
「責任」というものが、何を指すのかというと、そのクラスで「何の処理を行うのか」、もっというと、「コードを書くときに、気にかける事柄」です。
これが、一つのクラスに二つも三つもあってはいけない、なぜなら、その責任の1つに変更を加えると、知らないうちに他の責任に影響を与える可能性があり、バグが発生しやすくなるから。というのがこの原則の概要です。
では、ドメイン層のModelと、インフラ層のModelを、分けずに一つのクラスで実現しようとしたらどうなるでしょうか?
インフラ層のModelは、DBからデータを取得するという責任を持っています。それに対して、ドメイン層のModelは、ビジネスルールに基づいた処理という責任を持っています。
つまり、ドメイン層のModelと、インフラ層のModelを分けなかった場合、一つのクラスで複数の責任を持つことになるため、バグが起こる可能性が高くなり、よろしくないです。
最後に
少し短いですが、ドメイン駆動設計の勉強をして出た疑問点について調べました。
調べてみると、よく考えたら当たり前のことだよなーと思ったのですが、やはり、知識として知っているのと、自分でコードを書いて理解するのとでは違いますね。
次回は、この設計について学ぶ、という感じではなく、設計全般についての考え方を勉強してみるつもりです。(もしかしたら変わるかもしれません)
参考