そこそこの規模なWebシステムを設計するための知識セット
何年も使われるWebシステムを使うなら、保守性は大事です。代表的な設計手法は押さえておく必要があります。 オブジェクト指向設計、ドメイン駆動設計、マイクロサービスなどの設計手法を確認しながら、実際に設計する際のポイントをまとめます。
いくつかの設計手法
有名な設計手法についてまとめておきたいと思います。 どういった設計手法があるか知りそれに沿うことで、 未来の自分だけではなく他の開発者にも比較的簡単にアプリケーションの構造を理解してもらうことができます。
オブジェクト指向設計(OOP)
現実世界のある対象をオブジェクトという単位に切り出し、クラスで表現します。 オブジェクトを表現するクラスは、対象の状態であるプロパティと、その操作であるメソッドを持ちます。 オブジェクトごとにふるまいを定義することで、旧来の手続き的プログラミングよりも保守性の高いシステムを作ることができます。
インスタンス化するとメモリのスタック領域に参照が保存され、ヒープ領域に実体が保存されます。
カプセル化
クラスのプロパティやメソッドをそのクラス内でのみ、または継承したクラスのみで使えるようにすることで堅牢性を高めます。
1<?php
2//カプセル治療薬クラス
3class CapsuleMedicine {
4 //日付プロパティ
5 private string $created;
6
7 function __construct (string $datetime = null) {
8 $dateTime = new Datetime($datetime);
9 $this->created = $dateTime->format('Y-m-d H:i:s');
10 }
11 //日付を取得するメソッド
12 public function getCreated(): string {
13 return $this->created;
14 }
15}
16$capsule = new CapsuleMedicine('2022-05-20 00:00:00');
17echo $capsule->getCreated(); // 2022-05-20 00:00:00
18echo $capsule->created; // Fatal error: Uncaught Error: Call to undefined method Capsule::created() in /var/www/html/encapsulation.php:16
上記の場合、private
アクセス修飾子が付けられた$created
プロパティはクラス外部から直接呼び出すことはできません。
public
指定されたgetCreated()
メソッドによって呼び出しています。
継承
共通のメソッドを抽出し親クラスにして継承する形にします。 もしくは、特定の用途向けにカスタマイズするために、子クラスへ継承させます。
また、メソッド定義のみ抽象クラスやインターフェースに書くことで、 実装は継承先のクラスで行うことにより設計と実装を分担することができます。
1//Medicine抽象クラス(インスタンス化できない)
2abstract class Medicine {
3 protected string $created;
4 function __construct (string $datetime = null) {
5 $dateTime = new Datetime($datetime);
6 $this->created = $dateTime->format('Y-m-d H:i:s');
7 }
8 public function getCreated(): string {
9 return $this->created;
10 }
11 //抽象メソッドは継承したクラスで実装する
12 abstract protected function take(): void;
13}
14
15//Medicine抽象クラスを継承したCapsuleMedicineクラス
16class CapsuleMedicine extends Medicine {
17 public function take(): void {
18 $now = new Datetime();
19 echo 'Took a capsule medicine at ' . $now->format('Y-m-d H:i:s');
20 }
21}
22
23//Medicine抽象クラスを継承したLiquidMedicineクラス
24class LiquidMedicine extends Medicine {
25 public function take(): void {
26 $now = new Datetime();
27 echo 'Took a liquid medicine at ' . $now->format('Y-m-d H:i:s');
28 }
29}
30
31$capsule = new CapsuleMedicine();
32$liquid = new LiquidMedicine();
33echo $capsule->getCreated(); // 2022-05-19 01:06:43
34echo $capsule->take(); // Took a capsule medicine at 2022-05-19 01:06:43
35echo $liquid->getCreated(); // 2022-05-19 01:06:43
36echo $liquid->take(); // Took a liquid medicine at 2022-05-19 01:06:43
上記の場合、CapsuleMedicineクラスとLiquidMedicineクラスはMedicine抽象クラスを継承しているので、
getCreated
メソッドが使えます。また、Medicine抽象クラスの抽象メソッドtake
は定義のみで、実装はそれぞれの継承先のクラスで行います。
多態性(ポリモーフィズム)
クラスのコンストラクタで、引数として指定したクラスだけではなく、そのクラスを継承した子クラスも受け取ることができます。 また、コンストラクタにインターフェースを指定し、実際はその実装クラスを引数に渡すこともできます。
1//投薬治療クラス
2class Medication {
3 function __construct (Medicine $medicine) {
4 $this->medicine = new $medicine;
5 }
6 public function treat(): void {
7 $created = $this->medicine->getCreated();
8 echo "This medicine was created at {$created}.";
9 $this->medicine->take();
10 }
11}
12$medication = new Medication(new LiquidMedicine());
13$medication->treat();
14// This medicine was created at 2022-05-19 01:17:00.Took a liquid medicine at 2022-05-19 01:17:00
上記の場合、MedicationクラスのコンストラクタではMedicine抽象クラスを引数に指定していますが、 実際はMedicineクラスを継承したLiquidMedicineクラスを引数に用いています。
データベースのモデルなどを引数に取る場合、引数に取るべきクラスを継承していれば、 その実体が関係データベースであってもNoSQLであっても引数として取ることができます。実体が変わってもそのクラスが動くことを保証できます。
ドメイン駆動設計(DDD)
ドメインオブジェクト
現実世界の要素を抽出したものになります。不変な性質をもつ「値オブジェクト」や可変な性質を持つ「エンティティ」として表現します。 ソフトウェアで現実を表現する際、現実世界のドメインを説明書を書くかのようにそれ自身の状態とふるまいを定義します。
サービス
ドメインに関係があるがドメインオブジェクトに持たせるべきでない処理を書くためのドメインサービスと、 アプリケーションのユースケースを実現するためのアプリケーションサービスがあります。
DDDではアプリケーションサービスにドメインのルールを流出させてはいけません。 また、ドメインのふるまいはドメインオブジェクトそのものに定義し、それが難しい場合にドメインサービスに定義します。 (無口なドメインオブジェクト、ドメインモデル貧血症を避ける)
レポジトリ
データ保存や操作の特別な処理はレポジトリに分離し、ドメインモデルを整頓します。
集約(Aggregate)
ドメインオブジェクトの操作の責任範囲を集約の境界と呼び、外からの操作依頼を受け付ける役割を果たすものを集約のルートと呼びます。 集約の外側からその集約のデータを直接変更することはできません。集約のルートを介して変更します。
仕様(Specification)
ドメインオブジェクトが条件に沿っているかどうかを評価する仕組みです。
ユビキタス言語
メソッド名は共通認識を持てる名前にし、読み手によって解釈が変わらないようにすべきという考えです。
境界づけられたコンテキスト
ドメインの境界線で、後述するマイクロサービスの境界線にもなります。
ドメイン駆動設計入門 エリック・エヴァンスのドメイン駆動設計
マイクロサービス
マイクロサービスを一言で表すと、「独立してデプロイできるように分割した(独立デプロイ可能性)サービス群」のことです。
あるドメインに沿って分割することで、意図せぬ依存関係が生まれてしまう可能性を排除することができます。 また、マイクロサービスごとに別の言語やフレームワークなどを採用することができるので、複数チームでプロダクトを開発する場合に真価を発揮します。 ソフトウェアのバージョンアップについても、それぞれが独立しているために段階的な移行を進めることができます。
モノリス
ドメインごとにサービスが分割されず、一枚岩になっているシステムのことです。 メリットは(依存を気にしなければ)楽にコードを書ける、単にインポートすれば必要なクラスを使えることなどが挙げられます。 デメリットは依存関係が増し時間が経つごとにメンテナンスの難易度が跳ね上がることです(モジュラーモノリスで軽減することができます)。
マイクロサービス
メリットは前述の通り複数チームでの開発しやすい、段階的なリリース・移行が可能、障害が発生しても影響が部分的に抑えられる、などがあります。 デメリットはドメインの分割やデータの分散などにより一見扱いづらい、サービス間の通信が発生し処理速度に少し影響がある、構築がモノリスに比べて手間がかかるなどがあります。
モジュラーモノリス
モノリスとマイクロサービスの中間のような存在で、デプロイ単位は一つですが独立性の高いモジュールに分割されているモノリスです。 言語が一つだったり、チームが一つだったり、新規開発フェーズなどでマイクロサービスにする必要性が感じられないなら、モジュラーモノリスを検討するのも良いと思われます。
いくつかのアーキテクチャ
設計手法はどのように設計するかの方針を与えてくれますが、アーキテクチャはどのように実装するかの方針を示してくれます。
コードを役割ごとにどのように整頓するかを示しています。
レイヤードアーキテクチャ
もっとも伝統的なアーキテクチャで、MVCモデルもこれに沿っていると言えます。
プレゼンテーション層(ユーザーインターフェース)
ユーザーインターフェースとアプリケーションの結びつけを行います。MVCではVに相当します。
アプリケーション層
ドメインの取りまとめを行います。MVCではCに相当します。
ドメイン層
ソフトウェアを構築する上で必要な概念を表現します。MVCではMに相当します。
インフラストラクチャ層
ドメインやアプリケーションが実現するための技術基盤(データベース)へのアクセスを担当します。MVCではMに相当します。
クリーンアーキテクチャ
フレームワーク、UI、データベース、その他のクライアントのそれぞれの要素を独立させ、依存関係にルールを持たせる考え方です。 考案者の図では円状の層に分けられ、外部の円は内部の円の方向にのみ依存します。ビジネスルールを表現するエンティティを中心に据え、 その外部にユースケース、インターフェースアダプター、フレームワークとドライバの順に配置します。
Clean Architecture 達人に学ぶソフトウェアの構造と設計
実際の設計で考慮すべきこと
開発者とビジネスサイドの間の工数の認識に違いがあったり、開発者間のスキルセットの違いによる問題は発生しがちです。 全体として最適な選択は何かを考慮する必要があります。
オーバーエンジニアリングでないか?
実現したいシステムに対して過剰な仕組みを構築してしまうことは避けたいです。 設計手法やアーキテクチャにとらわれてしまって不必要に複雑になったり工数が肥大化する恐れがあるので、 適切なレベルで取り入れるようにしたいです。
よい設計者になるためには、技術のみに傾倒するのではなくビジネスサイドの状況も考慮して最適な判断ができる必要があると思います。
開発チームのスキルセットを考慮する
技術者としてはトレンドの技術を取り入れたい気持ちは出てきますが、それが必ずしも最適な判断ではないかもしれません。 企業のシステムの場合はそういった観点で技術や設計を採用することも必要になってきいます。
現状の開発メンバーはそれをすんなり受け入れてくれるか?教育にどれくらいコストがかかるだろうか? のちにアサインするエンジニアはいるだろうか?採用しやすいだろうか?
現状を総合的に判断してベストな選択をしたいですね。