web-dev-qa-db-ja.com

クリーンなコード:メンバー変数をプライベートメソッドに渡さずに「注文事項」を回避しますか?

私は、Robert C. MartinがClean Codeに示した原則のいくつかを実装しようとしています。

私は注文の問題にひどく苦しんでいたクラスを持っていました。私は、コードを個別のクラスに抽出することにより、これのほとんどを解決しました。
メインクラスは次のようになっています。

_public class Planner {
   public EstimateCollection ChosenEstimates { get; set; } // Want a better name for this..
   public PlannedYear YearPlan { get; set; }

   public Planner(List<Estimate> estimates) {
      ChosenEstimates = new EstimateCollection(estimates);
      YearPlan = ReadExistingPlan();
   }                                                                                       

   private PlannedYear ReadExistingPlan() {
      var planReader = new PlanReader();
      var rawPlan = planReader.GetScheduleFromDatabase(ChosenEstimates.StartDate, ChosenEstimates.EndDate);

      var planConverter = new PlanConverter();
      return planConverter.ConvertRowsToPlannedYear(rawPlan);
   }
}

// This class was made by extracting most of what used to be in Planner's constructor
public class EstimateCollection {
   public List<Estimate> Estimates { get; set; }
   public DateTime StartDate { get; }
   public DateTime EndDate { get; }

   public EstimateCollection(List<Estimate> estimates) {
      Estimates = estimates;
      StartDate = GetEarliestStartDate();
      EndDate = GetLatestEndDate();
   }

   private DateTime GetEarliestStartDate() {
      // Do something to get start date from Estimates list
   }

   private DateTime GetLatestEndDate() {
      // Do something to get end date Estimates list
   }
}
_

今私が抱えている問題は、Plannerクラスのコンストラクター(8行目)にあります。最初にChosenEstimates変数が設定され、次にYearPlan変数が設定されます。 ReadExistingPlan()ChosenEstimatesを使用するため、YearPlan varを設定してからChosenEstimatesReadExistingPlan()メソッドで設定する必要があります。

パラメータとしてChosenEstimatesからReadExistingPlan()を指定することもできますが、クリーンコードの例ではほとんどこれを実行できません。

これを回避できるように、コードにさらに構造的な変更はありますか?
今、私はどちらの問題をしたいかを選択しています。

1
MSOACC

ChosenEstimatesをパラメーターとしてReadExistingPlan()に提供することはできますが、クリーンコードの例ではほとんどこれを行いません。

実際にクリーンなコードは、303ページでこれを正確に実行します。

G31:非表示の時間的カップリング
一時的な結合が必要になることがよくありますが、結合を非表示にしないでください。 >関数の引数を、それらを呼び出す順序が明白になるように構造化します。次の点を考慮してください。

_public class MoogDiver {
 Gradient gradient;
 List<Spline> splines;
 public void dive(String reason) {
     saturateGradient();
     reticulateSplines();
     diveForMoog(reason);
 }

 ...
}
_

3つの関数の順序は重要です。スプラインを網状にする前に勾配を飽和させてから、ムーグに飛び込むことができます。残念ながら、コードはこの一時的な結合を強制しません。別のプログラマーは、saturateGradientが呼び出される前にreticulateSplinesを呼び出し、UnsaturatedGradientExceptionを引き起こす可能性があります。より良い解決策は:

_public class MoogDiver {
     Gradient gradient;
     List<Spline> splines;
     public void dive(String reason) {
         Gradient gradient = saturateGradient();
         List<Spline> splines = reticulateSplines(gradient);
         diveForMoog(splines, reason);
     }
     ...
}
_

これは、バケット旅団を作成することにより、時間的カップリングを公開します。各関数は次の関数が必要とする結果を生成するため、それらを順不同で呼び出す合理的な方法はありません。これにより機能が複雑になると文句を言うかもしれませんが、あなたは正しいでしょう。しかし、その余分な構文の複雑さは、状況の真の時間的な複雑さを明らかにします。インスタンス変数はそのままにしたことに注意してください。これらはクラスのプライベートメソッドで必要になると思います。それでも、時間的な結合を明示的にするために適切な議論が必要です。

ここでマーティン氏は、オブジェクト内でより機能的なアプローチを取ることを提唱しています。奇妙に思われるが、それは動作します。それはあなたが物事を順番に行うことを強制します。

ただし、まったく別の理由により、このクラスでは引き続き問題が発生します。オブジェクトの状態のChosenEstimatesYearPlanの両方を作成しています。しかし、YearPlanは、事実上、ChosenEstimatesの関数です。したがって、YearPlanは文字どおりChosenEstimatesの関数である必要があります。どういう意味ですか?

YearPlanを保存する場所にChosenEstimatesを保存しないことを意味します。この2つは互いに同意しない可能性があります(特に、公開セッターを提供している場合)。 YearPlansゲッターがReadExistingPlan(ChosenEstimates)を返すようにして、それらの整合性を強制します。

このようにして、あなたのPlannerを奇妙な状態にすることはできません。

これをさらに一歩進めて、 コンストラクタで行われている余分な作業を排除する とすることができます。コンストラクターが引数を検証して状態を設定するだけの場合は、この方法を使用します。コンストラクターでそれ以上のことが起こった場合、それを取り除く方法を探します。コンストラクターの仕事は、オブジェクトメソッドの動作を変更することです。彼らのために彼らの仕事をすることではありません。

estimatesは唯一の状態フィールドである可能性があり、ここにある他のすべてのものは、それを開始点として使用します。しかし、ChosenEstimatesも同様です。 Plannerを新しいestimatesに渡して、ChosenEstimatesestimatesを強制的に構築し、EstimateCollectionも構築します。ここでは何もestimatesを直接使用していないため、ここで裸になる理由はありません。

私は本格的なコードレビュー(ここではcodereview.stackexchange.comで行います)に発展しているように見えるかもしれませんが、これを行うことにより、それ自体がクラスから押し出されます。順不同のことはコンパイルされないため、今は順不同ではありません。

3
candied_orange

この回答はcandied_orangeの回答を拡張したものです。(回答とこのスタブを削除します。)

ReadExistingPlanメソッドはChosenEstimatesフィールド全体を使用しません。むしろ、そのStartDateおよびEndDateプロパティのみを使用します。呼び出し元が適切な量の情報をReadExistingPlanに渡すと、コードがわかりやすくなります。

これが完了すると、ReadExistingPlanフィールドの「依存関係」が完全な状態から縮小されているため、PlannedYearメソッドをChosenEstimatesクラスに移動できる可能性があることがわかります。 -blownオブジェクトを2つのDateTimeフィールドに下げ、パラメーターとして渡すことができます。

ReadExistingPlanPlannedYearの静的メソッド(ファクトリーメソッド)にすることも、コンストラクターメソッドにすることもできます。ただし、このメソッドにPlannerからの追加が必要な場合は、さらに検討する必要があります。

2
rwong