読書メモ『良いコード/悪いコードで学ぶ設計入門』 5~7章
書籍について
staticメソッドについて
次の場面以外では基本的には使用しない
1. ファクトリメソッド
インスタンスの初期化ロジックが複数存在する場合、そのロジックをクラス外に記述してしまうことで凝集度が下がってしまう
それを避けるためにprivateコンストラクタとファクトリメソッドを使用することで初期化ロジックも凝集したクラスとなる
class GiftPoint
{
private const MIN_POINT = 0;
private const STANDARD_MEMBERSHIP_POINT = 3000;
private const PREMIUM_MEMBERSHIP_POINT = 10000;
// コンストラクタをprivateにする
private function __construct(
public readonly int $point,
) {
if ($point < self::MIN_POINT) {
throw new InvalidArgumentException();
}
}
// staticメソッドであるファクトリメソッド
public static function forStandardMembership(): self
{
return new self(self::STANDARD_MEMBERSHIP_POINT);
}
public static function forPremiumMembership(): self
{
return new self(self::PREMIUM_MEMBERSHIP_POINT);
}
}
2. 横断的関心事
さまざまなユースケースに広く横断する事柄を横断的関心事と呼ぶ
- ログ出力
- エラー検出
- デバッグ
- 例外処理
- キャッシュ
- 同期処理
- 分散処理
条件分岐を解きほぐす技法
早期リターンや早期continueなどを行わず、if文のネストが深くなる処理に対しては次のような技法が有効となる
interfaceを使用して条件を切り替えるストラテジパターン
種類ごとに切り替えたい機能をinterfaceのメソッドとして定義することで条件分岐を使わずに処理を切り替える
Magic
のinterfaceを実装したクラスは同じMagic
として扱うことが出来る
interface Magic
{
public function name(): string;
public function costMagicPoint(): int;
public function attackPowoer(): int;
public function costTechnicalPoint(): int;
}
class Fire implements Magic
{
public function __construct(
private readonly Member $member,
) {}
public function name(): string
{
return "ファイア";
}
public function costMagicPoint(): MagicPoint
{
return new MagicPoint(2);
}
public function attackPowoer(): AttackPower
{
$value = 20 + (int) ($this->member->level * 0.5);
return new AtackPower($value);
}
public function costTechnicalPoint(): TechnicalPoint
{
return new TechnicalPoint(0);
}
}
class HellFire implements Magic
{
public function __construct(
private readonly Member $member,
) {}
public function name(): string
{
return "地獄の業火";
}
public function costMagicPoint(): MagicPoint
{
return new MagicPoint(16);
}
public function attackPowoer(): AttackPower
{
$value = 200 + (int) ($this->member->level * 0.5 + $this->member->vitality * 2);
return new AtackPower($value);
}
public function costTechnicalPoint(): TechnicalPoint
{
$value = 20 + (int) ($this->member->level * 0.4);
return new TechnicalPoint($value);
}
}
Magic
のinterfaceを実装したクラスは同じMagic
として扱うことが出来るため、if文やswitch文を使用することなく、処理を分岐させることが出来る
$fire = new Fire($member);
$helFire = new HellFire($member);
$magics = array(
MagicType->fire => $fire,
MagicType->helFire => $helFire,
);
$usingMagic = $magics[MagicType->fire];
複雑な条件を集約するポリシーパターン
複数の条件によって処理を切り替えたい場合に、条件を部品化して部品となった条件を組み替えることでカスタマイズ可能にするのがポリシーパターン
interface ExcellentCustomerRule
{
public function ok(PurchaseHistory $history): bool;
}
class GoldCustomerPurchaseAmountRule implements ExcellentCustomerRule
{
public function ok(PurchaseHistory $history): bool
{
return 100000 <= $history->totalAmount;
}
}
class PurchaseFrequencyRule implements ExcellentCustomerRule
{
public function ok(PurchaseHistory $history): bool
{
return 10 <= $history->purchaseFrequencyPerMonth;
}
}
class ReturnRateRule implements ExcellentCustomerRule
{
public function ok(PurchaseHistory $history): bool
{
return $history->returnRule <= 0.001;
}
}
各条件を部品化して、その部品を組み替えることで複雑な条件の組み合わせに対して、カスタマイズ可能なものとしている
class ExcellentCustomerPolicy
{
public function __construct(
private readonly array $rules = array(),
) {}
public function add(ExcellentCustomerRule $rule): void
{
$this->rules[] = $rule;
}
public function complyWithAll(PurchaseHistory $history): bool
{
foreach ($this->rules as $rule) {
if (!$rule->ok($history)) {
return false;
}
}
return true;
}
}
class GoldCustomerPolicy
{
public function __construct(
private readonly ExcellentCustomerPolicy $policy,
) {
$policy = new ExcellentCustomerPolicy();
$policy->add(new GoldCustomerPurchaseAmountRule());
$policy->add(new PurchaseFrequencyRule());
$policy->add(new ReturnRateRule());
$this->policy = $policy;
}
public function complyWithAll(PurchaseHistory $history): bool
{
return $this->policy->complyWithAll($history);
}
}
class SilverCustomerPolicy
{
public function __construct(
private readonly ExcellentCustomerPolicy $policy,
) {
$policy = new ExcellentCustomerPolicy();
$policy->add(new PurchaseFrequencyRule());
$policy->add(new ReturnRateRule());
$this->policy = $policy;
}
public function complyWithAll(PurchaseHistory $history): bool
{
return $this->policy->complyWithAll($history);
}
}
型チェックで条件を分岐しない
同じ基本型を持つクラスに対して、型チェックを行い条件を分岐させるコードは「基本型を継承型に置き換えても問題なく動作しなければいけない」というリスコフの置換原則に違反することになる
リスコフの置換原則の例として、基本型であるHotelRates
というinterfaceの継承型となるRegularRates
とPremiumRates
に対して、HotelRates
を継承型に置換しても問題なく動作する
interface HotelRates
{
public function fee(): Money;
public function busySeasonFee(): Money;
}
class RegularRates implements HotelRates
{
public function fee(): Money
{
return new Money(7000);
}
public function busySeasonFee(): Money
{
return $this->fee()->add(new Money(3000));
}
}
class PremiumRates implements HotelRates
{
public function fee(): Money
{
return new Money(12000);
}
public function busySeasonFee(): Money
{
return $this->fee()->add(new Money(5000));
}
}
設計スキルのレベルに対する分岐の考え
中級者以上は分岐をinterface設計によって、分岐ごとの処理をクラス化によって解消する
設計スキル向上のために意識することは
「分岐を書きそうになったら、まずinterface設計!」
Discussion