初めて API を設計し、SOLID ガイドラインに従おうとしています。私が苦労していることの 1 つは、OCP とテスト容易性と、単純さと拡張性の容易さとのバランスを取ることです。
このオープンソース API は、科学的なモデリングと計算を対象としています。目的は、さまざまなグループが特定のモデルをこの「プラグ可能な」アーキテクチャに簡単にインポートできるようにすることです。したがって、プロジェクトとしての成功は、これらの科学者が不必要なオーバーヘッドや急な学習曲線なしでドメイン固有の知識を容易に伝えることができるかどうかにかかっています。
たとえば、計算エンジンは「ベクトル化された」計算に依存しています。1 つのスカラー値だけを計算する必要はほとんどありません。多くのモデルはこれを利用して「オーバーヘッド」計算を実行し、各スカラー サブ計算で再利用できます。しかし、ユーザーがデフォルトのベクトル化動作を継承する (または別の方法で提供される) 単純なスカラー操作を定義できるようにしたいと考えています。
私の目標はそれを作ることでした
1) 初心者ユーザーが基本的な計算モデルをできるだけ簡単に実装できるようにする 2) 上級ユーザーがベクトル化動作をオーバーライドできるようにできるだけ簡単にする
...もちろん、SoC、テスト容易性などを維持しながら。
いくつかの改訂の後、シンプルでオブジェクト指向のものになりました。計算コントラクトはインターフェイスを介して定義されますが、ユーザーは、デフォルトのベクトル化を提供する抽象 ComputationBase クラスから派生することをお勧めします。以下は、デザインを縮小したものです。
public interface IComputation<T1, T2, TOut>
{
TOut Compute(T1 a, T2 b);
}
public interface IVectorizedComputation<T1, T2, TOut>
{
IEnumerable<TOut> Compute(IEnumerable<T1> a, IEnumerable<T2> b);
}
public abstract class ComputationBase<T1, T2, TOut> :
IComputation<T1, T2, TOut>,
IVectorizedComputation<T1, T2, TOut>
{
protected ComputationBase() { }
// the consumer must implement this core method
public abstract TOut Compute(T1 a, T2 b);
// the consumer can optimize by overriding this "dumb" vectorization
// use an IVectorizationProvider for vectorization capabilities instead?
public virtual IEnumerable<TOut> Compute(IEnumerable<T1> a, IEnumerable<T2> b)
{
return
from ai in a
from bi in b
select Compute(ai, bi);
}
}
public class NoobMADCalculator
: ComputationBase<double, double, double>
{
// novice user implements a simple calculation model
// CalculatorBase will use a "dumb" vectorization
public override double Compute(double a, double b)
{
return a * b + 1337;
}
}
public class PwnageMADCalculator
: ComputationBase<double, double, double>
{
public override double Compute(double a, double b)
{
var expensive = PerformExpensiveOperation();
return ComputeInternal(a, b, expensive);
}
public override IEnumerable<double> Compute(IEnumerable<double> a, IEnumerable<double> b)
{
foreach (var ai in a)
{
// example optimization: only perform this operation once
var expensive = PerformExpensiveOperation();
foreach (var bi in b)
{
yield return ComputeInternal(ai, bi, expensive);
}
}
}
private static double PerformExpensiveOperation() { return 1337; }
private static double ComputeInternal(double a, double b, double expensive)
{
return a * b + expensive;
}
}
ComputationBase のベクトル化された Compute には、もともと (コンストラクター DI を介して) プロバイダー パターンを使用していましたが、スカラーの Compute は抽象化したままにしました。基本クラスは常にベクトル化操作を「所有」しますが、注入されたプロバイダーに計算を委譲します。これは、テスト容易性とベクトル化コードの再利用の観点からも、一般的に有益であると思われました。ただし、これには次の問題がありました。
1) スカラー (継承) およびベクトル (プロバイダー) 計算のアプローチの不均一性は、ユーザーを混乱させる可能性が高く、要件に対して過度に複雑に見え、コードの悪臭を放っていました。
2) ベクトル化のために「別個の」プロバイダーを作成することは、漏れやすい抽象化でした。プロバイダーがスマートに何かを行う場合、通常、クラスの実装に関する内部知識が必要になります。それらを実装するためにネストされたプライベート クラスを作成していることに気付きました。
これは、OCP 対テスト容易性対シンプルさを考慮した適切なアプローチですか? 他の人は、さまざまなレベルの複雑さで拡張するために API をどのように設計しましたか? 私が含めたよりも多くの依存性注入メカニズムを使用しますか? また、この特定の例に対する回答と同じくらい、優れた API 設計に関する優れた一般的な参考文献にも関心があります。ありがとう。
ありがとう、デビッド