关于java:编程到接口是什么意思?

What does it mean to program to a interface?

我一直在听大多数与编程相关的网站上的声明:

Program to an interface and not to an
Implementation

但是我不明白这意味着什么?例子会有帮助。

编辑:我收到了很多很好的答案,所以你可以补充一些代码片段来更好地理解这个主题。谢谢!


你可能在找这样的东西:

1
2
3
4
5
6
7
public static void main(String... args) {
  // do this - declare the variable to be of type Set, which is an interface
  Set buddies = new HashSet();

  // don't do this - you declare the variable to have a fixed type
  HashSet buddies2 = new HashSet();
}

为什么第一种方法被认为是好的?稍后我们假设您决定使用不同的数据结构,例如LinkedHashSet,以便利用LinkedHashSet的功能。必须按如下方式更改代码:

1
2
3
4
5
6
7
8
9
public static void main(String... args) {
  // do this - declare the variable to be of type Set, which is an interface
  Set buddies = new LinkedHashSet();  // <- change the constructor call

  // don't do this - you declare the variable to have a fixed type
  // this you have to change both the variable type and the constructor call
  // HashSet buddies2 = new HashSet();  // old version
  LinkedHashSet buddies2 = new LinkedHashSet();
 }

这看起来没那么糟,对吧?但是如果你用同样的方式写getters呢?

1
2
3
public HashSet getBuddies() {
  return buddies;
}

这也必须改变!

1
2
3
public LinkedHashSet getBuddies() {
  return buddies;
}

希望您能看到,即使有这样一个小程序,您在声明变量类型时也会有深远的影响。如果你仅仅依赖一个被声明为接口的变量,而不是该接口的一个特定实现(在本例中,声明它是一个集合,而不是LinkedHashSet或其他任何东西),那么对象来回移动无疑有助于使程序更容易编码和维护。可能就是这样:

1
2
3
public Set getBuddies() {
  return buddies;
}

还有另一个好处,那就是(至少对我来说)差异有助于我更好地设计一个程序。但希望我的例子能给你一些想法…希望它有帮助。


一天,一个初级程序员被他的老板指示编写一个应用程序来分析业务数据,并用度量、图表和所有这些东西将其浓缩成漂亮的报告。老板给了他一个XML文件,上面写着"这是一些示例业务数据"。

程序员开始编码。几周后,他觉得这些指标、图表和东西都足以让老板满意,于是他介绍了自己的工作。""那太好了,"老板说,"但它还能显示我们拥有的这个SQL数据库中的业务数据吗?".

程序员又开始编码了。从XML中读取业务数据的代码散布在他的应用程序中。他重写了所有这些片段,用"if"条件将它们包装起来:

1
2
3
4
5
6
7
8
if (dataType =="XML")
{
   ... read a piece of XML data ...
}
else
{
   .. query something from the SQL database ...
}

当新的软件迭代出现时,老板回答说:"这很好,但是它也能报告来自这个Web服务的业务数据吗?"记住那些冗长乏味的语句,如果他不得不再次重写的话,程序员就会大发雷霆。"首先是XML,然后是SQL,现在是Web服务!业务数据的真正来源是什么?"

老板回答说:"任何能提供的东西。"

在那一刻,程序员得到了启发。


我对那句话的最初理解与我读过的任何答案都大不相同。我同意所有人所说的对方法参数等使用接口类型是非常重要的,但这不是这个语句对我的意义。

我的看法是,它告诉您编写的代码只取决于您使用的接口(在本例中,我使用"interface"是指类或接口类型的公开方法)在文档中说明了它的作用。这与编写依赖于所调用函数的实现细节的代码相反。您应该将所有函数调用视为黑盒(如果这两个函数都是同一类的方法,则可以对此进行例外处理,但理想情况下,可以一直对其进行维护)。

示例:假设有一个Screen类上有Draw(image)Clear()方法。文档中说"draw方法在屏幕上绘制指定的图像"和"clear方法清除屏幕"。如果要按顺序显示图像,正确的方法是反复调用Clear(),然后再调用Draw()。这将是对接口的编码。如果要对实现进行编码,您可能只需要调用Draw()方法,因为您从Draw()的实现中了解到,在进行任何绘图之前,它在内部调用Clear()。这是不好的,因为您现在依赖于从查看公开的接口中无法知道的实现细节。

