WHITEPLUS TechBlog

株式会社ホワイトプラスのエンジニアによる開発ブログです。

クリーンアーキテクチャの層の基本:役割・依存方向・実務で効いたポイント

はじめまして!株式会社ホワイトプラス、コアシステム開発Gの たなか です。

当社では、クリーンアーキテクチャやドメイン駆動設計(Domain-Driven Design / DDD)をベースに設計や実装を行っています。 が、なんと私、入社するまでどちらの設計手法にもちゃんと触れたことがありませんでした 😵‍💫

前職までの環境で意識していたのは「フレームワークのお作法に則る」程度で、特定の設計手法を意識して使っていたのは Strategy パターンぐらいです。

そんな私でも設計業務を担当させてもらうようになり、層構造を理解していく中で、設計の見え方が大きく変わりました。

この記事では、DDD の思想を踏まえたクリーンアーキテクチャの層構造と、その仕組みが実務でどのように機能しているかを整理して紹介します。

クリーンアーキテクチャの層の基本

クリーンアーキテクチャでは、依存を内向きに保ちながらシステムを複数の層に分けます。
各層には「どこで何をするか」が明確に定義されており、以下でその役割を整理します。

ざっくりいうと 主な役割
プレゼンテーション層 利用者との接点 入力を受け取り、結果を返す(Controller や View 等)
アプリケーション層 処理の進行役 処理の流れを組み立て、必要なドメインの操作を指示する
ドメイン層 業務ルールを表す役 ビジネスのルールや判断基準をコードで表し、状態や振る舞いの正しさを保証する
インフラ層 外の世界との接点 DB・APIなど外部とのやり取りを担い、永続化や通信などを実装する

プレゼンテーション層からアプリケーション層、ドメイン層、インフラ層へと一方向に依存が向かう参照関係と、インフラ層がドメイン層に対して実装を提供する関係を表した図
層間依存の方向性

上の層が下の層を呼び出すのが基本ルールですが、インフラ層だけは少し関係が異なります。

ユーザー登録を例にすると、アプリケーション層が「ユーザーを登録する」ユースケースを実行する際に、ドメイン層で定義した User オブジェクトを生成し、インフラ層で定義したリポジトリを通じて実際の保存処理を行います。

このように、ドメイン層はあくまでビジネスルールや意味付けを担う部分であり、DB 操作などの具体的な手段には関与しません。

実装を始めてすぐ感じた違和感

クリーンアーキテクチャを用いた構成に触れた当初、メソッドが execute() 1 つしかないクラスの存在に驚きました。引数を受け取って別クラスの関数に渡すだけであれば、このクラスを挟む必要はないように見えたからです。

同じような処理が別の箇所にもあった時に「共通処理を直接呼ぶだけでいいのでは?」と感じることもありました。それは、当時、なぜこのような構成になっているのかを理解しきれていなかったためです。

実務を通じて見えてきた構造の意図

コードを読み進めたりレビューを重ねるうちに、層を分けること自体が“変更に強い構造”を作るための前提であることが分かってきました。

要点は次の三つです。

1. アプリケーション層は「進行役」

ユースケース単位で処理の流れを組み立て、必要なドメイン操作を呼び出します。
ここにビジネスルールを“書き込む”のではなく、ドメインの振る舞いをどう組み合わせるかに集中します。

当社の運用:UseCase と ApplicationService を使い分ける

当社では、アプリケーション層の中で UseCase と ApplicationService を使い分けています。

  • UseCase
    • ユーザーの操作やイベントの単位
    • 入力に基づき、ドメインモデルの生成・使用・永続化/復元を所定の順序で依頼する
  • ApplicationService
    • 複数ユースケースにまたがる共通の処理や、外部システム(API・サービス)との協調をまとめる
      • 「何を通知・連携したいか」をアプリケーション層で定義し、「どのように連携するか」をインフラ層で実装する
    • 特定の技術・ライブラリに依存する処理をドメインから隔離
      • 例:Bladeテンプレートエンジンを使用して HTML メール文面を生成するジェネレーター

たとえばユーザー更新に伴う通知や複数ユースケースで共通の前後処理を ApplicationService に集約すると、ユースケース本体は「何を達成するか」に集中でき、横断処理の変更は ApplicationService 側に閉じ込められます💡

2. ドメイン層は「業務ルールの中核」

Entity や ValueObject(値オブジェクト)など、状態と振る舞い・制約を表現する要素を中心に構成します。
値の検証や不変条件(整合性)の維持、ルールに基づく判断を担い、User::register() や Order::cancel() のような 意味を持った振る舞い を定義します。
これにより、UseCase からは「何をしたいのか」を意図のままに呼び出せるようになります。

たとえば、ユーザーを登録するユースケースでは User::register() を実行して登録用の User オブジェクトを生成し、インフラ層で永続化します。

3. インフラ層は「手段の実装」

リポジトリや外部 API 連携など、手段の選択やライブラリ依存を引き受けます。
ドメイン層に定義したインターフェースを具象クラスで実装し、差し替え可能な構造を保ちます。
Strategy パターンの経験があると、ここは理解しやすい領域でした。

