記事の背景
ドメイン駆動設計入門という書籍を職場の同僚からお薦めしてもらい、ぱらぱらっと5章くらいまで読んでみました。
なるほどたしかに具体をイメージしながら理解が進むので、わかりやすいなぁと。
また、当書籍の最初の方に、とりあえず、Value Object、Entity、Domain Service、Repositoryの4つの要素で最低限の準備が整って、アプリケーションとして組み立てることができるという記載があったので、 一応そこまでの来年を学べる5章までを読んだタイミングでハンズオンをしてみようと思いたち、簡単なサンプルコードを自分で作ってみました。
実務でLaravelを使っているので、Laravelの中に組み込んだらどういう実装するのかな?という点を考えながら、ハンズオンをしてみたので、この記事ではその記録を共有したいなと思います。
この記事を普通に読んでいても面白くないと思ったので、AさんとBさんの会話形式での記事にしているので、ライトな気持ちで読み進めてもらえたら嬉しいなと思っています。
プロローグ
Bさん「Aさん、最近“DDD”ってよく聞くんですけど、正直“どこでもドア”の略ですか?」
Aさん「惜しい!“ドメイン駆動設計”の略だよ。どこでもドアほど便利じゃないけど、システムを“変更、拡張しやすく”する設計手法なんだ。」
Bさん「おお…!なんかワクワクしてきました!」
1. DDDって何?MVCと何が違うの?
Aさん「MVCは“画面やAPIの整理整頓”が得意。でも、ビジネスルールがコントローラーやモデルに散らばりがち。DDDは“ビジネスの本質”をコードの中心に据えるんだ。」
Bさん「つまり、MVCは“部屋の片付け”、DDDは“家の設計図”みたいなものですか?」
Aさん「その例え、いいね!DDDは“家の設計図”をしっかり作ることで、増築やリフォームも楽になるんだ。今回はユーザー登録処理をする簡易的なサンプルコードを実装しながら理解していこう!」
要点まとめ
- MVCは「見た目やAPIの整理」
- DDDは「ビジネスの複雑さを整理し、コードに反映する」
- DDDで設計すると、変更や拡張に強くなる!
2. レイヤー構成の秘密
Aさん「実装進める上で、まずは責務ごとに4つの層に分けて考えることから始めようか」
Bさん「なんで4つも層を分けるんですか?正直、面倒くさそう…」
Aさん「“面倒くさい”のは最初だけ!層を分けることで“責任の押し付け合い”がなくなる。たとえば“DB変えたい”ってなったとき、インフラ層だけ直せばOK。ビジネスルールのテストも“ドメイン層”だけで済むから、バグも減るよ。」
Aさんのワンポイント解説
-
ドメイン層:ビジネスルールや業務知識を表現する層
- 例:エンティティ、値オブジェクト、リポジトリインターフェースなど
- アプリケーション層:ユースケース(業務の流れ)を実現する層
- インフラ層:データベースや外部サービスとのやりとりを担当する層
- プレゼンテーション層:ユーザーとシステム間のインターフェースを担当する層
3. もしDDDじゃなかったら…?
Bさん「ちなみにLaravelはMVCで初心者でもなんとなくいい感じにプログラム作れちゃいますよね?MVCだけだと、何が困るんですか?」
Aさん「例えば“ユーザー登録のルール”がコントローラーやモデルにバラバラに書かれてたら、仕様変更のたびに“宝探し”になるよ。DDDなら“ビジネスルールはここ!”って決まってるから、迷子にならない。」
4. ディレクトリ構成を決めよう
Bさん「DDD進める上で、まずわからないのですが、Laravelのプロジェクトの中で、DDDの場合は、どこに何を置けばいいんですか?」
Aさん「色々考えられるけど、今回はこんな感じで分けてみよう!」
app/
├── Domain/
│ └── User/
│ ├── User.php
│ ├── UserRepositoryInterface.php
│ └── ValueObjects/
│ ├── Email.php
│ ├── Name.php
│ └── Tel.php
├── Application/
│ └── User/
│ └── UserRegisterService.php
├── Infrastructure/
│ └── User/
│ └── EloquentUserRepository.php
├── Http/
│ └── Controllers/
│ └── UserController.php
├── Providers/
│ └── AppServiceProvider.php
5. ドメイン層を作ろう
Bさん「値オブジェクトって何ですか?」
Aさん「システム固有の“値そのもの”を表す小さなオブジェクトだよ。たとえば今回はユーザーの属性としてEmailやNameがあるよね。それが値オブジェクト。こうやってオブジェクトにしておくことで、Emailなら“正しい形式か”を保証できるし、Nameなら“姓と名”を分けて管理できる。こうすることで、バリデーションや値の扱いが一箇所にまとまるんだ。」
Email.php
<?php
namespace App\Domain\User\ValueObjects;
class Email
{
private string $value;
public function __construct(string $value)
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('メールアドレスが不正です');
}
$this->value = $value;
}
public function value(): string
{
return $this->value;
}
}
Bさん「なるほど、これなら、どこでEmailを使っても“必ず正しい形式”って保証できるんですね!」
Aさん「そうだね。続けて電話番号や氏名も作っていこう」
Name.php
<?php
namespace App\Domain\User\ValueObjects;
class Name
{
private string $firstName;
private string $lastName;
public function __construct(string $firstName, string $lastName)
{
$this->firstName = $firstName;
$this->lastName = $lastName;
}
public function firstName(): string
{
return $this->firstName;
}
public function lastName(): string
{
return $this->lastName;
}
}
Tel.php
<?php
namespace App\Domain\User\ValueObjects;
class Tel
{
private string $value;
public function __construct(string $value)
{
// 必要に応じて電話番号のバリデーションを追加
$this->value = $value;
}
public function value(): string
{
return $this->value;
}
}
Aさん「続いてエンティティだね!」
Bさん「エンティティって何ですか?」
Aさん「“一意なID”を持つ、システムの中核となるオブジェクトだよ。ユーザーはIDで区別されるからエンティティになる。値オブジェクトを使って“正しい値”しか持てないようにするのがポイント!」
<?php
namespace App\Domain\User;
use App\Domain\User\ValueObjects\Email;
use App\Domain\User\ValueObjects\Name;
use App\Domain\User\ValueObjects\Tel;
class User
{
private ?int $id;
private Email $email;
private Name $name;
private Tel $tel;
private string $passwordHash;
public function __construct(?int $id, Email $email, Name $name, Tel $tel, string $passwordHash)
{
$this->id = $id;
$this->email = $email;
$this->name = $name;
$this->tel = $tel;
$this->passwordHash = $passwordHash;
}
public function id(): ?int
{
return $this->id;
}
public function email(): Email
{
return $this->email;
}
public function name(): Name
{
return $this->name;
}
public function tel(): Tel
{
return $this->tel;
}
public function passwordHash(): string
{
return $this->passwordHash;
}
}
いよいよドメイン層も大詰めだね。リポジトリインターフェイスを実装しよう。
Bさん「あれそもそもリポジトリって何のためにあるんですか?」
Aさん「“ドメイン層がDBや外部サービスに依存しないようにする”ための“窓口”だよ。ドメイン層はあくまで、ビジネスルールや知識を表現するための責務だから、ここではインターフェースだけ定義して、実際に外部システムに連携するような保存処理はインフラ層に任せるように気をつけてね。」
<?php
namespace App\Domain\User;
interface UserRepositoryInterface
{
public function save(User $user): User;
public function findByEmail(string $email): ?User;
}
要点まとめ
- 値オブジェクトで「正しい値」を保証
- エンティティで「一意な存在」を表現
- リポジトリで「DB依存からの解放」
6. アプリケーション層を作ろう
Aさん「次は2つ目の層。アプリケーション層の実装に進もう!ここではアプリケーションサービスを実装するよ」
Bさん「アプリケーションサービスって何をするんですか?」
Aさん「“ユースケース”を実現する層だよ。たとえば“ユーザー登録”の流れ(重複チェック→生成→保存)をまとめる。ビジネスルール自体はドメイン層に任せて、流れの制御や外部連携を担当するんだ。」
<?php
namespace App\Application\User;
use App\Domain\User\User;
use App\Domain\User\UserRepositoryInterface;
use App\Domain\User\ValueObjects\Email;
use App\Domain\User\ValueObjects\Name;
use App\Domain\User\ValueObjects\Tel;
class UserRegisterService
{
private UserRepositoryInterface $userRepository;
public function __construct(UserRepositoryInterface $userRepository)
{
$this->userRepository = $userRepository;
}
public function handle(string $email, string $password, string $firstName, string $lastName, string $tel): User
{
// 既存ユーザーの重複チェック
if ($this->userRepository->findByEmail($email)) {
throw new \Exception('既に登録されています');
}
// パスワードハッシュ化
$passwordHash = password_hash($password, PASSWORD_BCRYPT);
// Userエンティティ生成
$user = new User(
null,
new Email($email),
new Name($firstName, $lastName),
new Tel($tel),
$passwordHash
);
// 保存
return $this->userRepository->save($user);
}
}
Bさん「“流れ”をここでまとめて、ビジネスルール自体はドメイン層に任せるんですね!」
要点まとめ
- ユースケースの流れを一箇所に集約
- 外部連携やバリデーションもここで制御
- ドメイン層は“ルール”に集中できる
7. インフラ層を作ろう
Bさん「インフラ層って何をするんですか?」
Aさん「DBや外部サービスとのやりとりを担当するよ。ドメイン層のインターフェースを実装して、実際の保存・取得処理を書くんだ。」
<?php
namespace App\Infrastructure\User;
use App\Domain\User\User;
use App\Domain\User\UserRepositoryInterface;
use App\Models\UserEloquent;
use App\Domain\User\ValueObjects\Email;
use App\Domain\User\ValueObjects\Name;
use App\Domain\User\ValueObjects\Tel;
class EloquentUserRepository implements UserRepositoryInterface
{
public function save(User $user): User
{
$model = new UserEloquent();
$model->email = $user->email()->value();
$model->password = $user->passwordHash();
$model->first_name = $user->name()->firstName();
$model->last_name = $user->name()->lastName();
$model->tel = $user->tel()->value();
$model->save();
return new User(
$model->id,
$user->email(),
$user->name(),
$user->tel(),
$user->passwordHash()
);
}
public function findByEmail(string $email): ?User
{
$model = UserEloquent::where('email', $email)->first();
if (!$model) {
return null;
}
return new User(
$model->id,
new Email($model->email),
new Name($model->first_name, $model->last_name),
new Tel($model->tel),
$model->password
);
}
}
※なお、モデルはUserEloquentとしていて、以下が実装です。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* UserEloquentクラス
* ユーザー情報を管理するEloquentモデルクラス
* データベースとのマッピングを行う。バリデーションやビジネスロジックは行わない
*/
class UserEloquent extends Model
{
protected $table = 'users';
protected $fillable = ['email', 'password', 'first_name', 'last_name', 'tel'];
}
Bさん「ここでEloquentを使ってDBとやりとりしてるんですね。もしDBを変えたくなったら、このクラスだけ差し替えればいいんですか?」
Aさん「その通り!インターフェースでやりとりしてるから、他の実装に差し替えるのも簡単だよ。」
要点まとめ
- DBや外部サービスとのやりとりはここだけ!
- ドメイン層は“DB知らず”でOK
- 実装の差し替えも楽々
8. サービスプロバイダーでバインドしよう
Bさん「バインドって何ですか?」
Aさん「LaravelのDI(依存性注入)コンテナに“このインターフェースにはこの実装を使ってね”と教える作業だよ。これをしないと、Laravelはどのクラスを使えばいいか分からなくてエラーになるんだ。」
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Domain\User\UserRepositoryInterface;
use App\Infrastructure\User\EloquentUserRepository;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
UserRepositoryInterface::class,
EloquentUserRepository::class
);
}
public function boot(): void
{
//
}
}
要点まとめ
- バインドしないと依存性注入できない!
- インターフェースと実装の“橋渡し”
- テストや実装切り替えも簡単
9. プレゼンテーション層を作ろう
Bさん「コントローラーは何をするんですか?」
Aさん「リクエストを受け取って、アプリケーションサービスに処理を委譲する役割だよ。ビジネスロジックは持たせず、橋渡し役に徹するのがポイント!ここにビジネスロジックを詰め込むといわゆるファットコントローラになるから気をつけて」
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Application\User\UserRegisterService;
class UserController extends Controller
{
private UserRegisterService $registerService;
public function __construct(UserRegisterService $registerService)
{
$this->registerService = $registerService;
}
public function createForm()
{
return view('user.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'email' => 'required|email',
'password' => 'required|min:8',
'first_name' => 'required',
'last_name' => 'required',
'tel' => 'required',
]);
try {
$this->registerService->handle(
$validated['email'],
$validated['password'],
$validated['first_name'],
$validated['last_name'],
$validated['tel']
);
return redirect()->route('user.create')->with('success', 'ユーザー登録完了');
} catch (\Exception $e) {
return back()->withErrors($e->getMessage());
}
}
}
要点まとめ
- コントローラーは“橋渡し”に徹する
- ビジネスロジックは持たせない
- ユーザー体験の窓口
10. マイグレーションとDB準備
Bさん「DBのテーブルはどう作ればいいですか?」
Aさん「usersテーブルに必要なカラムを用意しよう。first_name, last_name, tel も忘れずに!」
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('email')->unique();
$table->string('password');
$table->string('first_name');
$table->string('last_name');
$table->string('tel');
$table->timestamps();
});
}
要点まとめ
- DB設計も“現実世界”に合わせて
- カラム名や型も明確に
- 変更があればマイグレーションで対応
11. まとめとこれから
Bさん「DDDって、最初は“どこでもドア”みたいに魔法の道具かと思ったけど、実は“設計図”をしっかり描くための道具なんですね!」
Aさん「その通り!設計図がしっかりしていれば、どんな家(システム)も安心して建てられるよ。」
ということで、簡単にDDDのハンズオン記事みたいなものを書いてみました。
少しでも理解の助けになったでしょうか?
途中結構省略しながらの記事になっているので、大事なのは実際にハンズオンしながら実装してみることかなと思っています。
実際にコードを書きながらDDDの理解を進めていっていただければ幸いです。