クリーンアーキテクチャを使ったコードベースのお掃除
ソフトウェアアーキテクチャについて話しましょう。多くの人がMVCを知っていますよね。ほとんどのウェブフレームワークの基礎です。でも製品が成長するにつれて、MVCの問題点が出てきます。比較的シンプルな製品でも、コードベースが肥大化してごちゃごちゃしてしまいがちです。MVCは始まりですが、それを超えて進化する必要があるとき、どうすればいいでしょうか?
さらに進む前に、なぜ答えを説明するのに苦労するのか見てみましょう。
開発者にとってはよくある会話です(実際には)。
devA: 「うちのコードベースめちゃくちゃだよね、どうにかしようよ。」
devB: 「リファクタリングが必要だね、コードをオブジェクトに移して、関心事を分離しよう。」
devA: 「わかった、じゃあどうやって?」
devB: 「デザインパターンを使ってSOLID原則に従っていこう。」
ここで話は普通途切れます。それで始めるのに十分な答えだと思っているんでしょうね。まるで大工さんにテーブルの作り方を尋ねたところ、彼がただ工具を指して「あれを使うんだ」と言うようなものです。答えは技術的には正しいけれども、全てを物語っているわけではなく、ソフトウェアの書き方(またはテーブル作り)を学んでいる人にとっては全く役に立ちません。工具は重要ですし、何があるかは知っておくべきですが、それだけがプロセスの一部です。
パターンを学んでも十分じゃない
これが最もイライラする部分です。ただパターンを学んだだけでは十分ではありません。実際、パターンを学んだ直後は、パワーツールを持っていてもどこで使うかわからなくて、以前よりも悪い状態になることがあります。
パターンを学ぶことで、使いこなせるようになる前に答えなければならない新しい質問が出てきます。
- デザインパターンはどこで使うべきか?
- どのパターンを使うかをどう決めるか?
- 抽象化の線引きはどこにするか?
- インターフェイスを使うべきときは?
これらの質問に答えるための指針がないと、開発者はオブジェクト間にランダムな境界線を引いて、できるだけ多くのパターンを使うことになります。これは「デザイン」として一貫性がなく、全ての"設計"を使った状態よりも悪化させる結果になります。
なるほど、人々が「デザイン」や「パターン」について不平を言うのも不思議ではありませんね。私たちはどう効果的に使うかをうまく説明していないのですから。
どこから始めるか
私にとって、全てが腑に落ちた瞬間は、クリーンアーキテクチャについて学んだときでした。
クリーンアーキテクチャは、コードを分割する方法に関するガイドであり、どこにどの概念が属するかの方向性を示します。さまざまなアプローチがありますが、同じ核となる概念を共有しています。
- コードをレイヤーで分割する
- 抽象を内側のレイヤーに置く
- 実装の詳細を外側のレイヤーに置く
- 依存関係は内向きにのみ許され、外向きには許されない
こちらに、そのコンセプトについて非常にうまく伝えている素晴らしい記事があります。
https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
かっこいいでしょ?見たことない人には目からウロコです。
関心事を分ける方法や境界線を引く場所についてのガイドラインを提供します。境界線が引かれれば、パターンは明らかになります。ちゃらけたものから実用的なものを分けるための素晴らしい手段です。
さあ、例を見てみましょう。
例
ここに、ユーザーのプロフィール画像を設定するシンプルなユースケースがあります。このクラスはコントローラとコンソールコマンドの両方から内部的に使われているので、コードは非常に簡潔です。
<?php
namespace App\Usecases;
use Ramsey\Uuid\Uuid;
use SplFileInfo;
use Aws\S3\S3Client;
use League\Flysystem\AwsS3v3\AwsS3Adapter;
use League\Flysystem\Filesystem;
use Domain\User;
class SetProfileImage
{
private $filesystem;
public function __construct()
{
$client = S3Client::factory([
'credentials' => [
'key' => getenv('AWS_ACCESS_KEY'),
'secret' => getenv('AWS_SECRET'),
],
'region' => getenv('AWS_REGION'),
'version' => 'latest',
]);
$adapter = new AwsS3Adapter($client, 'bucket-o-images');
$this->filesystem = new Filesystem($adapter);
}
public function handle(Uuid $user_id, SplFileInfo $image): Uuid
{
$image_id = Uuid::uuid4();
$filepath = "profile_image/$image_id.".$image->getExtension();
$image_contents = $image->fread($image->getSize());
$this->filesystem->write($filepath, $image_contents);
$user = User::find($user_id->toString());
$user->setProfileImage($image_id);
User::where('id', $user_id->toString())->update($user->toArray());
return $image_id;
}
}
上記のコードに何が問題かというと、その簡潔さにもかかわらず、実は5つの概念が入り混じっており、混乱の元になっています。
- 我々のアプリケーション
- 設定
- AWS S3
- Flysystem
- Eloquent ORM
そのクラスが何をしようとしているのかを理解するために、これらの概念を全て理解している必要があります。これは、こんなにシンプルなクラスを理解するために頭に入れておく必要のある概念の数に驚かされます。
さらに悪いことに、それらは一貫性がありません。それぞれが独自の言語で自分たちの解決策を説明しており、よく混ざり合わない詳細や概念を使用しています。例えば、ORMの言語である find
、where
、update
はFlysystemの言語には存在しません。アプリケーションの概念を混ぜると、なぜ物事が混乱しやすいのかが見えてきます。
それはちょうど、フランス語、英語、ロシア語がランダムに点在し、時には単語から単語へと切り替わる本を読むようなものです。確かに、最終的には読めるようになるかもしれませんが、途中で多くの間違いを犯し、終わりにはイライラした状態になるでしょう。だから少し整理してみましょう。
レイヤーを分ける
まず、アプリケーションレイヤーから何を取り除くのか知る必要があります。だから、言語をガイドとして使いましょう。
言語をアプリケーション中心にしたいので、以下の概念に固有の言葉を取り除く必要があります。
- 設定
- Flysystem
- AWS S3
- Eloquent ORM
代わりに、実装ではなく、アプリケーションの言語を使用する統合ポイントに置き換えます。
ここでデザインパターンが登場します。あなたがどうやってそれをしようとしているのではなく、あなたが何をしようとしているのかを見ることによって、共通のパターンが現れるのを見つけることができます。
コードを見て、「画像」と「ユーザー」がコア/ドメインの概念だとわかりますが、それらを使用するコードの一部は純粋な実装です。これらが私たちの統合ポイントですので、"画像"と"ユーザー"で本当にやりたいことを詳しく見ていきましょう。
この例では、私たちがやりたいことは明らかに2つです。
- ユーザーの保存と取得
- 画像の保存
それ以外は実装の詳細です。モノの保存と取得は明らかにリポジトリパターンです。それでは、ユーザーリポジトリと画像リポジトリという、2つの新しいアプリケーションコンセプトを作りましょう。そして、それらをインターフェースとして実装します。
アプリケーションレベル
<?php
namespace App\Usecases;
use Ramsey\Uuid\Uuid;
use SplFileInfo;
class SetProfileImage
{
private $image_repo;
private $user_repo;
public function __construct(ImageRepo $image_repo, UserRepository $user_repo)
{
$this->image_repo = $image_repo;
$this->user_repo = $user_repo;
}
public function handle(Uuid $user_id, SplFileInfo $image): Uuid
{
$image_id = Uuid::uuid4();
$this->image_repo->store($image_id, $image);
$user = $this->user_repo->get($user_id);
$user->setProfileImage($image_id);
$this->user_repo->store($user);
return $image_id;
}
}
namespace App\Services;
use Ramsey\Uuid\Uuid;
use SplFileInfo;
interface ImageRepository
{
public function store(Uuid $image_id, SplFileInfo $image);
}
namespace App\Services;
use Domain\User;
use Ramsey\Uuid\Uuid;
interface UserRepository
{
public function get(Uuid $user_id): User;
public function store(User $user);
}
これでアプリケーションレベルが再構築されました。言語と概念をアプリケーションが必要とする最小限に絞り込み、意図を表現しました。また、古い実装に隠されていた2つの新しいコンセプトを作り出し、それらをシンプルなパターン、リポジトリパターンにまで単純化しました。
余談ですが、デザインパターン言語をアプリケーションレイヤーに追加することには問題ありません。なぜなら、それは開発者間の共有言語であり、曖昧さを増すのではなく明晰さを助けるからです。
それでは、実装を見てみましょう。
インフラストラクチャ
<?php
namespace Infrastructure\App\Services;
use App\Services\ImageRepository;
use SplFileInfo;
use League\Flysystem\AwsS3v3\AwsS3Adapter;
use League\Flysystem\Filesystem;
use Aws\S3\S3Client;
use Ramsey\Uuid\Uuid;
class S3ImageRepository implements ImageRepository
{
private $filesystem;
public function __construct()
{
$client = S3Client::factory([
'credentials' => [
'key' => getenv('AWS_ACCESS_KEY'),
'secret' => getenv('AWS_SECRET'),
],
'region' => getenv('AWS_REGION'),
'version' => 'latest',
]);
$adapter = new AwsS3Adapter($client, 'bucket-o-images');
$this->filesystem = new Filesystem($adapter);
}
public function store(Uuid $image_id, SplFileInfo $image)
{
$filepath = "profile_image/$image_id.".$image->getExtension();
$image_contents = $image->fread($image->getSize());
$this->filesystem->write($filepath, $image_contents);
}
}
namespace Infrastructure\App\Services;
use App\Services\UserRepository;
use Domain\User;
use Ramsey\Uuid\Uuid;
class EloquentUserRepository implements UserRepository
{
public function get(Uuid $user_id): User
{
return User::find($user_id->toString());
}
public function store(User $user)
{
User::where('id', $user->getId()->toString())->update($user->toArray());
}
}
これで、ユースケースのコードを2つのレイヤーに分けることができました。
これがなぜ良いのか?
読みやすさ
言語の観点から見て、これはずっとクリアです。それは、アプリケーションが何をしようとしているのかに焦点を当てた言語です。この一貫性のある言語は、理解の障害を低減します。簡潔に言うと、理解するために解析する必要のある概念が少なくなっています。
テスト
テストもずっと簡単になります。オリジナルはそれが多過ぎてテストが難しいです。我々の受入テストは、コードが機能していることを証明するためにS3とDBに接続する必要があります。これ
こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/barryosull/cleaning-up-your-codebase-with-a-clean-architecture