【PHP】安定度・抽象度等価の原則について SAP
はじめに
安定度・抽象度等価の原則は、パッケージの抽象度は、その安定度と同程度でなければいけない[1]ということを定めています。要するに、パッケージの安定度が高い場合には、そのパッケージの抽象度も高くする必要があり、パッケージの安定度が低い場合には、そのパッケージの抽象度は低くても問題ないという事を言っています。
この原則について、商品価格を計算する処理を例に挙げて順を追って解説していきます。
ここで使うサンプルコードと依存関係の図を下記に示します。
<?php
namespace Calculate;
class PriceCalculator
{
public function __construct(
private IDiscount $discount,
private ITax $tax
) {}
public function calculate(int $basePrice): int
{
// 割引した後の価格を計算
$discountedPrice = $this->discount->calculate($basePrice);
// 割引後の価格に税金を適用
$tax = $this->tax->calculate($discountedPrice);
return $discountedPrice + $tax;
}
}
interface IDiscount
{
/**
* 割引額を計算する
*/
public function calculate(int $basePrice): int;
}
interface ITax
{
/**
* 税金を計算する
*/
public function calculate(int $basePrice): int;
}
<?php
namespace Discount;
use Calculate\IDiscount;
class FiveHundredYenDiscount implements IDiscount
{
private const DISCOUNT_AMOUNT = 500;
public function calculate(int $basePrice): int
{
return max(0, $basePrice - self::DISCOUNT_AMOUNT);
}
}
<?php
namespace Tax;
use Calculate\ITax;
class TenPercentTax implements ITax
{
private const TAX_RATE = 0.1;
public function calculate(int $basePrice): int
{
return (int)floor($basePrice * self::TAX_RATE);
}
}
<?php
namespace Client;
use Calculate\PriceCalculator;
/**
* 無理やり作った感はありますが、PriceCalculatorクラスのクライアント側です
*/
class PricePrinter
{
public function __construct(
private PriceCalculator $priceCalculator
) {}
public function printPrice(int $basePrice): void
{
$totalPrice = $this->priceCalculator->calculate($basePrice);
echo "合計金額: " . $totalPrice . "円\n";
}
}
図1: サンプルコードのパッケージ間の依存関係
安定度について
図1より、Tax・Discount・Clientパッケージはどこからも依存されていないので、他のパッケージの都合を考えずに自由に変更することができます。そのため、これらのパッケージは変更しやすく安定度は低いです。
逆に、Calculateパッケージの中の処理を変更すると、Calculateパッケージに依存しているTax・Discount・Clientパッケージに変更の影響が出てしまいます。Calculateパッケージを変更したときの影響範囲が広いため、このパッケージはTax・Discount・Clientパッケージに比べて変更しづらく安定度が高いです。
抽象度について
この原則が述べている抽象度の高さについて、「ちょうぜつソフトウェア設計入門」という書籍の解説が分かりやすかったので下記に引用します。
- 抽象クラスやインターフェイスなど実装詳細を自身から排除したもの
- 上記のような詳細を持たないものだけに依存するロジック
- 固有の業務にも特定技術にも関係しない時刻や配列などの汎用概念とその操作
- プログラミング言語そのものや言語標準ライブラリと同等レベルの業界水準
引用: 田中ひさてる, PHPで理解するオブジェクト指向の活用 ちょうぜつソフトウェア設計入門, 2022年12月22日, 技術評論社, P33
上記の中の「抽象クラスやインターフェイスなど実装詳細を自身から排除したもの」についてはそのままの意味で、サンプルコード中のIDiscountやITaxインターフェイスが例として挙げられます。
「詳細を持たないものだけに依存するロジック」については、サンプルコード中のPriceCalculatorクラスが良い例だと思います。PriceCalculatorクラスは、どのように割引や税込み価格を計算するのかの詳細部分が含まれていません。そのため、割引や税率を変更しても一切変更の影響がありません。このクラスには普遍的な商品価格の計算部分しか含まれていないので、抽象度が高いと言えそうです。
「固有の業務にも特定技術にも関係しない時刻や配列などの汎用概念とその操作」については、サンプルコード中の下記のような処理が当てはまります。
// 最大値を返します。
max(0, $basePrice - self::DISCOUNT_AMOUNT);
// 端数を切り捨てます。
floor($basePrice * self::TAX_RATE)
上記のような処理は十分汎用的であり、自分達のシステムの仕様が変わっても変更されることはありません。そのため、様々なパッケージの中から安心して使うことができます。
「プログラミング言語そのものや言語標準ライブラリと同等レベルの業界水準」については、PHPだとPSRが挙げられます。PSRはPHPの標準であり変更されることはありません。そのため、自分達のシステムから安心してPSRのライブラリを使用することができます。
/**
* PSR-3のLoggerInterfaceという抽象に依存した処理
* LoggerInterfaceは変更されることがないため安心して依存できる
*/
class UserRegistration
{
public function __construct(private LoggerInterface $logger) {}
public function register(string $username): void
{
// ... ユーザー登録処理 ...
// ログを出力
$this->logger->info("New user registered: {$username}");
}
}
また、今までのコード例は全てPHPで書かれています。PHPで書いている以上、PHPよりも高い抽象度を得ることは不可能です。
「ちょうぜつソフトウェア設計入門」によると、抽象度の高さはこれらの4つにどれだけ近いかで決まると述べられていました。
安定度・抽象度等価の原則とは
「安定度が高く抽象度も高い場合」と「安定度が低く抽象度も低い場合」について、先ほどのサンプルコードを例に出して解説しつつ、安定度・抽象度等価の原則の意味についてまとめます。
安定度が高く抽象度も高い場合
サンプルコード中のCalculateパッケージは安定度が非常に高いです。また、安定度だけでなく抽象度も非常に高いです。なぜなら、どのように割引や税率の計算を行うのかの詳細に依存していなく、商品価格を計算する普遍的なロジックしか定義されていないからです。そのため、仮に割引価格や税率を変更したい場合でも、Calculateパッケージには一切手を加える必要はないため、変更作業がとても楽です。
ここで、Calculateパッケージの安定度の高さはそのままに抽象度が低いとどうなるでしょうか?
例えば、Calculateパッケージがどのように割引を行うのかの詳細部分に依存してしまっているとします。この時、仮に割引額を500円引きから3%引きに変更したい場合に、Calculateパッケージを変更する必要があります。Calculateパッケージを変更すると、このパッケージに依存している全てのパッケージにも変更の影響が出てしまうので、変更した時の影響範囲が広く仕様変更が辛くなってしまいます。
このように安定度が高いパッケージの抽象度が低いと、安定度の高さが仕様変更の妨げになってしまいます。
安定度が低く抽象度も低い場合
サンプルコード中のDiscount・Tax・Clientパッケージは安定度が非常に低いです。また、どのように割引や税込み価格の計算を行うのかの詳細が定義されているので抽象度も非常に低いです。そのため、仮に割引価格や税率を変更したい場合に、DiscountやTaxパッケージを修正する必要がありますが、それでも全く問題ありません。なぜなら、DiscountやTaxパッケージは安定度が低く、これらのパッケージは他のパッケージの都合を考えずに自由に変更することができるからです。
このことから、パッケージの安定度が低い場合には抽象度が低くても問題ありません。また、パッケージの安定度が低い場合にはそもそも変更しやすいため、いくら抽象度を高くして拡張しやすくしても宝の持ち腐れになってしまいます。
安定度・抽象度等価の原則の意味
これらの事から、パッケージの安定度が高い場合には、安定度の高さが仕様変更の妨げにならないように抽象度も高くして拡張しやすくする必要があります。逆にパッケージの安定度が低い場合には、そもそも変更しやすいため、抽象度は低くても問題ありません。この事を定めているのが安定度・抽象度等価の原則です。
安定度・抽象度等価の原則の可視化方法
こちらの記事でまとめた「I(不安定さ)」という指標と、「A(抽象度)」という指標を使うことにより、安定度・抽象度等価の原則にシステムが従っているかどうかを可視化することができます。
まず、「A(抽象度)」という指標の算出方法について解説します。
抽象度の算出方法
A(抽象度)の算出方法を下記に引用します。
Nc: コンポーネント内のクラスの総数
Na: コンポーネント内の抽象クラスとインターフェイスの総数
A: 抽象度。A = Na ÷ Nc引用: Robert C. Martin他, Clean Architecture, 2018年7月27日, 株式会社ドワンゴ, P139
サンプルコードの抽象度を算出したものを下記に示します。
図2: パッケージ間の抽象度
不安定さと抽象度のグラフ
「I(不安定さ)」を縦軸に、「A(抽象度)」を横軸にしたグラフを作成することで、安定度・抽象度等価の原則にシステムが従っているかどうかを可視化できます。
下記にグラフを示します。
図3: IとAの関連グラフ
安定度と抽象度の関連が、(1, 0)と(0, 1)をつなぐ直線を主系列と呼んでいます。この主系列に近いところにプロットされるパッケージが、安定度と抽象度のバランスが取れた理想的なパッケージとされています。
図3の無駄ゾーン近辺にあるパッケージは、安定度が低いわりに抽象度が高いパッケージです。
このゾーン近辺にあるパッケージは、安定度が低いため他のパッケージの都合を考えずに自由に変更することができます。抽象度を高くして拡張しやすくしていることが無駄になってしまっているパッケージです。
図3の苦痛ゾーン近辺にあるパッケージは、安定度が高いわりに抽象度が低いパッケージです。
このゾーン近辺にあるパッケージは、パッケージを変更する際に、そのパッケージに依存している多くのパッケージにも変更の影響が出てしまうため苦痛を伴います。抽象度が低いため拡張しづらく、安定度の高さが仕様変更の妨げになってしまっているパッケージです。
ここで注意点として、言語標準ライブラリや普遍的なロジックのみを含んだパッケージなどの、「抽象度について」の項でまとめた抽象度が高いものがこのゾーン近辺にくることは問題ありません。つまり、変更されることがないものがこのゾーン近辺にきても苦痛を伴うことはないので気にしないようにしましょう。
主系列からの距離の算出方法
あるパッケージがどれだけ主系列から離れているのかは下記の方法で算出できます。
D: 距離
D = |A + I - 1|引用: Robert C. Martin他, Clean Architecture, 2018年7月27日, 株式会社ドワンゴ, P142
Dは0以上1以下の値を取ります。Dが0のときは、パッケージが主系列上にあることを表します。Dが1のときは、パッケージが主系列から最も離れていることを表しています。
便利な可視化ツール
ここでまとめた安定度・抽象度等価の原則の可視化を手動で行うのは大変です。
PHPの場合、PhpMetricsというツールを使うことで、こちらの可視化をコマンド1つで行えます。
とても便利なのでぜひ使ってみてください。
まとめ
安定度・抽象度等価の原則は、パッケージの安定度と抽象度は関連しており同等でなければいけないことを定めています。安定度が高いパッケージは抽象度も高くすることで、仕様変更の際に苦痛を伴わない変更しやすいシステムになります。逆に安定度が低いパッケージは抽象度は低くても問題なく、無駄がないシステムになります。
-
Robert C. Martin他, Clean Architecture, 2018年7月27日, 株式会社ドワンゴ, P138 ↩︎
Discussion