我期待着看到是否有其他人分享这个短语在OP的问题上的解释,或者如果我完全偏离了基础…


这是一种在模块之间分离职责/依赖关系的方法。通过定义一个特定的接口(API),您可以确保接口两侧的模块不会相互"干扰"。

例如,假设模块1将负责显示特定用户的银行帐户信息,模块2将从使用的"任意"后端获取银行帐户信息。

通过定义一些类型和函数,以及相关的参数,例如定义银行事务的结构,以及一些方法(函数),如getlasttransactions(accountnumber、nbtransactionswanted、arrayToReturntSecrec)和getBalance(accountNumer1),模块1将能够获取所需的信息,而不必担心ho这个信息是存储或计算的或是其他的。相反,模块2只需根据定义的接口提供信息来响应方法调用,但不必担心在何处显示、打印或其他…

当一个模块被更改时,接口的实现可能会有所不同,但只要接口保持不变,使用API的模块最坏情况下可能需要重新编译/重建,但它们无论如何都不需要修改它们的逻辑。

这就是API的概念。


接口定义了一个对象被提交到响应的方法。

当您对接口进行编码时,您可以更改基础对象,并且您的代码仍然可以工作(因为您的代码不确定谁执行了该作业或该作业是如何执行的),这样您就获得了灵活性。

当您对特定的实现进行编码时,如果需要更改基础对象,您的代码很可能会中断,因为新对象可能不会响应相同的方法。

因此,举一个明确的例子:

如果您需要保存一些对象,您可能已经决定使用向量。

如果需要访问向量的第一个对象,可以编写:

1
2
3
 Vector items = new Vector();
 // fill it
 Object first = items.firstElement();

到现在为止,一直都还不错。

稍后您决定,因为"某些"原因,您需要更改实现(假设向量由于过度同步而造成瓶颈)

你意识到你需要使用一个数组列表。

好吧,你的代码会破坏…

1
2
3
ArrayList items = new ArrayList();
// fill it  
Object first = items.firstElement(); // compile time error.

不能。此行和所有使用firstElement()方法的行都将中断。

如果您需要特定的行为并且您确实需要这个方法,那么它可能是可以的(尽管您不能更改实现),但是如果您需要的只是简单地检索第一个元素(也就是说,除了向量具有first element()方法外,向量没有任何特殊之处),那么使用接口而不是impl实现会给你改变的灵活性。

1
2
3
 List items = new Vector();
 // fill it
 Object first = items.get( 0 ); //

在这种形式中,您不是按照矢量的get方法进行编码,而是按照list的get方法进行编码。

只要底层对象响应"get the 0th element of the collection"的约定,它就不管基础对象如何执行该方法。

这样,您以后可以将其更改为任何其他实现:

1
2
3
 List items = new ArrayList(); // Or LinkedList or any other who implements List
 // fill it
 Object first = items.get( 0 ); // Doesn't break

这个示例看起来可能很幼稚,但它是OO技术的基础(甚至是那些非静态类型的语言,如python、ruby、smalltalk、objective-c等)。

更复杂的例子是JDBC的工作方式。你可以换司机,但你的大部分电话都是一样的。例如,您可以使用Oracle数据库的标准驱动程序,也可以使用WebLogic或WebPShere提供的更复杂的驱动程序。当然,这并不神奇,你仍然需要测试你的产品,但至少你没有这样的东西:

1
 statement.executeOracle9iSomething();

VS

1
statement.executeOracle11gSomething();

类似于JavaSwing的情况。

附加阅读:

设计模式的设计原则

有效的Java项目:引用对象的接口

(买这本书是你一生中能做的最好的事情之一——当然,如果是这样的话,那就读吧!)