以下で簡易サンプルを提示します。

ドメイン層:
<?php

declare(strict_types=1);

namespace User\Domain;

use User\Domain\Type\Email;
use User\Domain\Type\Exception\DomainLogicException;
use User\Domain\Type\Identifier\UserId;
use User\Domain\Type\UserName;

class User
{
    public function __construct(
        public readonly ?UserId $id, // DB 採番前は null
        public readonly UserName $name,
        public readonly Email $email,
    ) {
    }

    /** ユーザー登録という「意味を持った振る舞い」(ID はまだ無い) */
    public static function register(UserName $name, Email $email): self
    {
        if ($name->isEmpty() || !$email->isValid()) {
            throw new DomainLogicException('invalid input');
        }

        return new self(null, $name, $email);
    }

    /** 保存後、DB 採番 ID を含む完全状態を再構成 */
    public static function of(UserId $id, UserName $name, Email $email): self
    {
        return new self($id, $name, $email);
    }
}
<?php
declare(strict_types=1);

namespace User\Domain;

interface UserRepositoryInterface
{
    /** 挿入して発番された ID を反映したインスタンスを返す */
    public function save(User $user): User;
}
アプリケーション層:
<?php

declare(strict_types=1);

namespace User\Application\Register;

use User\Domain\User;
use User\Domain\UserRepositoryInterface;
use User\Domain\Type\Email;
use User\Domain\Type\UserName;

class RegisterUserUseCase
{
    public function __construct(
        private UserRepositoryInterface $repository,
    ) {
    }

    public function execute(string $name, string $email): void
    {
        $user = User::register(new UserName($name), new Email($email));
        $this->repository->save($user);
    }
}
インフラ層(Laravel / Eloquent 実装例):
<?php

declare(strict_types=1);

namespace User\Infra\DB\Repository;

use App\Models\User as EloquentUser;
use User\Domain\User;
use User\Domain\UserRepositoryInterface;
use User\Domain\Type\Identifier\UserId;

class UserRepository implements UserRepositoryInterface
{
    public function save(User $user): User
    {
        $row = EloquentUser::create([
            'name'  => $user->name->value(),
            'email' => $user->email->value(),
        ]);

        $id = new UserId((string) $row->getKey());

        return User::of($id, $user->name, $user->email);
    }
}

層を意識するようになって変わったこと

層ごとの責務と依存方向を意識するようになってから、コードの読み方・直し方・共有の仕方が変わりました。

以前は「どこに書くべきか」で迷う場面が多かったのですが、今では自然と「これはどの層の関心事か」を考えるようになり、判断に迷う時間が減っています。

具体的には次の点で効果を感じています。

  • 見通しの良さが増した
    • 各層の役割がはっきりしているため、処理の流れや設計意図を汲み取りやすくなった
    • 修正が必要な箇所も層単位で当たりをつけやすく、PR の差分も境界に沿ってまとまりやすい
  • レビューの観点が揃うようになった
    • 「このロジックはドメイン寄り」「この処理はインフラに寄せたい」といった層の言葉で基準を共有しやすくなり、議論が短くなった
    • 一方で、責務の切り分けに迷うケースはまだあり、その際はレビューやメンバー相談で解像度を上げている
  • 責務の混在に気づきやすくなった
    • ユースケースの進行(アプリケーション)と業務ルール(ドメイン)が混ざっていないか、依存方向が逆流していないかを早い段階で気づけるようになった
  • テスト単位が明確になった
    • ドメインはルールの検証、アプリケーションはユースケースのフロー検証、インフラは接続の確認と、粒度の違うテストを分けて書けるようになった

結果として、変更時の不安が「どこを触るか」に収束し、影響範囲の説明や作業ボリュームの見積もりもしやすくなっています。

まとめ

最初は、層を分けることでクラスや呼び出し段数が増えて“遠回り”に見える(=形だけ冗長)と感じていました。execute() だけの UseCase はその一つです。

責務と呼び出し方向を意識して使い分けることで、変更点の局所化、依存の逆流防止、差し替えのしやすさ、テスト単位の明確化などが得られ、結果として設計の意図が読み取りやすくなり、変更にも対応しやすい土台になります。

層構造は単なる構造分けではなく、チーム開発の共通言語としても機能します。責務が明確になることでコードの可読性と一貫性も高まり、層ごとの関心事をはっきりさせておくと、設計の見通しも変更のしやすさも着実に上がります。

さいごに

ホワイトプラスでは、ビジョンバリューに共感していただけるエンジニアを募集しています!
ネットクリーニングの「リネット」など「生活領域×テクノロジー」で事業を展開している会社です。

どんな会社か気になった方は、オウンドメディア「ホワプラSTYLE」をぜひご覧ください。
エンジニアの方向けのエントランスブック「WHITEPLUS Entrance Book for Engineer」もございます。

オンラインでカジュアル面談もできますので、ぜひお気軽にお問い合わせください!

www.wh-plus.co.jp