3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

DDDとクリーンアーキテクチャをちょっとだけ理解した

Last updated at Posted at 2020-11-30

新卒1年目でまさかのクリーンアーキテクチャ

こんにちは、僕は今年からエンジニアになった新卒1年目です。
入った会社はバリバリコーディングさせてくれるような良い環境に身を投げいることに成功しました。

ですが、自分が今入っているプロジェクトのアーキテクチャがDDDにクリーンアーキテクチャの思想を取り入れて構築されているようで、チームに入った最初の1ヶ月は死に物狂いでした。
それなりに時間も経ちちょっとだけ理解できたので、備忘録として残しておきます。

環境

macOS BigSur: 11.0.1
Laravel: 6.18.43
PHP: 7.3.18

サンプル

ビジネスルール

・ユーザーはタイトル、本文を入力して記事を投稿することができる

機能仕様

routeはアーキテクチャとは別の概念になるかと思うので今回は記載なし
・ユーザーが記事を投稿できるApiLaravelで実装
・データ登録のため、id, title, bodyカラムを用意
・各レイヤーにBase...クラスを用意し、Baseを継承してクラスを実装
→ こうすることによって、共通の処理(そのレイヤーのベースになるような処理)を実装したいとき、または実装した後実装内容を変えた場合親クラスであるBase...クラスを修正し、小クラスを修正すれば共通の処理は比較的楽に管理することができます。

Controller

アーキテクチャ採用して開発したことある方だと大体聞いたことがあるかと思いますがControllerです。

Controllerレイヤーの責務はエンドポイント処理入り口の役割を担い、以下のように動作します。

  1. Requestクラスを利用してパラメータを取得
  2. Servieクラスのexecuteメソッドにパラメータを渡してビジネスロジックを実行(Serviceクラスのexecuteメソッドはビジネスロジック実行結果を返す)
  3. ビジネスロジックの事項結果を元にしてレスポンスデータを作成するため、Presenterクラスのoutputにビジネスロジック実行結果を渡す。
  4. Presenterクラスのoutputメソッドにレスポンスのデータフォーマットに加工する。
  5. Presenterクラスのoutputメソッドの結果をレスポンスで返す。
BaseController.php
namespace ProjectName\Controllers;

use Illuminate\Routing\Controller;

class BaseController extends Controller
{
}

PostController.php
namespace ProjectName\Controllers\Post;

use Illuminate\Http\JsonResponse;
use ProjectName\Requests\Post\StoreRequest;
use ProjectName\Presenters\Post\StorePresenter;
use ProjectName\Services\Post\StoreService;
use ProjectName\Controllers\Post\BaseController;

class StoreController extends BaseController
{
    public function store(
        StoreRequest $request, StorePresenter $presenter, StoreService $service
    ): JsonResponse {
        // Requestクラスを使用してパラメータ取得
        $title = (string) $request->input('title');
        $body = (string) $request->input('body');
        // ビジネスロジック実行
        $result = $service->execute($title, $body);
        // Presenterクラスのoutputメソッドの結果をレスポンス
        return response()->json($presenter->output($result));
    }
}

Service

Service層ではビジネスロジックを実装し、エンドポイント毎にServiceを用意してビジネスロジックの結果モデルを返す責務があります。

ControllerService1:1で作成します。

BaseService.php
namespace ProjectName\Services;

class BaseService
{
}
StoreService.php
namespace ProjectName\Services\Post;

use ProjectName\Services\BaseService;
use ProjectName\Repositories\PostRepositoryInterface;
use ProjectName\Results\Post\StoreResult;

class StoreService extends BaseService
{
    protected PostRepositoryInterface $postRepository;
    // DI
    public function __construct(PostRepositoryInterface $postRepository)
    {
        $this->postRepositoryInterface = $postRepository;
    }

    public function execute(string $title, string $body): StoreResult
    {
        $post = $this->postRepository->create($title, $body);
        // Resultクラスに結果を格納して返す
        return StoreResult::makeSuccess()
            ->setPost($post);
    }
}

InterfaceとRepository

Interafce

Repositoryクラスの抽象クラスで、メソッドに渡す値と返す値は具像クラスに依存しない値にする必要があります。

