我是第一次设计 API,并尝试遵循 SOLID 指南。我发现自己挣扎的一件事是平衡 OCP 和可测试性与简单性和易于扩展性。
这个开源 API 面向科学建模和计算。其目的是让各个小组能够轻松地将他们的特定模型导入到这种“可插拔”架构中。因此,它作为一个项目的成功将取决于这些科学家在没有不必要的开销或过于陡峭的学习曲线的情况下传授他们特定领域知识的难易程度。
例如,我们的计算引擎依赖于“矢量化”计算——我们很少需要只计算一个标量值。许多模型可以利用这一点并执行“开销”计算以在每个标量子计算中重复使用。但是,我希望用户能够定义一个简单的标量操作,它将继承(或以其他方式提供)默认的矢量化行为。
我的目标是成功
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 中的向量化计算,我最初使用了提供者模式(通过构造函数 DI),但将标量计算保持为抽象。理由是这是很好的“受保护的变体”——基类将始终“拥有”向量化操作,但将计算委托给注入的提供程序。从可测试性和矢量化代码重用的角度来看,这似乎通常也是有益的。但是,我遇到了以下问题:
1) 标量(继承)和向量(提供者)计算方法的异质性似乎可能使用户感到困惑,要求似乎过于复杂,而且代码味道很差。
2) 为向量化创建一个“单独的”提供者是一个有漏洞的抽象——如果提供者要做任何聪明的事情,它通常需要类的实现的内部知识。我发现自己创建了私有嵌套类来实现它们,这告诉我这是一个无法分离的问题
这是 w/r/t OCP vs 可测试性 vs 简单性的好方法吗?其他人是如何设计他们的 API 以在各种复杂程度下进行扩展的?你会使用比我所包含的更多的依赖注入机制吗?我也对关于良好 API 设计的一般参考资料感兴趣,而不是对这个特定示例的回答。谢谢。
谢谢,大卫