YelpCampをクリーンアーキテクチャで再構築する① - 理論編

今回は Udemy の人気講座で教材として使用されている「YelpCamp」というWeb アプリを「クリーンアーキテクチャ」で再構築 (リファクタリング) してみます。本記事ではクリーンアーキテクチャの基本的な定義と、移植 (PHP) 版の YelpCamp をクリーンアーキテクチャへ移行する際のリファクタリング戦略について解説します。

「クリーンアーキテクチャ(Clean Architecture)」とは、ソフトウェアの構造を整理して、変更に強く保守しやすい設計を実現するためのアーキテクチャパターンです。アメリカのエンジニア ロバート・C・マーチン (Robert C. Martin、通称 Uncle Bob) 氏が提唱しました。クリーンアーキテクチャの基本となる考え方自体は昔からありましたが、名称が広まったのは比較的最近 (2017 年頃) のようです。

クリーンアーキテクチャを使用するメリットとしては

  • 構造が明確で理解しやすい (役割が分離されている)
  • 技術変更に強く、長期保守が容易 (DB・フレームワーク・外部サービスを交換しやすい)
  • テストしやすく信頼性が高い (ビジネスロジックを外部依存から分離できる)
  • チーム開発と並行作業に向く (責務が明確なので並行作業向き)
  • 機能追加・拡張に強い (ドメインロジックが整理されている)

などがあげられます。データベースや周辺技術が目まぐるしく変化する Web アプリとは相性が良い設計手法と思われます。このようにいいことずくめですが、以下のようなデメリットもあります。

  • 抽象化のためコード量が増える
  • ある程度の設計力が求められる
  • 呼び出し階層が深くなりがちで、デバッグが困難な場合がある

それでも、うまく適用すれば 長期的に保守しやすいアプリを構築できる のは間違いありません。

今回の記事を作成するにあたり、以下の講座で勉強させていただきました。レイヤードアーキテクチャのサンプルアプリをクリーンアーキテクチャへリファクタリングするプロセスや、テンプレート作成ツールを使ったスキャフォールディング (ひな型ファイル作成) の方法も解説されています。

Udemy

現場で通用するアプリケーション設計入門 - レイヤードアーキテクチャからクリーンアーキテクチャまで

ベースになるアプリケーションを準備します

ここからは、リファクタリング対象となるベースアプリをセットアップします。

以前の記事でご紹介した静的解析ツール「PHPStan (レベル 10) 」を適用した PHP 版 YelpCamp アプリをベースアプリとして使用します。テスト環境 (Pest、Codeception) も含めてセットアップ済みです。今回は WSL2 上の VSCode で開発していきます。(MAMP 上でも開発可能ですが、インストールされている Pest がバージョン 4.x のため、PHP 8.3 未満のバージョンを使用している場合は Pest を 3.x にダウングレードする必要があります。) 

まずは、WSL2 で任意のフォルダを作成して移動します。その後こちらのファイルをダウンロードして展開します。

cd ~
sudo apt update ; sudo apt install -y unzip

wget -O YelpCampCleanArchBase.zip https://mimura-soft.jp/blog/wp-content/uploads/2025/12/YelpCampCleanArchBase.zip
unzip YelpCampCleanArchBase.zip

展開したフォルダに移動して、必要なパッケージをインストールします。

cd YelpCampCleanArchBase
composer install

MySQL (MariaDB) が入っていない場合は、以下のコマンドでインストールしておきます。

sudo apt install mariadb-server mariadb-client

以下のコマンドを実行してデータベースとユーザーを作成します。

sudo mysql < create-database.sql
sudo mysql < create-user.sql

サムネイル画像の作成と地図を表示するには、「Imagick」というソフトのインストールと、「Mapbox」のユーザー登録が必要です。どちらも無料で使用できますが、Mapbox は 25,000 回/月まで無料という制限があります。Imagick は以下のコマンドでインストールできます。

sudo apt install imagemagick

Mapbox の API キーの取得方法は、こちらのサイト様で詳しく解説されています。Mapbox の API キー(pk.で始まる文字列)は「config.php」ファイルの最後にある define 文に設定します。

