为什么大多数系统架构师坚持首先对接口进行编程?
-
09-06-2019 - |
题
我读过的几乎每一本 Java 书籍都谈到使用接口作为在对象之间共享状态和行为的一种方式,而这些对象在第一次“构造”时似乎并不共享关系。
然而,每当我看到架构师设计应用程序时,他们做的第一件事就是开始对接口进行编程。怎么会?您如何知道该界面中将发生的对象之间的所有关系?如果您已经知道这些关系,那么为什么不直接扩展一个抽象类呢?
解决方案
对接口进行编程意味着尊重使用该接口创建的“契约”。所以如果你的 IPoweredByMotor
接口有一个 start()
方法,未来实现接口的类,无论是 MotorizedWheelChair
, Automobile
, , 或者 SmoothieMaker
, ,在实现该接口的方法时,为您的系统增加灵活性,因为一段代码可以启动许多不同类型事物的马达,因为一段代码需要知道的是它们响应 start()
. 。没关系 如何 他们开始,只是他们 必须开始.
其他提示
很好的问题。我会推荐你去 Josh Bloch 在《Effective Java》中, ,谁写了(第 16 项)为什么更喜欢使用接口而不是抽象类。顺便说一句,如果你还没有这本书,我强烈推荐它!以下是他所说的摘要:
- 现有的类可以很容易地进行改造以实现新的接口。 您所需要做的就是实现接口并添加所需的方法。现有的类不能轻易地进行改造以扩展新的抽象类。
- 接口是定义混合的理想选择。 混合接口允许类声明附加的可选行为(例如,Comparable)。它允许将可选功能与主要功能混合在一起。抽象类不能定义混合——一个类不能扩展多个父类。
- 接口允许非分层框架。 如果您有一个具有许多接口功能的类,它可以实现所有这些接口。如果没有接口,您将不得不创建一个臃肿的类层次结构,并为每个属性组合创建一个类,从而导致组合爆炸。
- 接口可增强安全功能。 您可以使用装饰器模式创建包装类,这是一种健壮且灵活的设计。包装类实现并包含相同的接口,将一些功能转发给现有方法,同时向其他方法添加专门的行为。您不能使用抽象方法来做到这一点 - 您必须使用继承,而继承更脆弱。
抽象类提供基本实现的优点是什么?您可以为每个接口提供一个抽象骨架实现类。这结合了接口和抽象类的优点。骨架实现提供实现帮助,而不会施加抽象类在用作类型定义时所施加的严格约束。例如, 馆藏框架 使用接口定义类型,并为每个接口提供一个骨架实现。
接口编程有几个好处:
GoF 类型模式必需,例如访客模式
允许替代实现。例如,对于抽象正在使用的数据库引擎的单个接口,可能存在多个数据访问对象实现(AccountDaoMySQL 和 AccountDaoOracle 可能都实现 AccountDao)
一个类可以实现多个接口。Java 不允许具体类的多重继承。
摘要实施细节。接口可能只包含公共 API 方法,隐藏实现细节。好处包括记录清晰的公共 API 和记录良好的合同。
被现代依赖注入框架大量使用,例如 http://www.springframework.org/.
在 Java 中,接口可用于创建动态代理 - http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/Proxy.html. 。这可以非常有效地与 Spring 等框架一起使用来执行面向方面的编程。方面可以向类添加非常有用的功能,而无需直接向这些类添加 java 代码。此功能的示例包括日志记录、审计、性能监控、事务划分等。 http://static.springframework.org/spring/docs/2.5.x/reference/aop.html.
模拟实现、单元测试 - 当依赖类是接口的实现时,可以编写也实现这些接口的模拟类。模拟类可用于促进单元测试。
怎么会?
因为所有的书都是这么说的。就像 GoF 模式一样,许多人认为它普遍良好,并且从未考虑过它是否真的是正确的设计。
您如何知道该界面中将发生的对象之间的所有关系?
你不这样做,这是一个问题。
如果您已经知道这些关系,那么为什么不扩展抽象类呢?
不扩展抽象类的原因:
- 您有完全不同的实现,并且制作一个像样的基类太难了。
- 你需要为了别的东西而烧毁你唯一的基类。
如果两者都不适用,请继续使用抽象类。这会节省你很多时间。
你没有问的问题:
使用接口有哪些缺点?
你无法改变它们。与抽象类不同,接口是一成不变的。一旦你使用了它,扩展它就会破坏代码,就这样。
我真的需要吗?
大多数时候,没有。在构建任何对象层次结构之前,请认真思考。像 Java 这样的语言的一个大问题是,它使得创建大量、复杂的对象层次结构变得太容易了。
考虑一下 LameDuck 继承自 Duck 的经典示例。听起来很容易,不是吗?
好吧,直到你需要表明鸭子已经受伤并且现在跛了。或者表明跛脚鸭已经痊愈,可以重新行走了。Java 不允许更改对象类型,因此使用子类型来指示跛行实际上行不通。
对接口进行编程意味着使用该接口创建的“合同”
这是关于接口最容易被误解的事情。
无法通过接口强制执行任何此类契约。根据定义,接口根本不能指定任何行为。类是行为发生的地方。
这种错误的信念如此普遍,以至于被许多人视为传统智慧。然而,这是错误的。
所以OP中的这个声明
我阅读的几乎每本Java书都谈论使用该界面作为共享对象之间状态和行为的一种方式
这是不可能的。接口既没有状态也没有行为。他们可以定义实现类必须提供的属性,但这已经是他们所能得到的最接近的了。您无法使用接口共享行为。
您可以假设人们将实现一个接口来提供其方法名称所暗示的行为,但这并不是同一件事。并且它对何时调用此类方法没有任何限制(例如,应该在 Stop 之前调用 Start)。
这个说法
GoF 类型模式必需,例如访客模式
也是不正确的。GoF 书完全使用零接口,因为它们不是当时使用的语言的功能。这些模式都不需要接口,尽管有些模式可以使用它们。在我看来,观察者模式是一种接口可以发挥更优雅作用的模式(尽管现在该模式通常使用事件来实现)。在访问者模式中,几乎总是需要一个基类访问者类来实现每种类型的访问节点的默认行为,即 IME。
就我个人而言,我认为这个问题的答案有三个:
接口被许多人视为灵丹妙药(这些人通常在“契约”误解下工作,或者认为接口神奇地解耦了他们的代码)
Java 人员非常注重使用框架,其中许多框架(正确地)需要类来实现其接口
在引入泛型和注释(C# 中的属性)之前,接口是执行某些操作的最佳方式。
接口是一个非常有用的语言特性,但也被严重滥用。症状包括:
一个接口只能由一个类实现
一个类实现多个接口。通常被吹捧为接口的优点,通常这意味着相关类违反了关注点分离原则。
接口存在继承层次结构(通常由类层次结构反映)。这是您首先要通过使用接口来避免的情况。过多的继承对于类和接口来说都是一件坏事。
在我看来,所有这些都是代码味道。
在我看来,你经常看到这种情况,因为这是一种非常好的做法,但经常被应用在错误的情况下。
接口相对于抽象类有很多优点:
- 您可以切换实现,而无需重新构建依赖于接口的代码。这对于:代理类、依赖注入、AOP等。
- 您可以将 API 与代码中的实现分开。这可能很好,因为当您更改会影响其他模块的代码时,它会变得很明显。
- 它允许开发人员编写依赖于您的代码的代码,以便轻松模拟您的 API 以进行测试。
在处理代码模块时,您可以从接口中获得最大的优势。然而,没有简单的规则来确定模块边界应该在哪里。所以这种最佳实践很容易被过度使用,尤其是在第一次设计某些软件时。
我认为(@eed3s9n)它是为了促进松散耦合。此外,如果没有接口,单元测试就会变得更加困难,因为您无法模拟对象。
为什么延伸是邪恶的. 。这篇文章几乎是对所提出问题的直接回答。我几乎想不出你实际上会这么做的情况 需要 一个抽象类,以及很多情况下这是一个坏主意。这并不意味着使用抽象类的实现很糟糕,但您必须小心,以免接口契约依赖于某些特定实现的工件(例如:Java 中的 Stack 类)。
还有一件事:到处都有接口并不是必要的,也不是好的做法。通常,您应该确定何时需要接口,何时不需要。在理想的情况下,第二种情况大多数时候应该作为最终类来实现。
这里有一些很好的答案,但如果您正在寻找具体的原因,那么单元测试就是最好的选择。
假设您想要测试业务逻辑中的一个方法,该方法检索发生交易的区域的当前税率。为此,业务逻辑类必须通过存储库与数据库对话:
interface IRepository<T> { T Get(string key); }
class TaxRateRepository : IRepository<TaxRate> {
protected internal TaxRateRepository() {}
public TaxRate Get(string key) {
// retrieve an TaxRate (obj) from database
return obj; }
}
在整个代码中,使用类型 IRepository 而不是 TaxRateRepository。
存储库有一个非公开的构造函数,以鼓励用户(开发人员)使用工厂来实例化存储库:
public static class RepositoryFactory {
public RepositoryFactory() {
TaxRateRepository = new TaxRateRepository(); }
public static IRepository TaxRateRepository { get; protected set; }
public static void SetTaxRateRepository(IRepository rep) {
TaxRateRepository = rep; }
}
工厂是唯一直接引用 TaxRateRepository 类的地方。
因此,您需要一些支持此示例的类:
class TaxRate {
public string Region { get; protected set; }
decimal Rate { get; protected set; }
}
static class Business {
static decimal GetRate(string region) {
var taxRate = RepositoryFactory.TaxRateRepository.Get(region);
return taxRate.Rate; }
}
IRepository 还有另一个实现 - 模型:
class MockTaxRateRepository : IRepository<TaxRate> {
public TaxRate ReturnValue { get; set; }
public bool GetWasCalled { get; protected set; }
public string KeyParamValue { get; protected set; }
public TaxRate Get(string key) {
GetWasCalled = true;
KeyParamValue = key;
return ReturnValue; }
}
由于实时代码(业务类)使用工厂来获取存储库,因此在单元测试中,您将为 TaxRateRepository 插入 MockRepository。替换完成后,您可以对返回值进行硬编码,从而不再需要数据库。
class MyUnitTestFixture {
var rep = new MockTaxRateRepository();
[FixtureSetup]
void ConfigureFixture() {
RepositoryFactory.SetTaxRateRepository(rep); }
[Test]
void Test() {
var region = "NY.NY.Manhattan";
var rate = 8.5m;
rep.ReturnValue = new TaxRate { Rate = rate };
var r = Business.GetRate(region);
Assert.IsNotNull(r);
Assert.IsTrue(rep.GetWasCalled);
Assert.AreEqual(region, rep.KeyParamValue);
Assert.AreEqual(r.Rate, rate); }
}
请记住,您只想测试业务逻辑方法,而不是存储库、数据库、连接字符串等......每一个都有不同的测试。通过这样做,您可以完全隔离正在测试的代码。
一个附带的好处是,您还可以在没有数据库连接的情况下运行单元测试,这使得它更快、更便携(想想远程位置的多开发人员团队)。
另一个附带好处是您可以在开发的实施阶段使用测试驱动开发 (TDD) 流程。我并不严格使用 TDD,而是混合使用 TDD 和老式编码。
从某种意义上说,我认为您的问题归结为简单地说:“为什么使用界面而不是抽象的课程?”从技术上讲,您可以与两者都实现松散的耦合 - 基础实现仍未暴露于呼叫代码,并且您可以使用抽象的出厂模式返回基础实现(接口实现VS。抽象类扩展)以增加设计的灵活性。事实上,您可能会说抽象类为您提供了更多的功能,因为它们允许您要求实现来满足您的代码(“您必须实现 start()”)并提供默认实现(“我有一个标准的paint(),您如果您愿意,可以重写”)——对于接口,必须提供实现,随着时间的推移,这可能会因接口更改而导致脆弱的继承问题。
但从根本上来说,我使用接口主要是因为 Java 的单一继承限制。如果我的实现必须从调用代码使用的抽象类继承,这意味着我失去了从其他东西继承的灵活性,即使这可能更有意义(例如用于代码重用或对象层次结构)。
原因之一是接口允许增长和可扩展性。举例来说,假设您有一个将对象作为参数的方法,
公共无效的饮料(咖啡塞姆林克){
}
现在假设您想使用完全相同的方法,但传递一个 hotTea 对象。好吧,你不能。您刚刚将上述方法硬编码为仅使用咖啡对象。也许那是好的,也许那是坏的。上述方法的缺点是,当您想传递各种相关对象时,它会严格将您锁定在一种类型的对象中。
通过使用接口,比如 IHotDrink,
接口 IHotDrink { }
并重写上面的方法以使用接口而不是对象,
公共无效的饮料(ihotdrink SomeDrink){
}
现在您可以传递实现 IHotDrink 接口的所有对象。当然,您可以编写完全相同的方法,使用不同的对象参数执行完全相同的操作,但为什么呢?你突然要维护臃肿的代码。
这一切都是关于编码之前的设计。
如果在指定接口后您不知道两个对象之间的所有关系,那么您定义接口的工作就很糟糕——这是相对容易修复的。
如果您直接投入编码并在中途意识到自己遗漏了一些东西,那么修复起来就会困难得多。
您可以从 perl/python/ruby 的角度来看这一点:
- 当您将对象作为参数传递给方法时,您不传递它的类型,您只知道它必须响应某些方法
我认为将 java 接口作为类比可以最好地解释这一点。你并没有真正传递一个类型,你只是传递一些响应方法的东西(一个特征,如果你愿意的话)。
我认为在 Java 中使用接口的主要原因是单一继承的限制。在许多情况下,这会导致不必要的复杂化和代码重复。看看 Scala 中的 Traits: http://www.scala-lang.org/node/126 特征是一种特殊的抽象类,但一个类可以扩展其中的许多类。