単一責任の原則(Single Responsibility Principle)

定義

SOLID原則のひとつ。 単一責任の原則について調べると以下のように記述されている。

  • Every class should have only one reason ton change.
  • A module should be responsible to one, and only one actor.

前者は「クラスの変更理由はひとつ」、後者は「モジュールはひとつのアクターにのみ責任をもつ」。

"クラス"と"モジュール"についてはほぼ同義と考えて良いと思う。
そうすると、上記の2つは以下のように考えられる。

  • クラスはひとつのアクターの要求に答える責任を持つ
  • クラスの変更理由は対象のアクターの要求が変更された場合のみ

例)カロリー計算

以下は、1日の最大摂取カロリーと減量または増量を設定。食事ごとに1日の最大摂取カロリーとの比較を行うクラス。

public class CalorieControl
{
  private int MaxCalorie;    // 1日の最大摂取カロリー
  private bool LossWeight;   // 減量する場合はtrue
  
  private int TotalCalorie;  // 1日の摂取カロリー
  
  public CalorieControl(int maxCalorie, bool lossWeight)
  {
    this.MaxCalorie = maxCalorie;
    this.LossWeight = lossWeight;
  }
  
  public void Eat(int calorie)
  {
    this.TotalCalorie += calorie;
    
    if (this.LossWeight)
    {
        if (this.TotalCalorie <= this.MaxCalorie) return;

        System.Console.WriteLine($"Over Calorie. {this.TotalCalorie}");
    }
    else
    {
        if (this.TotalCalorie > this.MaxCalorie) return;

        System.Console.WriteLine($"Less Calorie. {this.TotalCalorie}");
    }
  }
}

問題点

上記のプログラムについて考えてみる。

『クラスはひとつのアクターの要求に答える責任を持つ』

"カロリー計算をしたい人"がアクターと考えると、その要求に答える責任は持っているように思える。

『クラスの変更理由は対象のアクターの要求が変更された場合のみ』

対象のアクターについては良いように思えたが、何を目的にカロリー計算をしたいのかに着目する。
対象クラスのLossWeightプロパティが該当する。
bool値であることから、2つの目的が考えられる。
Trueの場合は、減量するために"カロリー計算をしたい人"。
Falseの場合は、増量するために"カロリー計算をしたい人"。
『減量するために"カロリー計算をしたい人"』が、以下のような要求を出す。
『食品の表示カロリーと実際のカロリーには乖離があると思うので、比較する際に許容範囲を持たせたい。』
LossWeightがTrueの場合の比較部分の軽微な修正で終わりそう。
このとき、『増量するために"カロリー計算をしたい人"』は理由なくクラスが修正されている。

もう一度『クラスはひとつのアクターの要求に答える責任を持つ』

アクターは"カロリー計算をしたい人"ではなく、"減量したい人"と"増量したい人" が正しい。
そのために必要なのが"カロリー計算"(汎化)で、クラスは以下のようになる。

public class CalorieControl
{
  private int TargetCalorie; // 1日の目標摂取カロリー
  private int TotalCalorie;  // 1日の摂取カロリー
  
  public CalorieControl(int maxCalorie) => this.MaxCalorie = maxCalorie;
  
  public void Eat(int calorie)
  {
    this.TotalCalorie += calorie;
  }
}

public class WeightLoss : CalorieControl
{
  public WeightLoss(int targetCalorie) : base(targetCalorie) { }

  public override void Eat(int calorie)
  {
    base.Eat(calorie);

    if (this.TotalCalorie <= this.TotalCalorie) return;

    System.Console.WriteLine($"Over Calorie. {this.TodaysCalorie}");
  }
}

public class WeightGain : CalorieControl
{
  public WeightGain(int targetCalorie) : base(targetCalorie) { }

  public override void Eat(int calorie)
  {
    base.Eat(calorie);

    if (this.TotalCalorie > this.TotalCalorie) return;

    System.Console.WriteLine($"Less Calorie. {this.TodaysCalorie}");
  }
}