// Mapboxのパブリックトークンを設定します (地図を表示する場合は設定してください)
define('MAPBOX_TOKEN', '**ここに API キーを貼り付けます**');

VSCode に「Ctrl + @」と入力してターミナル画面を表示して、以下のように入力して PHP のビルトインサーバーを起動します。

php -S localhost:8080

以上でベースアプリの設定は終了です。ブラウザーを起動して URL 欄に「localhost:8080」と入力するとベースアプリが表示されます。初期状態では何もデータがないので「localhost:8080/seeds」にアクセスするとダミーデータが 50 件作成されます。 ユーザー名「test」、パスワード「test」でログインするとデータの編集などすべての操作が可能です。ダミーデータの作成画面が表示されない場合はリロード操作をお願いします。

現状アーキテクチャの分析とリファクタリング戦略

このセクションでは、まずクリーンアーキテクチャと現在の YelpCamp アプリのアーキテクチャの比較を行います。その上でクリーンアーキテクチャへ移行するための戦略を整理します。

リファクタリングの方針として、「動作しない状態を極力なくし、徐々にクリーンアーキテクチャへ移行できる状態をつくる」ようにします。

・クリーンアーキテクチャの定義

まずはクリーンアーキテクチャの定義を確認します。アイキャッチ画像にもありますが、以下の図がクリーンアーキテクチャの「概念図」になっています。

出展:The Clean Code Blog

クリーンアーキテクチャの主要な定義は以下のようになっています。

  • ビジネスルール (ドメイン) を中心に置き、外部技術から独立させる。
  • 依存方向は内側へ向かい、内側は外側に依存しない。(依存性の逆転)
  • UI・DB・フレームワークの変更がビジネスロジックに影響しない構造を作る。

定義はわかりましたが、実際にどのように実装すればいいのか良く分かりません。そこで、現在のベースアプリの構造を分析して、何をどう逆転すればいいのかを考えてみます。

・ベースアプリの構造

現在のベースアプリのフォルダ構成は以下のようになっています。(主要なフォルダのみ)

<app>
├── controllers/
├── db/
├── models/
├── routes/
└── views/

これは典型的な MVC モデルと呼ばれる構造で、依存関係を簡略的に表すと以下のようになっています。MVC や MVVM と呼ばれるモデルはアーキテクチャというよりも、UI 周りの「デザインパターン」という意味合いが強いです。ビジネスロジックの記述場所が特に規定されているわけではないので、規模が大きくなると Controller や Model (特に Model) 部分がどんどん肥大化していきます。また、ビジネスロジック (主に Model) がデータベース機能に直接依存するケースが多いため、データベースの置き換えは非常に困難です。

作り方にもよりますが MVC モデルは規模が大きくなると、以下のような問題が発生しやすくなります。

  • 画面変更するとロジックが壊れる
  • DB構造が変わると画面まで影響
  • 同じ機能を複数箇所で重複実装
  • 修正の影響範囲が予測できない
  • 各層の結合度が強くなりがちでテストが困難

次にご説明する「レイヤードアーキテクチャ」を使用するとこれらの問題をかなり改善できます。

・レイヤードアーキテクチャ

レイヤードアーキテクチャは上記の問題や以下のような背景から、以前からも存在していましたが 1990 年代頃から急速に普及し始めました。

  • 大規模化でコードがスパゲッティ化したため
  • 関心事の分離を構造として明確に表現する必要があったため
  • 開発チームの分業化が進み、役割を明確化する必要があったため
  • OOP の普及と共に成功パターンが標準化されたため

レイヤードアーキテクチャは現在最も一般的と思われるアーキテクチャで、以下の言語やフレームワークなどで採用または参考にされています。(リストは一例です。)

  • Spring Boot
  • Java EE
  • Laravel
  • Django
  • Ruby on Rails
  • ASP.NET Core (.NET)

レイヤードアーキテクチャは大まかに以下のような構成になっています。各層の責務が明確で、上位の層は下位の層に依存可能ですが、下位の層から上位の層を参照 (呼び出し) することは禁止されています。(呼び出しは不可能ではありませんが、アプリケーションの見通しが悪くなります。)

多少無理やりですが、MVC モデルと比較すると以下のようになります。