在其核心,这个语句实际上是关于依赖性的。如果我将类Foo编码为一个实现(Bar而不是IBar,那么Foo现在依赖于Bar。但是,如果我将类Foo编码到接口(IBar而不是Bar中),那么实现可能会有所不同,Foo不再依赖于特定的实现。这种方法提供了一个灵活的、松散耦合的代码库,更容易重用、重构和单元测试。


听着,我没意识到这是针对Java的,我的代码是基于C++的,但是我相信它提供了这一点。

每辆车都有门。

但并非每扇门的动作都一样,就像英国的出租车门是向后的。一个普遍的事实是它们"打开"和"关闭"。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
interface IDoor
{
    void Open();
    void Close();
}

class BackwardDoor : IDoor
{
    public void Open()
    {
       // code to make the door open the"wrong way".
    }

    public void Close()
    {
       // code to make the door close properly.
    }
}

class RegularDoor : IDoor
{
    public void Open()
    {
        // code to make the door open the"proper way"
    }

    public void Close()
    {
        // code to make the door close properly.
    }
}

class RedUkTaxiDoor : BackwardDoor
{
    public Color Color
    {
        get
        {
            return Color.Red;
        }
    }
}

如果你是一个车门修理工,你不在乎车门的外观,也不在乎它是以一种方式还是以另一种方式打开的。你唯一的要求是门的作用就像门一样,比如我。

1
2
3
4
5
6
7
8
9
class DoorRepairer
{
    public void Repair(IDoor door)
    {
        door.Open();
        // Do stuff inside the car.
        door.Close();
    }
}

修理厂可以处理减速轴门、正门和后门。以及任何其他类型的门,如卡车门、豪华轿车门。

1
2
3
4
5
DoorRepairer repairer = new DoorRepairer();

repairer.Repair( new RegularDoor() );
repairer.Repair( new BackwardDoor() );
repairer.Repair( new RedUkTaxiDoor() );

将此应用于列表,您有LinkedList、Stack、Queue、Normal列表,如果需要自己的列表,还可以使用MyList。它们都实现了IList接口,这要求它们实现添加和删除。因此,如果您的类在任何给定列表中添加或删除项…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ListAdder
{
    public void PopulateWithSomething(IList list)
    {
         list.Add("one");
         list.Add("two");
    }
}

Stack stack = new Stack();
Queue queue = new Queue();

ListAdder la = new ListAdder()
la.PopulateWithSomething(stack);
la.PopulateWithSomething(queue);


拿一个红色的2X4乐高积木,把它连接到蓝色的2X4乐高积木上,这样一个积木就位于另一个积木的顶部。现在取下蓝色块,换上黄色的2x4乐高积木。请注意,即使附加块的"实现"有所不同,红色块也不必更改。

现在去得到一些其他类型的块,不共享乐高"接口"。试着把它装到红色的2X4乐高上。要做到这一点,你需要改变乐高或其他块,也许通过削减一些塑料或添加新的塑料或胶水。请注意,通过更改"实现",您必须更改它或客户机。

能够让实现在不改变客户机或服务器的情况下发生变化——这就是编程到接口的含义。


一个接口就像是你和创建接口的人之间的一个契约,你的代码将执行他们的请求。此外,您希望以这样的方式编写代码,您的解决方案可以多次解决问题。思考代码重用。当您在对一个实现进行编码时,您纯粹是在考虑您正试图解决的问题的实例。因此,在这种影响下,您的解决方案将不那么通用,而更加集中。这将使编写一个遵守界面的通用解决方案变得更具挑战性。


除其他答案外,我还补充了:

你编程到一个接口,因为它更容易处理。接口封装了基础类的行为。这样,这个班就是一个黑板。你的整个现实生活都在编程接口。当您使用电视、汽车、立体声时,您是在它的接口上操作,而不是在它的实现细节上操作,并且您假设如果实现发生变化(例如柴油机或汽油机),则接口保持不变。编程到接口允许您在更改、优化或修复无中断细节时保留行为。这也简化了记录、学习和使用的任务。

此外,编程到接口允许您在编写代码之前描述代码的行为。你希望一个班能做些什么。您甚至可以在编写执行此操作的实际代码之前对其进行测试。当你的接口是干净的并且完成了,并且你喜欢与它交互时,你可以编写实际的代码来完成事情。


AllenHolub在2003年为JavaWorld写了一篇很棒的文章,题为"为什么扩展是邪恶的"。从他的标题中可以看出,他对"Program to the Interface"语句的看法是,您应该愉快地实现接口,但很少使用extends关键字进行子类。他指出,最重要的是,所谓的脆弱的基础阶级问题。维基百科:

a fundamental architectural problem of object-oriented programming systems where base classes (superclasses) are considered"fragile" because seemingly safe modifications to a base class, when inherited by the derived classes, may cause the derived classes to malfunction. The programmer cannot determine whether a base class change is safe simply by examining in isolation the methods of the base class.


"程序到接口"可以更灵活。

例如,我们正在编写一个提供打印服务的类打印机。目前有2个类(CatDog需要打印。所以我们写的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
class Printer
{
    public void PrintCat(Cat cat)
    {
        ...
    }

    public void PrintDog(Dog dog)
    {
        ...
    }
    ...
}

如果有一个新的类Bird也需要这个打印服务呢?我们必须更改Printer类以添加新的方法printbird()。在实际情况下,当我们开发打印机类时,我们可能不知道谁将使用它。那么怎么写Printer?程序到接口可以帮助,请参见下面的代码

1
2
3
4
5
6
7
8
9
class Printer
{
    public void Print(Printable p)
    {
        Bitmap bitmap = p.GetBitmap();

        // print bitmap ...
    }
}

有了这台新打印机,只要它实现接口Printable,就可以打印所有内容。这里的getBitmap()方法只是一个例子。关键是要公开一个接口,而不是一个实现。

希望有帮助。


通过编程到接口,您更可能应用低耦合/高内聚性原则。通过编程到接口,您可以轻松地切换该接口(特定类)的实现。


从本质上讲,接口是互操作的一般概念的更具体的表示形式——它们提供了规范,说明了您可能希望为特定函数"插件"的所有不同选项都应执行类似的操作,以便使用它们的代码不会依赖于一个特定选项。

例如,许多数据库库作为接口,它们可以与许多不同的实际数据库(mssql、mysql、postgresql、sqlite等)一起操作,而不必更改使用数据库库的代码。

总的来说,它允许您创建更灵活的代码——为您的客户提供更多关于他们如何使用它的选项,还可能允许您更容易地在多个地方重用代码,而不必编写新的专门代码。


这意味着变量、属性、参数和返回类型应该具有接口类型,而不是具体的实现。

这意味着你使用IEnumerable Foo(IList mylist)而不是ArrayList Foo(ArrayList myList)

仅在构造对象时使用实现:

1
IList list = new ArrayList();

如果您这样做了,您可以稍后更改对象类型,也许您希望在以后使用LinkedList而不是ArrayList,这没问题,因为您在其他任何地方都将其称为"ilist"


基本上就是在这里创建这样的方法/接口:create( 'apple' ),其中方法create(param)来自抽象类/接口fruit,后者随后由具体类实现。这与子类化不同。您正在创建一个类必须满足的契约。这也减少了耦合,并使每一个具体类在不同的地方实现它时更加灵活。

客户机代码仍然不知道使用的对象的具体类型,也不知道实现这些对象的类。客户机代码只知道接口create(param),它使用它来生成水果对象。就像是说,"我不管你是怎么得到它的,还是让它成为我的,只是想让你把它给我。"

与此类似的是一组开关按钮。这是一个接口on()off()。您可以在多个设备上使用这些按钮,如电视、收音机、灯。他们的处理方式各不相同,但我们不在乎,我们只关心打开或关闭它。


基于接口的编程在运行时提供了与特定对象的强耦合。由于Java对象变量是多态的,对象引用到超类可以引用任何子类的对象。用父类型声明的对象可以用属于父类型任何特定实现的对象进行分配。

注意,作为接口,可以使用抽象类。

enter image description here

基于实现的编程:

1
2
Motorcycle motorcycle = new Motorcycle();
motorcycle.driveMoto();

基于接口的编程:

1
2
3
Vehicle vehicle;
vehicle = new Motorcycle(); // Note that DI -framework can do it for you
vehicle.drive();