要約
私は、コードベースのいくつかの機能のまとまりに頭を悩ませようとしています。私はさまざまな方法でこの設計にアプローチしましたが、最近、単一責任の原則を誤って適用した場合など、間違ったアプローチを採用したと確信しています。
問題
問題の領域は井戸であり、地面の丸い穴としても知られています。現在のコードは次のようになります...データのみのオブジェクトにデータのセットがあります...
注:これは完全なコードではなく、物事を短くするためのメソッド名だけです...
public class WellData
{
public string Name { get; set; }
public ICollection<SurveyPoint> SurveyPoints { get; set; }
public ICollection<GeometryItem> GeometryItems { get; set; }
public ICollection<TemperaturePoint> WellTemperature { get; set; }
public ICollection<FluidPoint> WellFluids { get; set; }
}
このデータを操作する機能をいくつかの小さなクラスに分割しました。以下のように…
public class ReferenceWellSurvey
{
ICollection<SurveyPointCalculate> _calculatedSurveyPoints;
double GetTvdAtDepth(double depth) {}
double GetAzimuthAtDepthRad(double depth) {}
double GetInclinationAtDepthRad(double depth){}
double GetTortuosityPeriodAtDepth(double depth ) {}
double GetTortuosityAmplitudeAtDepth(double depth ) {}
}
public class ReferenceWellGeometry
{
ICollection<GeometryItem> _geometryItems;
double GetFrictionAtDepth(double depth) {}
double GetHoleIdAtDepth(double depth) {}
}
public class ReferenceWellTemperature
{
ICollection<TemperaturePoint> _wellTemperatures;
double GetTemperatureAtDepth(double depth) {}
double GetSurfaceTemperature(double depth) {}
}
Etc.
最近、コードを見て、アプリケーションロジックで必要な場所で使用できるようにするには、これらすべてを接着して戻す必要があることに気付きました。何かのようなもの…。
public class ReferenceWellData
{
private IReferenceWellGeometry _refWellGeometry;
private IReferenceWellTemperature _refWellGeometry;
private IReferenceWell _refWellGeometry;
private IReferenceFluid _refWellFluid;
//Proxy functions from each class to glue it all back together…
}
私を現在の混乱に導いたいくつかのこと。ウェルデータクラスにはいくつかの情報のコレクションが含まれており、すべてが1つのクラスにある場合、ほとんどの関数はデータの小さなサブセットに対してのみ動作します。それがまとまりを低くすると感じていた当時、数ヶ月後、これらのものは実際にはすべて一緒に属していると感じています。さらに、私のコードには、ReferenceWellSurveyクラスの1つのメソッドであるGetTvdAtDepth()を呼び出す必要があるクラスがいくつかあります。本当に私がしたことはこの問題を解決しませんでした。
これについてもっと考えた後、そしてこのウィキペディアの記事を読んだ後 http://en.wikipedia.org/wiki/Cohesion_(computer_science) 、私はいくつかのプログラミングの罪を犯したことに気づき始めています。
•低凝集度
•物事を接着しようとするコードのコンストラクターオーバーインジェクション
•GetTvdAtDepthに依存するクラスに、必要のない他のメソッドへのアクセスを許可すると、インターフェイス分離の原則に違反します。
ソリューション
ある程度のまとまりを保ちながら、どのクラスにもあまり責任を負わない(神オブジェクトなし)、Goldiloxのデザインを探しています。
これを修正することについての私の現在の考えは次のとおりです。井戸データを操作する機能を同じクラスに移動します。これは、この機能をコミュニケーション/情報のまとまりに近づけるのに役立つはずです。
public class WellData : IWellData
{
public string Name { get; set; }
public ICollection<SurveyPoint> SurveyPoints { get; set; }
public ICollection<GeometryItem> GeometryItems { get; set; }
public ICollection<TemperaturePoint> WellTemperature { get; set; }
public ICollection<FluidPoint> WellFluids { get; set; }
// Operates on SurveyPoint collection
double GetTvdAtDepth(double depth) {}
double GetAzimuthAtDepthRad(double depth) {}
double GetInclinationAtDepthRad(double depth){}
double GetTortuosityPeriodAtDepth(double depth ) {}
double GetTortuosityAmplitudeAtDepth(double depth ) {}
//Operates on WellGeometry collection
double GetFrictionAtDepth(double depth) {}
double GetHoleIdAtDepth(double depth) {}
//Operates on WellTemperature collection
double GetTemperatureAtDepth(double depth) {}
double GetSurfaceTemperature(double depth) {}
}
次に、1つの関数だけを必要とする複数のクラスの問題に対処するために、この1つの関数を個別のインターフェイスIGetTvdAtDepthにすることができます。このように、その機能を必要とするクラスは、WellDataインターフェイスに継承してそこに実装できるこのインターフェイスのみに依存できますが、他のクラスをWellDataクラスにあるすべての機能に結び付けることはできません。
このアプローチと元のアプローチについての考えは?私のAPIで、このクラスの必要な依存関係を満たすためにろくでなしの注入を使用していることです...現在、3つの必要な依存関係、Survey計算機、およびデータを参照する特別なクラスがあります。異なる測定深度で。私はこれから離れたいのですが、それはサービスにもっと多くのリフティングを行わせることに戻っているようです...
Uberインターフェースではなく、クラスを深く定義することから始めます 閉塞 抽象化。
私はそれを私の「油井ドメイン」データ構造を定義するものと考えています。
アイデアは、独立した/個別の意味を持つビットを発見することです。クラス定義が進化するにつれて、それ自体がクラスになる可能性のある部分を抽出します。その後、必要に応じて合成します。物事が進展するにつれて、常にクラスを編集します。このアプローチにより、横断的関心事に対処する高レベルのクラスを進化させることができます。
ReferenceWell
はWell
ですか、それとも派生したWell
ですか?静的または「参照」データ/状態は、それを単なる別のWell
にします。HoleID
は、Well
オブジェクトを一意に識別しますこれはSurveyPoint
とは何でしょうか?
public class SurveyPoint
{
// Each property is typed. This is intentional.
// It gives us a place to encapsulate properties and
// behaviors/methods that are "atomic" to each.
public int HoleID { get; set; }
public Geometry Geometry {get; set;}
public Depth Depth {get; set;}
public TemperaturePoint Temperature { get; set; }
public Inclination Inclination { get; set; }
public Tortousity Torture { get; set; }
public Asmuth Asmuth { get; set; }
public Tvd Tvd { get; set; }
}
そして、TemperaturePoint
、FluidPoint
、GeometryItem
などは、SurveyPoint
のサブセット(サブクラスではない)にすぎませんか?言い換えれば、さまざまな側面。そう..
public class SurveyPoint
{
//. . .
public Temperature GetTemperature() {return this.Temperature;}
public FluidPoint GetFluidPoint () {return this.FluidPoint;}
// etc. and overload as needed.
}
public class SurveyPointCollection : Collection<SurveyPoint> {
public TemperatureCollection GetTemperatures() {}
public FluidPointCollection GetFluidPoints() {}
public GetTemperature(Depth depth) {
// iterate collection to find the SurveyPoint with that depth
}
//etc.
}
public class Temperature {
public HoleID {get; set;}
public double Temperature {get; set;}
// any other properties help *define* a temperature for a well
// add any useful methods that operate only on properties defined in this class
}
// a similar class for each of the other SurveyPoint aspects.
コアデータ構造を中心に進化し、回転する結束の1つの側面がわかります。データクラスが(十分に)詳細に記述されると、動作をデータカプセル化の適切なレベルに「マッピング」し始め、非常にまとまりのあるオブジェクト指向クラスを作成できます(はい、@ FrankHilemanは正しいですが、機能的には客観的ではないと考えています)。
次に、Well
が次のようになり始めると思います
public class Well
{
public string Name {get; set;}
public int HoleID {get; set;}
public SurveyPointCollection SurveyPoints {get; set;}
public Well () { }
public TemperatureCollection GetTemperatures () {
SurveyPoints.GetTemperatures();
}
public Temperature GetTemperature ( Depth depth ) { }
// etc. for all other aspects.
}
行動の側面を探る
public class Surveyer
{
protected Well MyWell { get; set; }
protected DepthCalculator Calculator { get; set; }
public Surveyer ( Well theWell ) {
MyWell = theWell;
Calculator = new DepthCalculator (MyWell.GetSurveyPoints());
}
}
「深さ」はReferenceWellSurvey
メソッド名の統一的なもののように思われるので、私はDepthCalculator
を想像しました。ただし、さまざまなデータの側面に対応する計算がある可能性があります。だから私たちはこれを持っているかもしれません:
public class DepthCalculator {
protected TemperatureCalculator {get; set;}
protected InclinationCalculator {get; set;}
protected FrictionCalculator {get; set;}
// etc.
public DepthCalculator (SurveyPointCollection dataPoints) {
TemperatureCalculator = new TemperatureCalculator (dataPoints.GetTemperatures());
FrictionCalculator = new FrictionCalculator(dataPoints.GetFrictionPoints());
}
}
「オイルウェルドメイン」については、これをどこに行けばよいのか十分にわかりません。ただし、インスピレーションを得るために Builderパターン を見るかもしれません。
デザインパターンやデザインガイドラインが単純さの妨げになることがあります。あなたが説明する問題(設計上の問題ではなく、ソフトウェアによって解決される元の問題)は、構成可能な関数で構成されるカスタム言語によって最もよく解決されるように思えます。パーサーなどが必要であることを意味するのではなく、広い意味で「言語」という言葉を使用します。オブジェクトはデータを保持し、関数はそのデータに対する操作を提供します。これはオブジェクト指向分析ではなく、機能分析です。その後、よりオブジェクト指向になりたい場合は、データをカプセル化するいくつかのオブジェクトにいくつかの関数を割り当ててみることができます。おそらく、機能設計が最初に来るべきであり、次にオブジェクト指向設計が来るべきです。
私はあなたがあなたの仕事ではるかに悪い罪を犯したと思います:あなたは何のテストも書きませんでした。はるかに抽象的な方法から、コードを使用するコードの観点からコードを見ていませんでした。設計しようとしているものを使用するコードを実際に作成すると、抽象化と動作を分割するための可能な方法を確認するのがはるかに簡単になります。したがって、実際にはより良い設計につながります。
次に注意すべきことは、SRPは優れていますが、残りのOLID原則なしでは使用しないでください。 SRPは、責任と呼ばれるものがあり、それを認識しておく必要があることを私たちに思い出させます。しかし、責任とは「2つの数字を足す責任」から「すべての顧客の問題を解決する責任」まで、あらゆるものを意味する可能性があるため、設計の基礎となるのは非常に悪いルールです。これにより、SRPは非常にあいまいになります。ソフトウェア設計は複雑な分野であり、1つの原則だけに基づいているのはせいぜいばかげています。
どんな答えも、私たちが与えられた問題/モデルの説明によってバイアスされます。より役立つドメインを見る他の方法があるかもしれません
ウェルクラスと、ウェルに関連する測定値のコレクションがいくつかあるようです。測定が異なる時間に繰り返される可能性がある場合、同じ種類であっても、単一のウェルに複数の測定セットが含まれる可能性があります
これは単純な構造につながります:オブジェクトとオブジェクトの一連の測定/計算-
Well - a concrete class for an instance of a well, including properties intrinsic to the well (like HoleID or Name)
WellOperation - an abstract class for a set of related Measurements on a specific Well
ReferenceWellTemperature - a bunch of temperature measurements
ReferenceWellGeometry - a bunch of geometry measurements
etc.
各運用サブクラスには、特定の井戸を参照するデータが含まれています。単一のウェルには、これらが0個以上ある場合があり、異なる時点で同じ種類の倍数が取得される場合があります
したがって、ReferenceWellDataの例のような包括的なクラスは必要ありません。測定操作の数と種類が変更される可能性があります。これにより、包括的なクラスのインターフェイスが変更されますが、これは必須ではありません。
ReferenceWellSurvey、ReferenceWellGeometryなどが必要ない場合...コミュニケーションをとるには、この内訳を行うのが理にかなっています(実装の観点から)。ただし、クラス構造を確立するために実装レベルの詳細に依存することはお勧めできません。
「最近、コードを見て、アプリケーションロジックで必要な場所で使用できるようにするには、これらすべてを接着して戻す必要があることに気付きました。」つまり、このクラスのユーザーは、実装レベルの分解について気にしない(したがって、公開されないほうがよい)からです。しかし、この方法で物事を接着することは必ずしも悪いことではありません。
「私を現在の混乱に導いたいくつかのこと」...現在の混乱は何ですか?それぞれが関連データの小さなセットを担当するクラスのコレクションが別のクラスの実装をサポートするという事実は、混乱を構成するものではありません。ここで、ReferenceWellSurveyをReferenceWellGeometry(または同様のもの)からインポートする必要がある場合これらのサポートクラス間の依存関係)、まあ、それは混乱し、「実装指向」の内訳を損なうでしょう。
「物事のやり方で私を悩ませていることの1つは、私のAPIで、このクラスの必要な依存関係を満たすためにろくでなしの注入を使用していることです...現在3つの必要な依存関係があります」これは問題ですか?もしそうなら、それはIWellDataを定義するための理論的根拠を提供するかもしれません。
結論として、提案された解決策は問題ないかもしれないと思います。これは責任の原則に反しますか?たぶん、しかし、あなたのドメインで「Well」の定義を支持する設計は、実装レベルのクラスを作成するよりも優れた設計だと思います。
機能クラスには、実際の内部状態または可能なバリエーション/代替実装がありますか?そうでない場合は、ReferenceWellGeometry
を、コレクションに1つの追加パラメーターを受け取るすべてのstatic
メソッドを持つユーティリティクラスに書き換えることができますか?例えば.
static double GetFrictionAtDepth( ICollection<GeometryItem> _geometryItems, double depth) {}
このスタイルには欠点がありますが、あなたの場合、クラスの依存性/注入の問題をすべて単純化する可能性があります。