我读过的几乎每一本 Java 书籍都谈到了使用接口作为在对象之间共享状态和行为的一种方式,这些对象在第一次“构造”时似乎并不共享关系。
然而,每当我看到架构师设计应用程序时,他们做的第一件事就是开始对接口进行编程。怎么会?您如何知道将在该接口中发生的对象之间的所有关系?如果您已经知道这些关系,那么为什么不扩展一个抽象类呢?
我读过的几乎每一本 Java 书籍都谈到了使用接口作为在对象之间共享状态和行为的一种方式,这些对象在第一次“构造”时似乎并不共享关系。
然而,每当我看到架构师设计应用程序时,他们做的第一件事就是开始对接口进行编程。怎么会?您如何知道将在该接口中发生的对象之间的所有关系?如果您已经知道这些关系,那么为什么不扩展一个抽象类呢?
对接口进行编程意味着尊重使用该接口创建的“合同”。因此,如果您的IPoweredByMotor
接口有一个start()
方法,那么未来实现该接口的类,无论是MotorizedWheelChair
,Automobile
还是SmoothieMaker
,在实现该接口的方法时,都会为您的系统增加灵活性,因为一段代码可以启动许多不同类型的电机事情,因为一段代码只需要知道它们对start()
. 他们如何开始并不重要,只是他们必须开始。
好问题。我将把你推荐给Effective Java 中的 Josh Bloch,他写了(第 16 条)为什么更喜欢使用接口而不是抽象类。顺便说一句,如果你还没有这本书,我强烈推荐它!以下是他所说的摘要:
抽象类提供基本实现的优势是什么?您可以为每个接口提供一个抽象的骨架实现类。这结合了接口和抽象类的优点。骨架实现提供了实现帮助,而不会强加抽象类在用作类型定义时所强制的严格约束。例如,集合框架使用接口定义类型,并为每个接口提供一个框架实现。
对接口进行编程有几个好处:
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。
模拟实现、单元测试——当依赖类是接口的实现时,可以编写模拟类来实现这些接口。模拟类可用于促进单元测试。
我认为抽象类在很大程度上被开发人员抛弃的原因之一可能是一种误解。
当四人组写道:
编程到接口而不是实现。
没有 java 或 C# 接口之类的东西。他们谈论的是每个类都有的面向对象的接口概念。Erich Gamma 在这次采访中提到了这一点。
我认为不加思索地机械地遵循所有规则和原则会导致难以阅读、导航、理解和维护代码库。记住:最简单的事情可能会奏效。
怎么会?
因为所有的书都是这么说的。就像 GoF 模式一样,许多人认为它普遍适用,并且从不考虑它是否真的是正确的设计。
您如何知道将在该接口中发生的对象之间的所有关系?
你没有,这是个问题。
如果您已经知道这些关系,那么为什么不扩展一个抽象类呢?
不扩展抽象类的原因:
如果两者都不适用,请继续使用抽象类。它将为您节省大量时间。
你没有问的问题:
使用接口的缺点是什么?
你不能改变它们。与抽象类不同,接口是一成不变的。一旦你使用了一个,扩展它会破坏代码,句号。
我真的需要吗?
大多数时候,没有。在构建任何对象层次结构之前,请认真思考。像 Java 这样的语言的一个大问题是,它使创建大量复杂的对象层次结构变得太容易了。
考虑 LameDuck 继承自 Duck 的经典示例。听起来很容易,不是吗?
好吧,直到你需要表明鸭子已经受伤并且现在是跛脚的。或表示瘸腿鸭已经痊愈,可以再次行走。Java 不允许您更改对象类型,因此使用子类型来指示跛行实际上不起作用。
对接口进行编程意味着尊重使用该接口创建的“合同”
这是关于接口的最容易被误解的事情。
没有办法通过接口强制执行任何此类合同。根据定义,接口根本不能指定任何行为。类是行为发生的地方。
这种错误的信念是如此普遍,以至于被许多人认为是传统智慧。然而,这是错误的。
所以OP中的这个声明
我读过的几乎每一本 Java 书籍都谈到了使用接口作为在对象之间共享状态和行为的一种方式
这是不可能的。接口既没有状态也没有行为。他们可以定义实现类必须提供的属性,但这是尽可能接近的。您不能使用接口共享行为。
您可以假设人们将实现一个接口以提供其方法名称所暗示的那种行为,但这不是一回事。并且它对何时调用此类方法(例如,应在 Stop 之前调用 Start)没有任何限制。
这个说法
GoF 类型模式需要,例如访问者模式
也是不正确的。GoF 书完全使用零接口,因为它们不是当时使用的语言的特性。没有一种模式需要接口,尽管有些模式可以使用它们。IMO,观察者模式是一种接口可以扮演更优雅角色的模式(尽管现在该模式通常使用事件来实现)。在访问者模式中,几乎总是需要一个基本访问者类来为每种类型的访问节点实现默认行为,IME。
就个人而言,我认为这个问题的答案是三方面的:
接口被许多人视为灵丹妙药(这些人通常在“合同”误解下工作,或者认为接口神奇地解耦了他们的代码)
Java 人非常专注于使用框架,其中许多(正确地)需要类来实现它们的接口
在引入泛型和注释(C# 中的属性)之前,接口是做某些事情的最佳方式。
接口是一个非常有用的语言特性,但被滥用了很多。症状包括:
一个接口只能由一个类实现
一个类实现多个接口。通常被吹捧为接口的优势,通常意味着所讨论的类违反了关注点分离的原则。
接口有一个继承层次结构(通常由类层次结构反映)。这是您首先要通过使用接口来避免的情况。对于类和接口来说,过多的继承是一件坏事。
所有这些都是代码气味,IMO。
在我看来,您经常会看到这种情况,因为这是一种非常好的做法,但经常在错误的情况下应用。
相对于抽象类,接口有很多优点:
在处理代码模块时,您可以从接口中获得最大的优势。但是,没有简单的规则来确定模块边界应该在哪里。所以这个最佳实践很容易被过度使用,尤其是在第一次设计一些软件的时候。
我会假设(使用@eed3s9n)这是为了促进松散耦合。此外,没有接口,单元测试变得更加困难,因为您无法模拟您的对象。
为什么延伸是邪恶的。这篇文章几乎是对所提问题的直接回答。我几乎想不出你真的需要一个抽象类的情况,以及很多情况下这是一个坏主意。这并不意味着使用抽象类的实现不好,但您必须小心,不要使接口契约依赖于某些特定实现的工件(例如:Java 中的 Stack 类)。
还有一件事:没有必要,也没有好的做法,到处都有接口。通常,您应该确定何时需要接口,何时不需要。在理想情况下,大多数情况下第二种情况应该作为最终类来实现。
这里有一些很好的答案,但如果你正在寻找一个具体的原因,那就看看单元测试吧。
假设您想在业务逻辑中测试一个方法,该方法检索交易发生区域的当前税率。为此,业务逻辑类必须通过 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 和老式编码。
从某种意义上说,我认为您的问题可以归结为“为什么要使用接口而不是抽象类?” 从技术上讲,你可以实现两者的松耦合——底层实现仍然没有暴露给调用代码,你可以使用抽象工厂模式返回一个底层实现(接口实现与抽象类扩展)来增加你的灵活性设计。事实上,您可能会争辩说抽象类给了您更多,因为它们允许您既需要实现来满足您的代码(“您必须实现 start()”)并提供默认实现(“我有一个标准的 paint()如果你愿意,可以覆盖”)——对于接口,必须提供实现,随着时间的推移,这可能会通过接口更改导致脆弱的继承问题。
不过,从根本上说,我使用接口主要是因为 Java 的单一继承限制。如果我的实现必须从一个抽象类继承以供调用代码使用,这意味着我失去了从其他东西继承的灵活性,即使这可能更有意义(例如,对于代码重用或对象层次结构)。
原因之一是接口允许增长和可扩展性。例如,假设您有一个将对象作为参数的方法,
公共无效饮料(咖啡 someDrink){
}
现在假设您想使用完全相同的方法,但传递一个 hotTea 对象。好吧,你不能。您只是将上述方法硬编码为仅使用咖啡对象。也许那是好的,也许那是坏的。上面的缺点是,当您想传递各种相关对象时,它会严格地将您锁定在一种类型的对象中。
通过使用接口,比如 IHotDrink,
接口 IHotDrink { }
并重写您的上述方法以使用接口而不是对象,
公共无效饮料(IHotDrink someDrink){
}
现在您可以传递所有实现 IHotDrink 接口的对象。当然,您可以编写完全相同的方法,使用不同的对象参数执行完全相同的操作,但为什么呢?你突然维护臃肿的代码。
这都是关于在编码之前进行设计的。
如果您在指定接口后不知道两个对象之间的所有关系,那么您在定义接口方面做得很差——这相对容易修复。
如果您直接潜入编码并意识到在中途您错过了一些更难以修复的东西。
你可以从 perl/python/ruby 的角度看到这一点:
我认为将 java 接口作为类比将最好地解释这一点。你并没有真正传递一个类型,你只是传递一个响应方法的东西(如果你愿意的话,一个特征)。
我认为在 Java 中使用接口的主要原因是对单继承的限制。在许多情况下,这会导致不必要的复杂性和代码重复。看看 Scala 中的 Traits:http ://www.scala-lang.org/node/126 Traits 是一种特殊的抽象类,但一个类可以扩展其中的许多。