レイヤードアーキテクチャの言葉を整理していく
はじめに
アプリケーションエンジニアをしたことがある方であれば、レイヤードアーキテクチャに触れたことがある方が殆どかもしれません。近しい概念にクリーンアーキテクチャ・DDDといった概念もあります。
私も何となく把握しながらも、似たような意味の言葉が多くて概念を正しく理解しきれていませんでした。整理してみます。
レイヤードアーキテクチャとは
レイヤードアーキテクチャ(Layered Architecture)とは、アプリケーションを関心の異なる層(レイヤー)に分割し、それぞれの層に明確な責務を持たせる設計手法です。
この構造は古くから使われており、シンプルかつ理解しやすいため、Webアプリケーションや業務システムに広く採用されています。
レイヤードアーキテクチャの目的
アーキテクチャの目的は主に次の3点です:
- コードの整理と可読性の向上
- 各層の変更の影響範囲を限定する
- テスト・保守・拡張性の向上
レイヤー
一般的なレイヤードアーキテクチャは、次の4層に分かれます。
[ Interface層 ]
↓
[ Usecase層 ]
↓
[ Domain層 ]
↓
[ Infrastructure層 ]
それぞれの層の責務を以下に整理します。
Interface層(UI層、Controller)
ユーザーとの接点を担当する層です。WebであればHTTPリクエスト、CLIであればコマンド入力などを受け取ります。
主な役割は
- UI
- バリデーション
- Usecase呼び出し
- レスポンスの整形(JSONやHTML)
Usecase層(アプリケーション層)
アプリケーションの**具体的な処理の流れ(ユースケース)**を担当する層です。
主な役割は
- トランザクション制御
- 複数ドメインサービスの呼び出し
- ユーザー視点の操作単位(ビジネスルールの手続き)
Domain層
ビジネスロジックそのものを表す層であり、最も変更しにくい部分です。
主な役割は
- エンティティ(Entity)
- 値オブジェクト(ValueObject)
- ドメインサービス(DomainService)
- ビジネスルールのカプセル化
Infrastructure層
外部とのやりとりや技術的な詳細を担当する層です。
主な役割は
- データベースアクセス(Eloquent, QueryBuilderなど)
- メール送信、外部API通信
- フレームワークやライブラリ依存の処理
レイヤーの依存関係
レイヤードアーキテクチャでは、上位層が下位層に依存する構造を取ります。
[ Interface層 ]
↓
[ Usecase層 ]
↓
[ Domain層 ]
↓
[ Infrastructure層 ]
つまり、UI層がアプリケーション層に依存し、アプリケーション層がドメイン層に依存し…という一方向の依存関係です。上位->下位に依存していきます。ドメイン層は最も内側の層として、他の層に依存しません。これによって、ビジネスロジックを外部の技術変更から守ることができます。
レイヤードアーキテクチャのメリット
- 責務が明確: 各層で「何をやるべきか」が分かれているため、迷わず実装できる
- テストしやすい: 上位層をモック化して下位層をテストしやすくなる
- 再利用性が高い: ドメイン層がUIやDBに依存していないため、API・バッチ・CLIで共通利用できる
- 保守しやすい: 技術的な変更(DB, メール, UI)を他層に波及させにくい
- チーム開発しやすい: 層ごとに役割を分担しやすく、専門性を活かせる
実装例
ユーザー登録の処理をレイヤードアーキテクチャにして実装例を整理します。
[ Interface層 ]
namespace App\Http\Controllers;
use App\UseCases\RegisterUser\RegisterUserUseCase;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function store(Request $request, RegisterUserUseCase $useCase)
{
$validated = $request->validate([
'name' => ['required'],
'email' => ['required', 'email'],
'password' => ['required', 'min:8'],
]);
$useCase->execute($validated);
return response()->json(['message' => 'ユーザー登録完了']);
}
}
[ Usecase層 ]
namespace App\UseCases\RegisterUser;
use App\Domain\User\User;
use App\Domain\User\UserFactory;
use App\Domain\User\UserRepository;
use App\Domain\User\UserNotifier;
use Illuminate\Support\Facades\DB;
class RegisterUserUseCase
{
public function __construct(
private readonly UserFactory $userFactory,
private readonly UserRepository $userRepository,
private readonly UserNotifier $userNotifier,
) {}
public function execute(array $input): void
{
DB::transaction(function () use ($input) {
$user = $this->userFactory->create($input['name'], $input['email'], $input['password']);
$this->userRepository->save($user);
$this->userNotifier->sendWelcomeMail($user); // ← 追加されたメール送信処理
});
}
}
[ Domain層 ]
namespace App\Domain\User;
use Illuminate\Support\Facades\Hash;
use InvalidArgumentException;
class UserFactory
{
public function __construct(
private readonly UserDomainService $domainService
) {}
public function create(string $name, string $email, string $plainPassword): User
{
// ビジネスルール①:メールアドレス正規化
$normalizedEmail = strtolower($email);
// ビジネスルール②:名前にNGワードを含めない
if ($this->containsProhibitedWords($name)) {
throw new InvalidArgumentException('名前に不適切な単語が含まれています');
}
// ビジネスルール③:脆弱なパスワードを拒否
if ($this->isWeakPassword($plainPassword)) {
throw new InvalidArgumentException('パスワードが脆弱すぎます');
}
return new User(
name: $name,
email: $normalizedEmail,
hashedPassword: Hash::make($plainPassword),
role: 'user', // ビジネスルール④:デフォルトロール
);
}
private function containsProhibitedWords(string $name): bool
{
$ngWords = ['admin', 'god', 'null'];
foreach ($ngWords as $ng) {
if (str_contains(strtolower($name), $ng)) {
return true;
}
}
return false;
}
private function isWeakPassword(string $password): bool
{
$weakList = ['password', '123456', 'qwerty'];
return in_array(strtolower($password), $weakList, true);
}
}
namespace App\Domain\User;
interface UserRepository
{
public function save(User $user): void;
}
namespace App\Domain\User;
interface UserNotifier
{
public function sendWelcomeMail(User $user): void;
}
[ Infrastructure層 ]
namespace App\Infrastructure\Repositories;
use App\Domain\User\User;
use App\Domain\User\UserRepository;
use App\Models\User as EloquentUser;
class EloquentUserRepository implements UserRepository
{
public function save(User $user): void
{
EloquentUser::create([
'name' => $user->name,
'email' => $user->email,
'password' => $user->hashedPassword,
]);
}
}
namespace App\Infrastructure\Notification;
use App\Domain\User\User;
use App\Domain\User\UserNotifier;
use Illuminate\Support\Facades\Mail;
class MailUserNotifier implements UserNotifier
{
public function sendWelcomeMail(User $user): void
{
Mail::raw("ようこそ、{$user->name} さん!", function ($message) use ($user) {
$message->to($user->email)
->subject('登録ありがとうございます');
});
}
}
Usecase層とDomain層の違い
Usecase層とDomain層はどちらもロジックを表現しているので、違いをあまり分かっていませんでした。
- Usecaseは「操作の流れ」を組み立てる場所
- Domainは「処理の意味・ルール」を表現する場所
| 項目 | Usecase | Domain |
|---|---|---|
| 主な役割 | アプリケーションの**操作の流れ(処理の手順)**を表現 | 業務知識・ビジネスルールの意味や振る舞いを表現する |
| 関心 | 「何を、どの順序でやるか?」 | 「それはどういう意味か?正しいか?」 |
| 処理の例 | ・ユーザーを登録 → メール送信 → 通知登録 | ・パスワードをハッシュ化する ・メールアドレスを正規化する |
| 依存するもの | 複数の Domain, Repository, Notifier などを組み合わせる | 純粋なロジックのみ(副作用なしが理想) |
| 層の位置づけ | アプリケーション層 | ドメイン層(中核) |
| テスト単位 | 処理の**流れ(E2E寄り)**を検証 | 処理の**ルール(ロジック)**をユニットで検証 |
今回の実例のサンプルでも分かるように。
Usecase(操作の流れを制御)
// step1:userのmodelを作る
$user = $this->userFactory->create($input['name'], $input['email'], $input['password']);
// step2:userのdataを登録する
$this->userRepository->save($user);
// step3:mailを送信する
$this->userNotifier->sendWelcomeMail($user); // ← 追加されたメール送
実際にどんな値が登録されているか、どんなルールで処理が行われているか、どこに送信されているか。などはusecase走らないのです。
Domain(ビジネスルールを表現)
// ビジネスルール①:メールアドレス正規化
$normalizedEmail = strtolower($email);
// ビジネスルール②:名前にNGワードを含めない
if ($this->containsProhibitedWords($name)) {
throw new InvalidArgumentException('名前に不適切な単語が含まれています');
}
// ビジネスルール③:脆弱なパスワードを拒否
if ($this->isWeakPassword($plainPassword)) {
throw new InvalidArgumentException('パスワードが脆弱すぎます');
}
Domainにはこのビジネス固有のルールが記載されています。
Discussion