PostRepositoryInterface.php
namespace ProjectName\Repositories;

use ProjectName\Models\Domains\Entities;

interface PostRepositoryInterface
{
    public function create(string $title, string $body): Entities\Post; 
}

Repository

Interfaceの具像クラスで、Serviceに対してデータのライフサイクルを制御するための操作やデータを永続化する処理を実装します。

必要な場合はEntityのインスタンス作成もここで行います。

BaseRepository.php
namespace ProjectName\Repositories;

class BaseRepository
{
}
PostRepository.php
namespace ProjectName\Repositories\Database;

use ProjectName\Database\Eloquent\Post;
use ProjectName\Repositories\PostRepositoryInterface;
use ProjectName\Models\Domains\Entities;

class PostRepository implements PostRepositoryInterface
{
    protected Post $postEntity;

    public function __construct(Post $postEntity)
    {
        $this->postEntity = $postEntity;
    }
    
    public function create(string $title, string $body): Entities/Post
    {
        $attribute = ['title' => $title, 'body' => $body];

        return $this->postEntity->create($attribute);
    }
}

Presenter

必要に応じてエンドポイント毎にPresenterクラスを作成し、レスポンスやレンダリングに必要なデータ形式に加工する責務を持ちます。

BasePresenter.php
namespace ProjectName\Presenters;

class BasePresenter
{
}

StorePresenter.php
namespace ProjectName\Presenters\Post;

use ProjectName\Results\Post\StoreResult;

class StorePresenter extends BasePresenter
{
    public function output(StoreResult $result): array
    {
        $post = $result->getPost();

        return [
            'title' => $post->getAttribute('title'),
            'body' => $post->getAttribute('body'),
        ];
    }
}

Request

リクエストパラメータをバリデーションを行うレイヤーで、リクエストデータを取得する責務も持ちます。

BaseRequest.php
namespace ProjectName\Requests;

use Illuminate\Foundation\Http\FormRequest;

class BaseRequest extends FormRequest
{
    public function authorize(): bool
    {
        return false;
    }

    public function rules(): array
    {
        return [];
    }
}

StoreRequest.php
namespace ProjectName\Requests\Post;

use ProjectName\Requests\BaseRequest;

class StoreRequest extends BaseRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'title' => [
                // note: ルール内はスペース入れてしまうと動かなくなるので×
                'required_if:status,published',
                'max:50',
            ],
            'body' => [
                'required_if:status,published',
                'max:1000',
            ],
        ];
    }
}

Result

ビジネスロジックの結果を格納するモデルで、Service毎に作成が必要です。

BaseResult.php
namespace ProjectName\Results;

class BaseResult
{
    private const RESULT_STATUS_SUCCESS = 'success';
    private const RESULT_STATUS_ERROR = 'error';

    private string $resultStatus;

    private function __construct(string $resultStatus)
    {
        $this->resultStatus = $resultStatus;
    }

    final public static function makeSuccess(): self
    {
        return new static(self::RESULT_STATUS_SUCCESS);
    }

    final public static function makeError(): self
    {
        return new static(self::RESULT_STATUS_ERROR);
    }

    final public function isSuccess(): bool
    {
        return $this->resultStatus === self::RESULT_STATUS_SUCCESS;
    }

    final public function isError(): bool
    {
        return $this->resultStatus === self::RESULT_STATUS_ERROR;
    }
}

StoreResult.php
namespace ProjectName\Results\Post;

use ProjectName\Models\Domains\Entities;
use ProjectName\Results\BaseResult;

class StoreResult extends BaseResult
{
    protected Post $post;

    public function getPost(): Post
    {
        return $this->post;
    }

    public function setPost(Entities\Post $post): StoreResult
    {
        $this->post = $post;

        return $this;
    }
}

Domain Entity

-- 2020/12/21/更新 --

なぜドメインEntityを採用するのかの記事を更新しました。
こちらからどうぞ

-- 更新終わり --

ライフサイクルと連続性を持つデータを扱います。
ドメインEntityはServiceRepositoryのみで扱い、外部にドメインEntityを直接渡すことはNGとされています。
もし外部に値を渡したいときはDTO(Data Transfer Object)を渡します。

この層の導入目的としては、コードの表現力をあげ不正な値を存在させず、さらに誤った代入を防ぐためです。

