为啥大多数系统架构师都坚持首先对接口进行编程?
Posted
技术标签:
【中文标题】为啥大多数系统架构师都坚持首先对接口进行编程?【英文标题】:Why do most system architects insist on first programming to an interface?为什么大多数系统架构师都坚持首先对接口进行编程? 【发布时间】:2010-09-08 02:18:13 【问题描述】:我读过的几乎每一本 Java 书籍都谈到了使用接口作为在对象之间共享状态和行为的一种方式,这些对象在第一次“构造”时似乎并不共享关系。
但是,每当我看到架构师设计应用程序时,他们做的第一件事就是开始对接口进行编程。怎么会?您如何知道将在该接口中发生的对象之间的所有关系?如果您已经知道这些关系,那为什么不直接扩展一个抽象类呢?
【问题讨论】:
【参考方案1】:对接口编程意味着尊重使用该接口创建的“合同”。因此,如果您的IPoweredByMotor
接口具有start()
方法,那么实现该接口的未来类,无论是MotorizedWheelChair
、Automobile
还是SmoothieMaker
,在实现该接口的方法时,都会为您的系统增加灵活性,因为一段代码可以启动许多不同类型事物的马达,因为这一段代码只需要知道它们对start()
的响应。他们如何开始并不重要,只是他们必须开始。
【讨论】:
值得指出的是,接口不是合约。合同是行为规范:组件 x 保证它将在场景 z 中执行 y。接口什么都不保证。要获得与验证行为耦合的接口的描述,就必须使用类。 这是一个虚拟合约。就像大多数雇佣合同(不幸的是,还有其他一些合同)一样,您可以选择遵守它或将其视为点火器。 @Scott:我会说接口就是合约。不是技术意义上的(即除了接口中提到的所有方法的存在之外,编译器不会强制执行合同)。但是一个接口需要每个方法的描述才算完整,这个描述代表了契约 它可能在某种意义上增加了灵活性,但在另一种意义上也增加了刚性:如果您后来发现您的抽象是错误的,界面将渗透到项目的每个角落;你完蛋了。【参考方案2】:这是推广松散coupling的一种方式。
在低耦合的情况下,一个模块的更改将不需要更改另一个模块的实现。
这个概念的一个很好的用法是Abstract Factory pattern。在 Wikipedia 示例中,GUIFactory 接口生成 Button 接口。具体的工厂可能是 WinFactory(生产 WinButton)或 OSXFactory(生产 OSXButton)。想象一下,如果您正在编写一个 GUI 应用程序,并且您必须查看 OldButton
类的所有实例并将它们更改为 WinButton
。那么明年,你需要添加OSXButton
版本。
【讨论】:
【参考方案3】:我认为抽象类在很大程度上被开发人员抛弃的原因之一可能是一种误解。
当Gang of Four写道:
编程到接口而不是实现。
没有 java 或 C# 接口之类的东西。他们谈论的是每个类都有的面向对象的接口概念。 Erich Gamma 在this interview 中提到了它。
我认为不加思索地机械地遵循所有规则和原则会导致难以阅读、导航、理解和维护代码库。记住:最简单的事情可能会奏效。
【讨论】:
【参考方案4】:我会假设(使用@eed3s9n)这是为了促进松散耦合。此外,如果没有接口,单元测试会变得更加困难,因为您无法模拟您的对象。
【讨论】:
【参考方案5】:从某种意义上说,我认为您的问题可以归结为“为什么要使用接口而不是抽象类?”从技术上讲,你可以实现两者的松耦合——底层实现仍然没有暴露给调用代码,你可以使用抽象工厂模式返回一个底层实现(接口实现与抽象类扩展)来增加你的灵活性设计。事实上,您可能会争辩说抽象类给了您更多,因为它们允许您既需要实现来满足您的代码(“您必须实现 start()”)并提供默认实现(“我有一个标准的 paint()如果你愿意,可以覆盖") -- 对于接口,必须提供实现,随着时间的推移,这可能会通过接口更改导致脆弱的继承问题。
不过,从根本上说,我使用接口主要是因为 Java 的单一继承限制。如果我的实现必须从一个抽象类继承以供调用代码使用,这意味着我失去了从其他东西继承的灵活性,即使这可能更有意义(例如,对于代码重用或对象层次结构)。
【讨论】:
【参考方案6】:对接口进行编程有几个好处:
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。
模拟实现、单元测试 - 当依赖类是接口的实现时,可以编写模拟类来实现这些接口。模拟类可用于促进单元测试。
【讨论】:
实现访问者模式不需要接口。【参考方案7】:很好的问题。我会把你推荐给Josh Bloch in Effective Java,他写了(第 16 条)为什么更喜欢使用接口而不是抽象类。顺便说一句,如果你还没有这本书,我强烈推荐它!以下是他所说的摘要:
-
可以轻松改造现有类以实现新接口。您需要做的就是实现接口并添加所需的方法。现有的类不能轻易改造以扩展新的抽象类。
接口是定义混入的理想选择。混入接口允许类声明额外的可选行为(例如,Comparable)。它允许将可选功能与主要功能混合。抽象类不能定义混合——一个类不能扩展多个父类。
接口允许使用非分层框架。如果您的类具有许多接口的功能,则它可以实现所有接口。如果没有接口,您将不得不为每个属性组合创建一个带有类的臃肿类层次结构,从而导致组合爆炸。
接口可以增强安全功能。您可以使用 Decorator 模式创建包装类,这是一种强大而灵活的设计。包装类实现并包含相同的接口,将一些功能转发给现有方法,同时将特殊行为添加到其他方法。你不能用抽象方法来做到这一点 - 你必须使用继承,这更脆弱。
抽象类提供基本实现的优势是什么?您可以为每个接口提供一个抽象的骨架实现类。这结合了接口和抽象类的优点。骨架实现提供了实现帮助,而不会强加抽象类在用作类型定义时所强制的严格约束。例如,Collections Framework 使用接口定义类型,并为每个接口提供一个框架实现。
【讨论】:
【参考方案8】:在我看来,您经常看到这种情况,因为这是一种非常好的做法,但经常被应用在错误的情况下。
接口相对于抽象类有很多优点:
您可以切换实现,而无需重新构建依赖于接口的代码。这对于:代理类、依赖注入、AOP 等很有用。 您可以在代码中将 API 与实现分开。这可能很好,因为当您更改会影响其他模块的代码时,它会很明显。 它允许开发人员编写依赖于您的代码的代码,以便轻松地模拟您的 API 以进行测试。在处理代码模块时,您可以从接口中获得最大的优势。但是,没有简单的规则来确定模块边界应该在哪里。因此,这种最佳实践很容易被过度使用,尤其是在首次设计某些软件时。
【讨论】:
【参考方案9】:怎么会?
因为所有的书都是这么说的。与 GoF 模式一样,许多人认为它普遍适用,从不考虑它是否真的是正确的设计。
您如何知道将在该界面中发生的所有对象之间的关系?
你没有,这是个问题。
如果 你已经知道这些关系, 那么为什么不只是扩展一个摘要 上课?
不扩展抽象类的原因:
-
您的实现完全不同,制作一个体面的基类太难了。
你需要烧掉你唯一的基类来做别的事情。
如果两者都不适用,请继续使用抽象类。这将为您节省大量时间。
你没有问的问题:
使用界面有什么缺点?
您无法更改它们。与抽象类不同,接口是一成不变的。一旦你有一个在使用,扩展它会破坏代码,句号。
我真的需要吗?
大多数时候,没有。在构建任何对象层次结构之前,请认真思考。像 Java 这样的语言的一个大问题是它太容易创建大量复杂的对象层次结构。
考虑 LameDuck 继承自 Duck 的经典示例。听起来很简单,不是吗?
嗯,直到你需要表明鸭子已经受伤并且现在是瘸腿的。或表示瘸腿鸭已经痊愈,可以再次行走。 Java 不允许您更改对象类型,因此使用子类型来指示跛行实际上不起作用。
【讨论】:
【参考方案10】:一个原因是接口允许增长和可扩展性。例如,假设您有一个将对象作为参数的方法,
公共无效饮料(咖啡 someDrink)
现在假设您想使用完全相同的方法,但传递一个 hotTea 对象。好吧,你不能。您只是将上述方法硬编码为仅使用咖啡对象。也许那是好的,也许那是坏的。上述方法的缺点是,当您想传递各种相关对象时,它会将您严格锁定在一种类型的对象中。
通过使用接口,比如 IHotDrink,
接口 IHotDrink
并重写您的上述方法以使用接口而不是对象,
公共无效饮料(IHotDrink someDrink)
现在您可以传递所有实现 IHotDrink 接口的对象。当然,您可以编写完全相同的方法,用不同的对象参数执行完全相同的操作,但为什么呢?你突然维护臃肿的代码。
【讨论】:
【参考方案11】:Why extends is evil。这篇文章几乎是对所提问题的直接回答。我几乎想不出你真的需要一个抽象类的情况,而且在很多情况下这是个坏主意。这并不意味着使用抽象类的实现不好,但是您必须注意不要让接口契约依赖于某些特定实现的工件(例如:Java 中的 Stack 类)。
还有一件事:没有必要,也没有好的做法,到处都有接口。通常,您应该确定何时需要接口,何时不需要。在理想的世界中,第二种情况大部分时间都应该作为最终类来实现。
【讨论】:
【参考方案12】:对接口进行编程意味着尊重由 使用那个界面
这是关于接口的最容易被误解的事情。
没有办法通过接口强制执行任何此类合同。根据定义,接口根本不能指定任何行为。类是行为发生的地方。
这种错误的信念是如此普遍,以至于被许多人认为是传统智慧。然而,这是错误的。
所以OP中的这个语句
我读过的几乎每一本 Java 书籍都谈到了使用接口作为一种方式 在对象之间共享状态和行为
是不可能的。接口既没有状态也没有行为。他们可以定义实现类必须提供的属性,但这是尽可能接近的。您不能使用接口共享行为。
您可以假设人们将实现一个接口以提供其方法名称所暗示的那种行为,但这不是一回事。并且它对何时调用此类方法没有任何限制(例如,应在 Stop 之前调用 Start)。
此声明
GoF 类型模式所必需,例如访问者模式
也不正确。 GoF 书完全使用零接口,因为它们不是当时使用的语言的特性。没有一种模式需要接口,尽管有些模式可以使用它们。 IMO,观察者模式是一种接口可以扮演更优雅角色的模式(尽管现在该模式通常使用事件来实现)。在访问者模式中,几乎总是需要一个基本访问者类来实现每种类型的访问节点的默认行为,即 IME。
我个人认为这个问题的答案有三个:
接口被许多人视为灵丹妙药(这些人通常在“合同”误解下工作,或者认为接口神奇地解耦了他们的代码)
Java 人非常注重使用框架,其中许多(正确地)需要类来实现其接口
在引入泛型和注释(C# 中的属性)之前,接口是做某些事情的最佳方式。
接口是一个非常有用的语言特性,但被滥用了很多。症状包括:
一个接口只能由一个类实现
一个类实现多个接口。通常被吹捧为接口的一个优点,通常这意味着所讨论的类违反了关注点分离的原则。
接口有一个继承层次结构(通常由类层次结构反映)。这是您首先要通过使用接口来避免的情况。对于类和接口来说,过多的继承是一件坏事。
所有这些都是代码异味,IMO。
【讨论】:
【参考方案13】:一切都是在编码之前进行设计。
如果您在指定接口后不知道两个对象之间的所有关系,那么您在定义接口方面做得很差——这相对容易修复。
如果您直接潜入编码并在中途意识到您遗漏了一些东西,那么您将很难修复。
【讨论】:
【参考方案14】:这里有一些很好的答案,但如果你正在寻找一个具体的原因,那就看看单元测试吧。
假设您想在业务逻辑中测试一个方法,该方法检索交易发生区域的当前税率。为此,业务逻辑类必须通过 Repository 与数据库对话:
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 和老式编码。
【讨论】:
【参考方案15】:您可以从 perl/python/ruby 的角度看到这一点:
当您将对象作为参数传递给方法时,您没有传递它的类型,您只知道它必须响应某些方法我认为将 java 接口作为类比最能解释这一点。你并没有真正传递一个类型,你只是传递一个响应方法的东西(如果你愿意的话,一个特征)。
【讨论】:
【参考方案16】:我认为在 Java 中使用接口的主要原因是对单继承的限制。在许多情况下,这会导致不必要的复杂性和代码重复。看看 Scala 中的 Traits:http://www.scala-lang.org/node/126 Traits 是一种特殊的抽象类,但一个类可以扩展其中的许多。
【讨论】:
以上是关于为啥大多数系统架构师都坚持首先对接口进行编程?的主要内容,如果未能解决你的问题,请参考以下文章