・クリーンアーキテクチャ

さていよいよクリーンアーキテクチャ迄たどり着きました。「クリーンアーキテクチャ」は「レイヤードアーキテクチャ」などの考え方をさらに昇華させたアーキテクチャになっています。(同様の系譜に「ヘキサゴナルアーキテクチャ」や「オニオンアーキテクチャ」などがあります。) 両者を比較して大きく異なっている箇所は「Infrastructure 層」の依存の向きと層の位置と思われます。まずは Infrastructure 層の「依存の向きを逆転」します。

さらに「Infrastructure 層」を「Presentation 層」の外側に移動します。(必ずそうする訳ではないようです。プロジェクトのケースによって異なります。)

以上でクリーンアーキテクチャへの変換は終了です。以下のにすると概念図に近くなります。

・どのように実装するか

クリーンアーキテクチャの実装ポリシーは極めて単純です。

  • 内側が外側に依存してはいけない。
    (内側のモジュールが外側のメソッドを呼び出してはいけない。)

たったこれだけです。非常に単純ですが、データベース操作 (Infrastructure) を外側に排除したために、以下のようなコーディングはルール違反になります。(コーディング例はベースアプリのユーザー登録関数になっています。)

<?php

namespace controller\users;

use Throwable;
use utils\Flush;
use db\UserQuery;
use utils\ReturnTo;
use models\UserSchema;

function register(): void
{
    try {
        $new_user = UserSchema::getModel();
        // Infrastructure層の関数を使ってDBからユーザーを取得している
        $user = UserQuery::fetchByUserName($new_user->username);

        if (!empty($user)) {
            Flush::push(Flush::ERROR, "そのユーザー名はすでに使われています");
            redirect(GO_REFERER);
            return;
        };
        // Infrastructure層の関数を使ってDBにユーザーを挿入している
        UserQuery::insert($new_user);

        login('Yelp Campへようこそ!');
    } catch (Throwable $e) {
        Flush::push(Flush::ERROR, "エラーが発生しました");
        redirect('/register');
    }
}

この問題を解決するために以下のような構造を導入します。(この構造はドメイン層と Infrastructure 層の「依存の逆転」方法を示したものになりますが、ほかの階層間でも仕組みは同じです。) まずドメイン層内にデータ操作のためのインターフェイスを定義して、後でご説明する Application 層のユースケースでこのインターフェイスを使用してビジネスロジック (Entity) の保存や取得を行います。実際のデータ操作は Infrastructure 層に実装されている機能に任せます。また、データ操作を行う機能のデータ構造に影響されないように、階層間は専用のデータオブジェクト (DTO) を使用してデータの受け渡しを行います。

この様な構造にすることで、内部 (ビジネスロジック) は外部 (データベース、フレームワークなど)の影響を受けなくなります。また、実際にデータベースを操作する機能はアプリの起動時などに、ビジネスロジックを受け持つクラスにコンストラクターなどを通じて「依存を注入」するので、この部分を「モック (疑似オブジェクト)」に置き換えやすくなり、単体テストなども容易になります。

・リファクタリング戦略

以上を踏まえ、以下の順序でリファクタリングを行っていきます。

  1. ドメイン層に Entities と Repository Interface を配置
  2. Controller に散らばったビジネスロジックを UseCase に切り出す
  3. Presentation (Controller / View) は UseCase を呼ぶだけにする
  4. DB アクセスを Infrastructure に移動し、Interface 実装として登録

クリーンアーキテクチャでは責務の分離を徹底するために Application 層に「ユースケース」と呼ばれる単一責務のクラスを用意することが推奨されています。そのため機能毎にファイルを分割して個別に開発してくことが可能です。そこでまずは、先ほどのサンプルコードを上記の手順 (1 ~ 4) に沿ってリファクタリングを進めていきます。

リファクタリング成果物は新たに作成する以下のフォルダ内に格納します。最終的には最初にあった MVC 関連のフォルダは削除します。

<app>
└── src/
    ├── adapter/
    ├── application/
    ├── domain/
    └── infrastructure/

今回は以上です。
次回からは当初の方針どおり、破壊的な変更を避けつつ段階的にリファクタリングを進めていきます。