BaseEntity.php
namespace ProjectName\Models\Domains\Entities;

abstract class BaseEntity
{
}
Post.php
namespace ProjectName\Models\Domains\Entities;

class Post extends BaseEntity
{
    protected array $primaryKey = ['id'];

    protected array $columns = [
        'id',
        'title',
        'body',
    ];

    protected array $nullable = [];

    protected array $dates = [
        'created_at',
    ];

    protected array $fillable = [
        'title',
        'body',
    ];
}

主要レイヤーの各依存先

スクリーンショット 2020-11-30 15.54.53.png

一番下のDataAccessは実際にデータを取得する役割を持ちます。例えばApiを叩いたり、実際にDBにデータを取得しに行ったりといった具合です。

-- 2020/12/21更新 --

APIを叩く場合のDataAccess層

データベースから直接値を持ってくる,APIを叩いてデータを取得する場合はDataAccess層を用います。

<?php

namespace ProjectName\Infrastructure\DataAccess\Http;

use GuzzleHttp\Client;
use Illuminate\Contracts\Config\Repository as Config;
use Psr\Http\Message\ResponseInterface;

abstract class BaseClient
{
    private Client $client;

    protected Config $config;

    protected array $defaultOptions;

    abstract protected function getBaseUri(): string;

    public function __construct(Client $client, Config $config)
    {
        $this->client = $client;
        $this->config = $config;
    }

    public function requestWithCommonKey($method, $uri, array $options = []): ResponseInterface
    {
        $options['headers'] = array_merge(($this->default_options['headers'] ?? []), [
            $this->config->get('projectName-api-client.projectName_key_name') => $this->config->get('projectName-api-client.projectName_key_value'),
        ]);
        $options = $this->getOptions($options);

        return $this->client->request($method, $uri, $options);
    }

    private function getOptions(array $options): array
    {
        return array_merge($this->defaultOptions, [
            'base_uri' => $this->getBaseUri(),
        ], $options);
    }
}
<?php

namespace ProjectName\Infrastructure\DataAccess\Http;

use GuzzleHttp\Client;
use Illuminate\Contracts\Config\Repository as Config;

class Post extends BaseClient
{
    private array $options;

    public function __construct(Client $client, Config $config)
    {
        parent::__construct($client, $config);

        $this->options = $this->config->get('ProjectName.ProjectName.options');
    }

    protected function getBaseUri(): string
    {
        return $this->config->get('ProjectName-api-client.ProjectName.base_uri');
    }

    public function getArticles(string $name, string $email, string $password): array
    {
        $uri = $this->getBaseUri() . '/v1/post';
        $response = $this->requestWithCommonKey('GET', $uri, $this->options);
        $jsonResponse = $response->getBody()->getContents();

        return json_decode($jsonResponse, true);
    }
}

この様な感じでAPIを叩いてデータをjson形式で受け取ってarrayで返します。
ここから先はInterfaceを介して具象クラスに上記メソッドを呼ぶ処理を実装します。

<?php

namespace ProjectName\Infrastructure\Repositories;

use ProjectName\Models\Domains\Entities;

interface PostRepositoryInterface
{
    public function getArticles(): array;
}
namespace ProjectName\Infrastructure\Repositories\Database;

class PostRepository implements PostRepositoryInterface
{
    /** @var Post */
    protected $post;

    public function __construct(Post $post)
    {
        $this->post = $post;
    }

    public function getArticles()
    {
        return $this->post->getArticles();
    }
}

-- 更新終わり --

これで終わりじゃない

ここまででも多くのレイヤーが登場しましたが、このほかにもValue ObjectJob,Enumなど多くのクラスを作成し、コードの保守性をあげ機能追加やメンテナンス性の向上などを図ることが可能です。
なので、まだまだ紹介する部分はありますが全部書くとかなり長くなってしまうので一旦ここまでにしておきます。

また、このアーキテクチャはまだ改良する余地があると思いっていて、例えばEntityは今の使い方だとSOLID原則に違反する可能性があったりします。

アーキテクチャは奥が深く、理解まで時間がかかるものの身につけるとリターンはでかい気がするので是非アーキテクチャに興味を持ってみてください。

3
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?