スマホの料金プランの割引で分かるオープンクローズドの原則(OCP)

## 初めに
私は都内で受託、SESを柱としている会社で受託開発をしており、
バックエンドとフロントエンドを担当しています。
経験年数は後もう少しで3年になります。
自分はコード設計等の本や動画を見ていますが人にあまり語った経験がなく、どこかでアウトプットしたいと思い今回の記事を書く事に致しました。


## 概要
スマホにはいくつかのプランが存在します、そしてそれに伴う割引条件等も存在します。
今回はそのプランに応じての割引処理をする場合に発生する、
条件分岐や適用条件の判定などのロジックをオープンクローズドの原則を適用すると
シンプルになるという一例を示します。
今回はPHP(Laravel)で説明します。

###  テーブル
今回の説明で使うテーブルとmodelです。
主に使うものです。これ以外もさらっと出てきます
料金テーブル(rate_plans), 
割引テーブル(discounts), 
プランに対して適用可能割引の紐付けテーブル(applicable_discount_each_rate_plans),
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3326308/dcc6d675-8aae-1e25-8593-752b2d1b47ec.png)

```php
class RatePlan extends Model
{
    public function discounts()
    {
        return $this->belongsToMany(ApplicableDiscount::class, 'applicable_discount_each_rate_plans', 'rate_plan_id', 'discount_id');
    }
}

class ApplicableDiscount extends Model
{
    public function plans()
    {
        return $this->belongsToMany(RatePlan::class, 'applicable_discount_each_rate_plans', 'discount_id', 'rate_plan_id');
    }
}
```


#### 適用前

```CalcRateService.php
class CalcRateService
{

    /**
     * @var int $contractLineId 契約回線のID
     */
    private int $contractLineId;


    public function __construct(int $contractLineId)
    {
        $this->contractLineId = $contractLineId;
    }


    /**
     * 毎月の料金の算出
     */
    public function calcRate(): int
    {
        // 契約回線の契約内容取得
        $content = ContractContent::where('contractor_line_id', $this->contractLineId);

        // 紐づく適用可能割引
        $plan = $content->ratePlan

        // 適用可能割引取得
        $applicableDiscounts = $plan->discounts()->get();

        // 算出
        $ratePlanPrice = $plan->price;

        foreach($applicableDiscounts as $applicableDiscount) {
            // 光割引の場合
            if ($applicableDiscount->key === "OPTICAL_LINE") {
                // 適用条件
                // 契約回線の契約者またはその回線のファミリーグループ内に光回線の契約があるか
                if (光回線の契約がある) {
                    $ratePlanPrice = $ratePlanPrice - $applicableDiscount->price;
                }
            }

            // 家族割引の場合
            if ($applicableDiscount->key === "FAMILY") {
                // 適用条件
                // 契約回線がファミリーグループに含まれているかつグループに回線が3回線以上あるか
                if (契約回線がファミリーグループに含まれているかつグループに回線が3回線以上ある) {
                    $ratePlanPrice = $ratePlanPrice - $applicableDiscount->price;
                }
            }
            
        }
        return $ratePlanPrice;

    }


}
```

割引の種類は実際は学割などもう少し多いです。
その際に今のままのコードだと割引の種類に応じてif文を追加する必要があります。
また適用条件もかく割引ごとに異なるのでそこの部分の処理の追加も必要になります。
上記のコードでは省略していますが、実際にはデータベースから取得して判断等をします。
結構コードが読みづらくなると思います。


#### 適用後
まずオープンクローズドの原則とは

>SOLID原則の、Open Closed Principleのことで、「ソフトウェアの構成要素は拡張に対して開いていて、修正に対して閉じていなければならない」という原則です。

言い換えると新しいバリエーション(割引ごとの処理)が増える時に既存のコードのロジックをほとんどいじる必要がなく新しいバリエーションを追加するだけで動く状態にできるようにしとくということだと思います。

それぞれは割引の適用可能かの判定の処理が今回バリエーションになります。
以下のコードのようにバリエーションを抽象化します。
そしてその具象クラスとして各バリエーションを作成します。

```php
interface ApplicablePlanDiscount
{
    // 適用可能か判断
    public function applicable(): bool;
}

// 光割引
class ApplicableOpticalLineDiscount implements PlanDiscount
{
    public static $key = "OPTICAL_LINE";

    /**
     * @var int $contractLineId 契約回線のID
     */
    private int $contractLineId;


    public function __construct(int $contractLineId)
    {
        $this->contractLineId = $contractLineId;
    }

    public function applicable(): bool
    {
        // 契約回線の契約者またはその回線のファミリーグループ内に光回線の契約があるか
    }

}

// 家族割引
class ApplicableFamilyDiscount implements PlanDiscount
{
    public static $key = "FAMILY";

    /**
     * @var int $contractLineId 契約回線のID
     */
    private int $contractLineId;


    public function __construct(int $contractLineId)
    {
        $this->contractLineId = $contractLineId;
    }

    public function applicable(): bool
    {
        // 契約回線がファミリーグループに含まれているかつグループに回線が3回線以上あるか
    }
}
```
```ApplicablePlanDiscountFactory.php
class ApplicablePlanDiscountFactory
{
    private array $discounts = [
        'OPTICAL_LINE' => ApplicableOpticalLineDiscount::class,
        'FAMILY'       => ApplicableFamilyDiscount::class,
    ];

    public static function create(string $key, int $contractLineId): ApplicablePlanDiscount
    {
        $className = self::$discounts[$key];
        return new $className($contractLineId);
    }

}
```
バリエーションを抽象化したことでkeyに対応するクラスのapplicable()を呼び出しをApplicableFamilyDiscountのapplicable()を呼び出すというものと同一となりました。
これにより、新しいバリエーション(割引)を追加する場合は、ApplicablePlanDiscountを
実装した新しいクラスを作り、ApplicablePlanDiscountFactoryの$discountsの配列に同じ形で追加するだけです。
算出ロジックはいじらなくていい状態です。



```CalcRateService.php
class CalcRateService
{
    /**
     * @var int $contractLineId 契約回線のID
     */
    private int $contractLineId;


    public function __construct(int $contractLineId)
    {
        $this->contractLineId = $contractLineId;
    }


    /**
     * 毎月の料金の算出
     */
    public function calcRate(): int
    {
        // 契約回線の契約内容取得
        $content = ContractContent::where('contractor_line_id', $this->contractLineId);

        // 紐づく適用可能割引
        $plan = $content->ratePlan

        // 適用可能割引取得
        $applicableDiscounts = $plan->discounts()->get();

        // 割引を加味した料金を算出
        $ratePlanPrice = $plan->price;
        foreach($applicableDiscounts as $applicableDiscount) {
            // 割引適用条件判定インスタンス生成
            $discount = ApplicablePlanDiscountFactory::create(
                $applicableDiscount->key, 
                $this->contractLineId
            );
            if ($discount->applicable()) {
                $ratePlanPrice = $ratePlanPrice - $applicableDiscount->price;
            }
        }
        return $ratePlanPrice;
    }
}
```
## 注意
オープンクローズドの原則はバリエーションが複数存在するものに対して分かりやすく適用し易いと思います。
ただ、原則は適用させようと言うよりは、自分の考えのエビデンス的なものに近いのかなと思います。
今回の例題で大事なのは様々な割引を割引という概念で一括りで捉えることで具体的なバリエーションをカプセル化することだと思います。

## 最後に
私自身これが正解かどうかは分かりません。
色んな方のアドバイスや指摘を受けコード設計の力を高めていきたいと思っています。
ですので、少しでも疑問や違和感を感じたことがあれば質問して頂ければ幸いです。