为什么接口在实现松散耦合方面比父类更有用?


15

出于这个问题的目的,当我说“接口”时,我的意思是语言结构interface,而不是用另一种意义上的“接口”,即,一个类提供了与外界进行交流的外部方法,并且操纵它。

松散耦合可以通过使对象依赖于抽象而不是具体类型来实现。

这允许松散耦合,主要有两个原因:1-与具体类型相比,抽象的更改可能性较小,这意味着从属代码中断的可能性较小。2-不同的具体类型可以在运行时使用,因为它们都适合抽象。以后也可以添加新的具体类型,而无需更改现有的从属代码。

例如,考虑一个类Car和两个子类VolvoMazda

如果您的代码依赖于Car,则可以在运行时使用VolvoMazda。同样,以后可以添加其他子类,而无需更改从属代码。

另外,Car-是一种抽象-,更改的可能性小于VolvoMazda。汽车在相当长的一段时间内大体相同,但沃尔沃和马自达的变化可能性更大。即抽象比具体类型更稳定。

所有这些都是为了表明我理解什么是松散耦合以及如何通过依赖抽象而不是依赖具体实现来实现松耦合。(如果我写的东西不准确,请这样说)。

我不明白的是:

抽象可以是超类或接口。

如果是这样,为什么接口因其允许松耦合而受到特别赞扬?我没有看到它与使用超类有什么不同。

我看到的唯一区别是:1-接口不受单一继承的限制,但这与松耦合这一主题无关。2-接口更加“抽象”,因为它们根本没有实现逻辑。但是,我仍然不明白为什么会有如此大的不同。

请向我解释为什么说接口在允许松散耦合方面是很棒的,而简单的超类却不是。


