コンテンツにスキップ

オブジェクト指向の原則 オープン・クローズドの原則

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

NGな例

/// <summary>
/// Pointボタン(機能拡張に弱い例:NGパターン)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ng_pointButton_Click(object sender, EventArgs e)
{
    int price = 100;

    // 1%のポイントを付与する仕様
    //int point = Convert.ToInt32(price * 0.01f);
    //this.pointLabel.Text = $"{point.ToString()}p";

    // 上記のポイント付与条件に仕様変更があった場合、修正に閉じられていないため
    // 仕様変更があるたびに条件分岐を修正する必要がある
    int point;
    if (this.cardNumber.StartsWith("P"))
    {
        point = Convert.ToInt32(price * 0.02f);
    }
    else
    {
        point = Convert.ToInt32(price * 0.01f);
    }

    this.pointLabel.Text = $"{point.ToString()}p";

    // さらに言えば、画面にビジネスロジックを書いてしまっているため、
    // 本来は修正する必要のない画面クラスが修正対象となってしまう
}

ファクトリーメソッドによる拡張

ポイントクラスは、インターフェイスで拡張する

classDiagram
    IPoint <|-- Point
    IPoint <|-- GoldPoint
    IPoint <|-- SilverPoint

    class IPoint{
      <<interface>>
      +GetPoint()
    }

    class Point{
      +GetPoint()
    }

    class GoldPoint{
      +GetPoint()
    }

    class SilverPoint{
      +GetPoint()
    }

拡張する場合は、生成のみを責務としたFactoryと、クラスを追加するだけでOK

ファクトリー

/// <summary>
/// ポイントファクトリー
/// Gof : ファクトリーメソッドパターン
/// 新しいポイントを作成する場合はPointクラスを追加して、
/// 以下のCreatePointメソッドに条件分岐を追加する
/// </summary>
public class PointFactory
{
    /// <summary>
    /// ポイントを作成する
    /// </summary>
    /// <param name="cardNumber"></param>
    /// <returns></returns>
    public static IPoint CreatePoint(string cardNumber)
    {
        if (cardNumber.StartsWith("G"))
        {
            return new GoldPoint();
        }

        if (cardNumber.StartsWith("S"))
        {
            return new SilverPoint();
        }

        return new Point();
    }
}

ポイントクラス、ファクトリー

/// <summary>
/// ポイントインターフェイス
/// </summary>
public interface IPoint
{
    /// <summary>
    /// ポイントを取得する
    /// </summary>
    /// <param name="price"></param>
    /// <returns></returns>
    int GetPoint(int price);
}

/// <summary>
/// ゴールド会員ポイントクラス
/// </summary>
public class GoldPoint : IPoint
{
    public int GetPoint(int price)
    {
        return Convert.ToInt32(price * 0.03f);
    }
}

/// <summary>
/// スタンダード会員ポイントクラス
/// </summary>
public sealed class Point : IPoint
{
    public int GetPoint(int price)
    {
        return Convert.ToInt32(price * 0.01f);
    }
}

クライアント側は、ファクトリーで必要なポイントクラスを生成する

/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="cardNumber"></param>
public PointForm(string cardNumber) : this()
{
    this.cardNumber = cardNumber;
    this.point = PointFactory.CreatePoint(cardNumber);
}

/// <summary>
/// Pointボタン
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void pointButton_Click(object sender, EventArgs e)
{
    int price = 100;
    this.pointLabel.Text = $"{this.point.GetPoint(price).ToString()}p";
}

GitHub
https://github.com/pixcelo/CSharp-app/tree/main/OpenCloedPrinciple

予想されるバリエーション

インターフェイスを使用すべきか?を考えるには、拡張ポイントが必要かどうかを考慮する

  • 外部接続(データベースの違い、CSV、ファイルシステム)
  • 会員(会員ランク、通常or有料会員)
  • 決済方法(クレジット、現金、QR決済)
  • 通知方法(メール、SMS、プッシュ通知)
  • レポート形式(PDF、Excel、HTML)
  • ディスカウント戦略(パーセンテージ割引、固定割引、バンドル割引)
  • ロギング(ファイルログ、データベースログ、クラウドログ)
  • 認証方法(パスワード、OAuth、SAML)
  • UI(ライトテーマ、ダークテーマ、高コントラストテーマ)

Reference