新卒1年目でまさかのクリーンアーキテクチャ
こんにちは、僕は今年からエンジニアになった新卒1年目です。
入った会社はバリバリコーディングさせてくれるような良い環境に身を投げいることに成功しました。
ですが、自分が今入っているプロジェクトのアーキテクチャがDDDにクリーンアーキテクチャの思想を取り入れて構築されているようで、チームに入った最初の1ヶ月は死に物狂いでした。
それなりに時間も経ちちょっとだけ理解できたので、備忘録として残しておきます。
環境
・macOS BigSur: 11.0.1
・Laravel: 6.18.43
・PHP: 7.3.18
サンプル
ビジネスルール
・ユーザーはタイトル、本文を入力して記事を投稿することができる
機能仕様
・route
はアーキテクチャとは別の概念になるかと思うので今回は記載なし
・ユーザーが記事を投稿できるApi
をLaravel
で実装
・データ登録のため、id
, title
, body
カラムを用意
・各レイヤーにBase...
クラスを用意し、Base
を継承してクラスを実装
→ こうすることによって、共通の処理(そのレイヤーのベースになるような処理)を実装したいとき、または実装した後実装内容を変えた場合親クラスであるBase...
クラスを修正し、小クラスを修正すれば共通の処理は比較的楽に管理することができます。
Controller
アーキテクチャ採用して開発したことある方だと大体聞いたことがあるかと思いますがController
です。
Controller
レイヤーの責務はエンドポイント処理入り口の役割を担い、以下のように動作します。
-
Request
クラスを利用してパラメータを取得 -
Servie
クラスのexecute
メソッドにパラメータを渡してビジネスロジックを実行(Service
クラスのexecute
メソッドはビジネスロジック実行結果を返す) - ビジネスロジックの事項結果を元にしてレスポンスデータを作成するため、
Presenter
クラスのoutput
にビジネスロジック実行結果を渡す。 -
Presenter
クラスのoutput
メソッドにレスポンスのデータフォーマットに加工する。 -
Presenter
クラスのoutput
メソッドの結果をレスポンスで返す。
namespace ProjectName\Controllers;
use Illuminate\Routing\Controller;
class BaseController extends Controller
{
}
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
を用意してビジネスロジックの結果モデルを返す責務があります。
Controller
とService
は1:1
で作成します。
namespace ProjectName\Services;
class BaseService
{
}
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
クラスの抽象クラスで、メソッドに渡す値と返す値は具像クラスに依存しない値にする必要があります。
namespace ProjectName\Repositories;
use ProjectName\Models\Domains\Entities;
interface PostRepositoryInterface
{
public function create(string $title, string $body): Entities\Post;
}
Repository
Interface
の具像クラスで、Service
に対してデータのライフサイクルを制御するための操作やデータを永続化する処理を実装します。
必要な場合はEntityのインスタンス作成もここで行います。
namespace ProjectName\Repositories;
class BaseRepository
{
}
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
クラスを作成し、レスポンスやレンダリングに必要なデータ形式に加工する責務を持ちます。
namespace ProjectName\Presenters;
class BasePresenter
{
}
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
リクエストパラメータをバリデーションを行うレイヤーで、リクエストデータを取得する責務も持ちます。
namespace ProjectName\Requests;
use Illuminate\Foundation\Http\FormRequest;
class BaseRequest extends FormRequest
{
public function authorize(): bool
{
return false;
}
public function rules(): array
{
return [];
}
}
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
毎に作成が必要です。
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;
}
}
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はService
とRepository
のみで扱い、外部にドメインEntityを直接渡すことはNGとされています。
もし外部に値を渡したいときはDTO(Data Transfer Object)
を渡します。
この層の導入目的としては、コードの表現力をあげ不正な値を存在させず、さらに誤った代入を防ぐためです。
namespace ProjectName\Models\Domains\Entities;
abstract class BaseEntity
{
}
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',
];
}
主要レイヤーの各依存先
一番下の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 Object
やJob
,Enum
など多くのクラスを作成し、コードの保守性をあげ機能追加やメンテナンス性の向上などを図ることが可能です。
なので、まだまだ紹介する部分はありますが全部書くとかなり長くなってしまうので一旦ここまでにしておきます。
また、このアーキテクチャはまだ改良する余地があると思いっていて、例えばEntity
は今の使い方だとSOLID原則に違反する可能性があったりします。
アーキテクチャは奥が深く、理解まで時間がかかるものの身につけるとリターンはでかい気がするので是非アーキテクチャに興味を持ってみてください。