目录
介绍
我们为什么需要接口?
那么我们为什么不能使用它们呢?
真正的问题——业务逻辑
但我需要接口......
所以我该怎么做?
介绍关于如何编写好代码有很多指南。许多组织实现静态代码分析以验证和提高代码质量。开发人员越来越意识到干净的代码是什么,SOLID,设计模式,GRASP ......如此多的指令如何编码,所以我们开始忘记这些是什么。
我们为什么需要接口?接口的第一个用例是描述实现它的对象的行为。如果一个类Dog实现了接口IAnimal,我们确信它(并非总是)可以Move和Eat。当然,Dog也可以Bark或者Sit,但这是具体的实现,不同于Bird可以Fly。
接口的更复杂的用例是多态(通常与依赖注入相结合)。如果我们的代码依赖于抽象(在这种情况下是接口),那么它更灵活。我们可以使用任何我们想要的类,它实现了必要的接口。
由于接口,我们可以减少类之间的耦合。如果您的实现基于抽象(例如接口),则可以通过更改接口的实现来更改应用程序的输出而无需更改源代码。
那么我们为什么不能使用它们呢?在我刚开始做开发的时候,我没有问很多问题,我只是做了其他更有经验的同事所做的事。但过了一段时间,我开始看到设计和实现方面的缺陷。
你有多少次见过这样的接口:
namespace Calculator
{
public interface IMathCalculator
{
int Sum(int a, int b);
int Subtract(int a, int b);
}
}
通过这样的实现:
namespace Calculator
{
public class MathCalculator : IMathCalculator
{
public int Sum(int a, int b)
{
return a + b;
}
public int Subtract(int a, int b)
{
return a - b;
}
}
}
甚至是这样的:
public class CalculatorConfigProvider : ICalculatorConfigProvider
{
public double GetPiValue()
{
return 3.14;
}
}
public interface ICalculatorConfigProvider
{
double GetPiValue();
}
好的,那么这段代码可能有什么问题呢?
在我看来,这个接口完全没有必要。接口建议可能的多个实现。在这种情况下,我们只有一个具有接口名称的实现。接口及其实现位于同一名称空间中,因此使用具体实现而不是抽象并不成问题。
更重要的是,如果使用IoC容器(例如,Ninject),则需要使用其接口注册所有类。这是另一件要记住的事情,它可能看起来像这样:
Kernel.Bind().To();
Kernel.Bind().To();
Kernel.Bind().To();
项目越大,注册IoC 容器时添加的条目就越多。初始化类变得更大,更难维护。如果需要在两个不同的作用域中注册依赖项(例如,WebApi的请求范围和某些异步无限循环的命名作用域),则需要两次注册依赖项,这是一个灾难。
真正的问题——业务逻辑我们谈论的是良好实践、耦合、可维护性等。但我认为真正的问题是当你处理域和业务逻辑时。这是应用程序的关键部分——有时是应用程序,甚至是整个公司的关键点和目的。这部分包含一些严肃的计算,算法或规则。
如果您向此类添加接口然后注入此接口,则您无法控制它的使用方式。特别是当您制作API或其他应用程序使用的库时。
OrderGenerator执行从Products生成Order的域逻辑的一个简单示例显示了一个想法:
public class OrderGenerator
{
private readonly TaxCalculator calculator;
public OrderGenerator(TaxCalculator calculator)
{
this.calculator = calculator;
}
public Order Generate(IEnumerable products)
{
var order = new Order();
foreach (var product in products)
{
var price = calculator.CalculatePrice(product);
order.AddPosition(product, price);
}
return order;
}
}
TaxCalculator注入到OrderGenerator并根据产品类型、税收价格和国家法律进行一些计算。
public class TaxCalculator
{
public Price CalculatePrice(Product product)
{
// Some domain-specific calculations for getting price of a product.
}
}
通过此实现,您可以确定OrderGenerator将为给定产品正确计算价格。
现在考虑代码审查,一些资深专家建议在TaxCalculator中增加一个接口,并在OrderGenerator中注入一个ITaxCalculator。看起来很简单,这听起来是个好主意。根据SOLID您认为接口隔离,您知道接口是好的。代码看起来也不错。:)
public interface ITaxCalculator
{
public Price CalculatePrice(Product product);
}
现在,OrderGenerator开始进行修改。这是什么意思?这意味着您可以实现一个新的TaxCalculator,例如,NoTaxCalculator实现ITaxCalculator和返回0价格。
问题是它是您的应用程序或者你的生态系统或者你的公司的关键部分。
现在,有人可以打破它。:)
但我需要接口......嗯......我听说过为什么有必要在类中添加接口。这里是其中的一些:
- 在IoC 容器中注册类以将它们注入另一个类
- 单元测试中的模拟测试
- 编写干净的代码并减少类之间的耦合
但是,这是真的吗?
当IoC容器包含一个public构造函数时,它应解析一个类,该构造函数包含可由IoC 容器解析的所有参数。
如果你为测试添加一个JUST接口——不要。您的生产代码不应该只是为了满足测试而编写。您可以在测试项目中执行任何操作,但保留生产代码。:)
使用接口而不是具体实现可以减少耦合,这是事实。但是在IoC容器中注册接口会增加复杂性并降低可维护性,因此如果您在应用程序中仅使用它一次,那么我认为它的优缺点相等。
所以我该怎么做?首先,您应该考虑满足所有业务需求。:)如果您编写有史以来最干净的代码,但不满足您的业务,那么该应用程序是无用的。
接口具有很大的价值,应该在每个应用程序中使用它们。多亏了接口,你可以使用多态,添加很多模式,比如我们的策略、工厂、命令模式等等。您可以反向依赖(在SOLID中为D),因此如果您的域使用存储库或适配器,则可以注入接口,并且实现应该在单独的层中。
但作为一切,你应该明智地使用它们。:)并非所有类都应该有自己的接口。这不是唯一的解决方案。