3
大多数具有“接口”的语言(例如Java,C#)仅支持单一继承。由于每个类只能有一个直接超类,因此(抽象)超类太有限,以至于一个对象不能支持多个抽象。查看特征(例如ScalaPerl的角色)以寻找现代替代方案,该替代方案还避免了具有多重继承的“ 钻石问题 ”。
阿蒙2014年

@amon因此,您要说的是,在尝试实现松散耦合时,接口相对于抽象类的优势是它们不受单一继承的限制吗?
阿维夫·科恩

不,我的意思昂贵来讲,编译器多,当处理一个抽象类做的,但是这可以或许忽略不计。
糊状

2
看起来@amon处在正确的轨道上,我在这篇文章中发现了这样的帖子:(interfaces are essential for single-inheritance languages like Java and C# because that's the only way in which you can aggregate different behaviors into a single class这使我可以与C ++进行比较,在C ++中,接口只是具有纯虚函数的类)。
Pasty

请告诉谁说超类是不好的。
TulainsCórdova'14

Answers:


11

术语:我将把语言构造interface称为interface,并将类型或对象的接口称为Surface(因为缺少更好的术语)。

松散耦合可以通过使对象依赖于抽象而不是具体类型来实现。

正确。

这允许松散耦合,其主要原因有两个:1-抽象比具体类型更不可能更改,这意味着从属代码不易被破坏。2-在运行时可以使用不同的具体类型,因为它们都适合抽象。以后也可以添加新的具体类型,而无需更改现有的从属代码。

不太正确。当前的语言通常不会期望抽象会改变(尽管有一些设计模式可以处理这种情况)。将细节与一般事物分开抽象。这通常是通过某些抽象层来完成的。可以将这一层更改为其他一些细节,而不会破坏基于此抽象的代码-实现了松散耦合。非OOP示例:sort例程可能从版本1中的Quicksort更改为版本2中的Tim Sort。因此,仅依赖于要排序的结果(即基于sort抽象的代码)与实际的排序实现脱钩。

我上面所说的表面是抽象的一般部分。现在在OOP中,一个对象有时必须支持多种抽象。一个不很理想的例子:Java java.util.LinkedList支持List关于“有序,可索引集合”抽象的Queue接口,并支持(大致而言)关于“ FIFO”抽象的接口。

一个对象如何支持多个抽象?

C ++没有接口,但是它具有多个继承,虚拟方法和抽象类。然后可以将抽象定义为声明但不定义虚拟方法的抽象类(即无法立即实例化的类)。然后,实现抽象细节的类可以从该抽象类继承并实现所需的虚拟方法。

这里的问题是多重继承会导致菱形问题,即在类中搜索方法实现的顺序(MRO:方法解析顺序)会导致“矛盾”。对此有两种回应:

  1. 定义一个合理的订单并拒绝那些无法合理线性化的订单。该C3 MRO是还算懂事和行之有效的。它于1996年出版。

  2. 采取简单的方法,并始终拒绝多重继承。

Java选择了后者,并选择了单一行为继承。但是,我们仍然需要对象支持多种抽象的能力。因此,必须使用不支持方法定义,仅支持声明的接口。

结果是MRO很明显(只需按顺序查看每个超类),并且我们的对象可以具有多个表面以进行任意数量的抽象。

事实证明,这是相当不令人满意的,因为很多行为是表面的一部分。考虑一个Comparable接口:

interface Comparable<T> {
    public int cmp(T that);
    public boolean lt(T that);  // less than
    public boolean le(T that);  // less than or equal
    public boolean eq(T that);  // equal
    public boolean ne(T that);  // not equal
    public boolean ge(T that);  // greater than or equal
    public boolean gt(T that);  // greater than
}

这是非常用户友好的(具有许多便捷方法的不错的API),但是实现起来很繁琐。我们希望接口仅包含cmp,并根据该所需方法自动实现其他方法。Mixins,但更重要的是,Traits [ 1 ],[ 2 ]解决了这个问题,而没有陷入多重继承的陷阱。

这是通过定义特征构成来完成的,从而使特征实际上不会最终参与MRO-而是将定义的方法组合到实现类中。

Comparable接口可以在Scala中表示为

trait Comparable[T] {
    def cmp(that: T): Int
    def lt(that: T): Boolean = this.cmp(that) <  0
    def le(that: T): Boolean = this.cmp(that) <= 0
    ...
}

当一个类使用该特性时,其他方法将添加到该类定义中:

// "extends" isn't different from Java's "implements" in this case
case class Inty(val x: Int) extends Comparable[Inty] {
    override def cmp(that: Inty) = this.x - that.x
    // lt etc. get added automatically
}

所以Inty(4) cmp Inty(6)-2Inty(4) lt Inty(6)true

许多语言都支持特征,任何具有“超对象协议(MOP)”的语言都可以添加特征。最近的Java 8更新添加了类似于trait的默认方法(接口中的方法可以具有后备实现,因此对于实现类来实现这些方法是可选的)。

不幸的是,特征是一个相当新的发明(2002年),因此在较大的主流语言中很少见。


好的答案,但是我要补充一点,单继承语言可以使用具有组合的接口软化多重继承。

4

我不明白的是:

抽象可以是超类或接口。

如果是这样,为什么接口因其允许松耦合而受到特别赞扬?我没有看到它与使用超类有什么不同。

首先,子类型化和抽象是两件事。子类型化仅意味着我可以用一种类型的值替换另一种类型的值-两种类型都不需要抽象。

更重要的是,子类直接依赖于其超类的实现细节。那是最强的耦合。实际上,如果基类的设计没有考虑继承性,则不更改其行为的基类更改仍然会破坏子类,并且无法事先知道是否会发生破坏。这被称为脆弱的基类问题

除了接口本身(不包含任何行为)之外,实现接口不会使您与其他任何事物耦合。


谢谢回答。看一下我是否理解:当您希望一个名为A的对象依赖于一个名为B的抽象,而不是该名为C的抽象的具体实现时,通常更好的做法是让B成为由C实现的接口,而不是由C扩展的超类。 C.这是因为:C的B子类将C与B紧密耦合。如果B改变-C改变。但是,实现B的C(B是接口)不会将B耦合到C:B只是C必须实现的方法列表,因此没有紧密耦合。但是,对于对象A(从属对象)而言,B是类还是接口都没有关系。
Aviv Cohn 2014年

正确?.........
Aviv Cohn 2014年

您为什么会考虑将接口耦合到任何东西?
Michael Shaw

我认为这个答案很重要。我使用C ++相当多,正如其他答案之一所述,C ++并没有接口,但是您可以通过使用超类来伪造它,而所有方法都保留为“纯虚拟”(即由孩子实现)。关键是,使基类可以做一些与委托功能一起工作的对象很容易。在很多很多情况下,我和我的同事发现这样做会带来一个新的用例并使共享的功能失效。如果需要共享功能,则可以很容易地创建一个帮助程序类。
J特拉纳2014年

@Prog您的思路在大多数情况下是正确的,但同样,抽象和子类型化是两回事。当您说时,you want an object named A to depend on an abstraction named B instead of a concrete implementation of that abstraction named C您假设类在某种程度上不是抽象的。抽象是任何隐藏实现细节的东西,因此具有私有字段的类与具有相同公共方法的接口一样抽象。
Doval 2014年

1

父类和子类之间存在耦合,因为子类依赖于父类。

假设我们有一个类A,而类B继承自它。如果我们进入A类并进行更改,B类也将发生变化。

假设我们有一个接口I,并且类B实现了它。如果我们更改接口I,则尽管类B可能不再实现它,但类B保持不变。


我很好奇那些拒绝投票的人是有原因的,还是日子不好过。
Michael Shaw

1
我没有投票,但我认为可能与第一句话有关。子类与父类耦合,而不是相反。父母不需要了解有关孩子的任何信息,但是孩子需要了解有关父母的知识。

@JohnGaughan:感谢您的反馈。为清楚起见进行了编辑。
Michael Shaw
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.