"面向接口编程"是什么意思?

39

可能是重复问题:
什么是“按接口编程”?

我一直遇到这个术语:

按接口编程。

它究竟是什么意思?非常感谢提供一个真实的设计场景。


1
又是一个重复的问题。我真的很喜欢这个问题,但它已经被发布了很多次......你试过使用搜索栏吗?在这里你可以得到很多建议:http://stackoverflow.com/search?q=programming+to+interface - manuel aldana
8个回答

83
简而言之,你不应该写出以下这样的类:

我依赖于这个特定的类来完成我的工作。

相反,你应该写出以下这样的类:

我依赖于做这件事情任何类来完成我的工作。

第一个例子表示一个依赖于具体实现来完成任务的类。本质上来说,这并不是很灵活。
第二个例子表示一个写成接口的类。它不关心你使用的具体对象是什么,只关心它实现了某些行为。这使得类更加灵活,因为可以提供任意数量的具体实现来完成工作。
例如,一个特定的类可能需要执行一些日志记录。如果你将这个类写成依赖于TextFileLogger的形式,那么这个类就必须永远写出其日志记录到文本文件中。如果你想改变日志记录的行为,你必须更改这个类本身。这个类与其日志记录器耦合度很高。
然而,如果你将这个类写成依赖于ILogger接口的形式,并提供一个TextFileLogger作为参数,那么你可以完成同样的事情,而且更加灵活。你可以随意提供任何其他类型的ILogger,而不必更改这个类本身。这个类和它的日志记录器现在是松散耦合的,你的类也更加灵活。

很遗憾,这不是一个好的答案。 "面向接口编程" 就是指这个意思。它并不意味着你不能依赖于特定的类来完成工作。作为对你的 ILogger 示例的反例,我提供了 java.util.logging.Loggerorg.apache.log4j.Logger 这两个非常流行的日志记录 API,它们都没有实现单独的接口,直接从客户端代码中使用。客户端代码仍然是“面向接口编程”,是 Logger 类的接口。 - Rogério
2
@Rogério 重点不在于特定的Java类是否提供了显式接口,而在于您编写的代码是否依赖于特定类而不是其公开的接口。如果您编写的代码特别依赖于org.apache.log4j.Logger,那么您就不是“面向接口编程”。如果您编写的代码不关心提供的记录器是java.util.logging.Loggerorg.apache.log4j.Logger还是任何其他公开通用接口的记录器实现,则您正在进行接口编程。 - Eric King
@EricKing 但是人们一直在编写与“Logger”类接口相对应的代码。当他们这样做时,他们正在编写接口;毫无疑问,他们没有编写类内任何实现。我认为你对“接口”的理解有些混淆了。像任何其他公共类一样,“java.util.logging.Logger”公共类具有隐式公共接口,但仍然是一个接口,您可以根据它进行编程。 - Rogério
@Rogério 不,我非常清楚接口的隐式和显式。我们都同意,如果你编写的代码只知道、关心和依赖于接口,那么我们就没问题了。“按照实现编程”并不意味着“依赖于类的封装内部”,这似乎是你在暗示的。它是依赖于“任何特定的类”(例如,创建一个需要“java.util.logging.Logger”实现的构造函数,这将无法与“org.apache.log4j.Logger”一起使用)来完成你的工作。 - Eric King
@EricKing 好的,我们同意接口是什么。关于“针对接口编程,而不是实现”(来自GoF),我的解释与“Effective Java”,第二版,条款52“按其接口引用对象”中所述一样。这涉及到你在本地变量、参数类型和返回类型中使用的类型;它并不禁止使用new实例化一个实现类,也不要求每个类都有一个单独的接口。引用原书的话:“如果不存在适当的接口,则完全可以通过类来引用对象。” - Rogério
显示剩余3条评论

20
一个接口是相关方法的集合,它仅包含这些方法的签名,而不包括实际的实现。
如果一个类实现了一个接口(class Car implements IDrivable),则它必须为接口中定义的所有签名提供代码。 基本示例:
你有两个类Car和Bike。两者都实现了接口IDrivable:
interface IDrivable 
{
    void accelerate();
    void brake();      
}

class Car implements IDrivable 
{
   void accelerate()
   { System.out.println("Vroom"); }

   void brake()
   { System.out.println("Queeeeek");}
}

class Bike implements IDrivable 
{
   void accelerate()
   { System.out.println("Rattle, Rattle, ..."); }

   void brake()
   { System.out.println("..."); }
}
现在假设您有一个对象集合,它们都是“可驾驶的”(它们的类都实现了IDrivable接口):
List<IDrivable> vehicleList = new ArrayList<IDrivable>();
list.add(new Car());
list.add(new Car());
list.add(new Bike());
list.add(new Car());
list.add(new Bike());
list.add(new Bike());

