1

我是第一次设计 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 设计的一般参考资料感兴趣,而不是对这个特定示例的回答。谢谢。

谢谢,大卫

4

1 回答 1

1

如果你可以没有继承,你可以只使用 Funcs。它们提供了一种传递任意代码的简单方法,并且可以提供更简单的东西。基本上是这样的:

Func<double, double, double> pwnageComputation;//takes 2 doubles and returns one double
pwnageComputation = (num1, num2) => 
{
    if (num1 + num2 > 1337)
        return 1;
    else if (num1 + num2 < 1337)
        return -1;
    return 0;
}

Func<>s 是 Lambda 表达式的实现,它基本上是委托的包装器,以使它们更易于使用(至少在 c# 中)。通过这种方式,您可以让您的用户编写临时函数(类似于您的代码),但没有类定义的复杂性(他们只需要提供一个函数)。您可以在此处(下半场)或此处了解有关它们的更多信息。

于 2009-09-29T06:04:10.837 回答