🛫

クリーンアーキテクチャ基礎 #2 OCP

に公開

はじめに

『Clean Architecture 達人に学ぶソフトウェアの構造と設計』を読んで学んだ内容をここでアウトプット。

SRP編

https://zenn.dev/th_code/articles/ef55f1bea7b6a5

OCP:オープン・クローズド原則について

ソフトウェアの構成要素は拡張に対しては開いていて、修正に対して閉じていなければならない。
『アジャイルソフトウェア開発の奥義 第2版』より引用

SRP編でも触れたように 「機能修正や仕様変更に強い設計」を実現するための原則です。
システムに拡張(追加)または修正が発生した際に影響範囲を最小化し、リスクを低減し、テスト工数を削減することを目的とします。

どのようにして実現するのか?
それは変更や追加の発生が見込まれる箇所に「拡張ポイント」と呼ばれるインターフェース(厳密にはインターフェース・抽象クラス・仮想メソッド)を設置し追加または修正した機能を呼び出す側が呼び出し先を変更を気にすることなく実行させることです。

要するに変更や追加が見込まれる場合、使用する側がそれを意識しなくても動作できるよう抽象的に呼び出すことができるようにしましょうということ。

インターフェースをimplementsを付けたクラスで機能を実装すること、そしてインターフェースで統一的な受け口を用意した後、どの具象を使用するのかの判断がいる場合にはGoFデザインパターンとしてファクトリーを使用し、分岐を切り離すことで使用する側が機能追加の度にテストしないといけない煩雑さを排除し、修正・テストの範囲を局所・明確化させることができる。

安定したソフトウェアアーキテクチャは、変化しやすい具象への依存を避け、安定した抽象インターフェイスに依存すべきである。
『Clean Architecture 達人に学ぶソフトウェアの構造と設計』より引用

ポイントシステムを例に紹介します。
【要件】

  • 特定のユーザによってポイントを切り替える
  • 切り替える条件を追加し新しいポイント制度 -ゴールド会員- を導入したい

BAD例

通常会員 ポイント1倍

class RegularPoint
{
    public function calculate(int $basePoint): int
    {
        return $basePoint;
    }
}

シルバー会員 ポイント 1.2倍

class SilverPoint
{
    public function calculate(int $basePoint): int
    {
        // 1.2倍にする
        return (int) ($basePoint * 1.2);
    }
}

クライアントコード

$regularPoint = new RegularPoint();
$silverPoint = new SilverPoint();

if ($memberType === "regular") {
    $point = $regularPoint->calculate($payment);
} else if ($memberType === "silver") {
    $point = $silverPoint->calculate($payment);
}

echo $point;

OCP導入

  • 拡張ポイントとなるインターフェースを作成します。
interface PointStrategy
{
    public function calculate(int $basePoint): int;
}

次に追加したいゴールド会員用のクラスを作成します。

  • ゴールド会員(ポイント 1.5倍)
class GoldMemberPoint implements PointStrategy
{
    public function calculate(int $basePoint): int
    {
        return (int) ($basePoint * 1.5);
    }
}

そして、RegularPointSilverPointクラスに対してimplements PointStrategyを行います。

どのユーザによるのか?という分岐部分をファクトリーに委任させます。

<?php
class PointStrategyFactory
{
    public static function create(string $memberType): PointStrategy
    {
        return match ($memberType) {
            'silver' => new SilverPoint()
            'gold'   => new GoldMemberPoint(),
            default  => new RegularPoint(),
        };
    }
}

最終的なクライアントコード

$strategy = PointStrategyFactory::create($memberTypeFromDB);
$service  = new PointService($strategy);
$point   = $service->give(200);

まとめ(要約)

  • OCP の目的は「拡張に開き、修正に閉じる」ことで変更コストとリスクを最小化すること。
  • 変更が予測される箇所に 抽象インターフェース を置き、振る舞いを後付けできるようにする。
  • ファクトリー+戦略パターンを活用すると、呼び出し側から分岐が消え、テスト範囲が局所化される。

Discussion