如果您现在想要循环遍历该集合,您可以依赖于这个事实:该集合中的每个对象都实现了 accelerate() 方法:

for(IDrivable vehicle: vehicleList) 
{
  vehicle.accelerate(); //this could be a bike or a car, or anything that implements IDrivable
}

通过调用该接口方法,您不是针对实现编程,而是针对接口编程-这是一种确保调用目标实现某些功能的契约。
使用继承可以实现相同的行为,但从共同的基类派生会导致紧耦合,使用接口可以避免这种情况。


2
这个答案深入我的脑海 :) - Setu Kumar Basak
接口本身的解释很好,但它并没有真正回答问题。 - Nyerguds

9

现实世界中有很多例子。其中之一:

对于JDBC,您正在使用接口 java.sql.Connection。然而,每个JDBC驱动程序都提供了其自己的Connection实现。您不必知道特定实现的任何信息,因为它符合Connection接口。

另一个例子来自Java集合框架。有一个java.util.Collection接口,它定义了sizeaddremove方法(以及许多其他方法)。因此,您可以交替使用各种类型的集合。假设您有以下内容:

public float calculateCoefficient(Collection collection) {
    return collection.size() * something / somethingElse;
}

而另外两个调用此方法的方法中,一个使用了LinkedList因为它对于其目的更有效率,而另一个使用了TreeSet
因为LinkedListTreeSet都实现了Collection接口,你只需使用一个方法执行系数计算,不需要重复编写代码。
这里来了"面向接口编程" - 你不需要关心size()方法如何实现,只需要知道它应该返回集合的大小 - 也就是说你已经编程到了Collection接口,而不是特定于LinkedListTreeSet
但我的建议是找一本书(例如 "Thinking in Java"),详细解释这个概念。

9
多态性取决于针对接口而非实现进行编程。
仅基于抽象类定义的接口操作对象具有两个好处:
1. 只要对象符合客户端期望的接口,客户端就无需了解使用的特定对象类型。 2. 客户端不需要了解实现这些对象的类。客户端只知道定义接口的抽象类(们)。
这极大地减少了子系统之间的实现依赖,从而导致了这个编程接口的原则。
请参见工厂方法模式以进一步理解这个设计。
来源:G.O.F.的“可重用面向对象软件元素的设计模式”。
还请参见工厂模式。何时使用工厂方法?

5

每个对象都有一个公开的接口。集合有AddRemoveAt等。套接字可能有SendReceiveClose等。

实际上您可以获取引用的每个对象都有这些接口的具体实现。

这两个事情是显而易见的,然而还有一件事情不太明显...

您的代码不应该依赖于对象的实现细节,而只是依赖于其公开的接口。

如果您采取极端措施,您只需编码使用 Collection<T> 等(而不是 ArrayList<T>)。更为实用的方法是,确保您可以随时替换概念上相同的内容而不会破坏您的代码。

Collection<T> 为例:您有一些东西的集合,实际上您正在使用 ArrayList<T> 因为为什么不呢。您应该确保您的代码在未来使用 LinkedList<T> 等时不会崩溃。


每个对象都有一个公开的接口吗?嗯,其实并不是这样的。你列出来的那些确实有,但很多对象并没有实现任何接口。 - Nyerguds

4
"编程到接口" 是指在编写代码时使用库和其他依赖项的代码。这些代码对您呈现的方式,方法名称、参数、返回值等构成了您需要编程的 接口。因此,它关注的是如何使用第三方代码。
它还意味着,只要接口保持不变,您就不必关心所依赖的代码的内部情况(当然,更或多或少安全)。
从技术上讲,有一些细节,例如 Java 中称为 "接口" 的语言概念。
如果您想了解更多信息,可以询问 "实现接口" 意味着什么...

3
这基本上意味着你只应该依赖于库的API(应用程序接口),而不应该基于库的具体实现来构建应用程序。
例如,假设你有一个给你一个“堆栈”的库。这个类给你了一些方法,比如push、pop、isempty和top。你应该仅仅依靠这些方法来编写你的应用程序。违反这个原则的一种方式是窥视内部,并发现堆栈是使用某种类型的数组实现的,因此如果你从一个空堆栈中弹出,你会得到某种类型的索引异常,然后捕获它而不是依赖于类提供的isempty方法。前一种方法将在库提供者从使用数组切换到使用某种列表时失败,而后一种方法仍将工作,假设提供者保持他的API仍然有效。

2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接