抽象类/方法过时了吗?


37

我曾经创建许多抽象类/方法。然后我开始使用接口。

现在,我不确定接口是否不会使抽象类过时。

您需要一个完全抽象的类吗?创建一个接口。您需要一个带有一些实现的抽象类吗?创建一个接口,创建一个类。继承该类,实现接口。另一个好处是某些类可能不需要父类,而只实现接口。

那么,抽象类/方法是否过时了?


如果您选择的编程语言不支持接口怎么办?我似乎记得C ++就是这种情况。
伯纳德

7
@Bernard:在C ++中,抽象类除名称之外的所有接口。他们还可以做比“纯”界面更多的事情,这并不是一个缺点。
gbjbaanb

@gbjbaanb:我想。我不记得使用它们作为接口,而是提供默认的实现。
伯纳德

接口是对象引用的“货币”。一般来说,它们是多态行为的基础。抽象类具有不同的目的,deadalnix可以完美地解释它。
jiggy

4
这不是在说“现在我们有车了吗?运输方式已经过时了吗?” 是的,大多数时候,您会开车。但是,无论您是否需要除汽车以外的任何东西,说“我不需要使用交通工具”都不是正确的。接口没有任何实现的抽象类非常相似,并且具有特殊名称,不是吗?
Jack V.

Answers:


111

没有。

接口不能提供默认实现,抽象类和方法可以。在许多情况下,这对于避免代码重复特别有用。

这也是减少顺序耦合的一种非常好的方法。没有抽象方法/类,就无法实现模板方法模式。我建议您看一下这篇维基百科文章:http : //en.wikipedia.org/wiki/Template_method_pattern


3
@deadalnix模板方法模式可能非常危险。您可以轻松地获得高度耦合的代码,并开始修改代码以扩展模板化抽象类以处理“仅一种情况”。
quant_dev

9
这似乎更像是一种反模式。您可以通过接口的组成获得完全相同的结果。同样的事情也适用于带有某些抽象方法的部分类。而不是强迫客户端代码继承和覆盖它们,您应该注入实现。参见:en.wikipedia.org/wiki/Composition_over_inheritance#Benefits
back2dos

4
这根本不能解释如何减少顺序耦合。我确实同意有几种方法可以实现此目标,但是请回答您正在谈论的问题,而不是盲目地引用某些原则。如果您无法解释为什么这与实际问题有关,那么您最终将进行“崇拜”编程。
deadalnix

3
@deadalnix:说使用模板模式可以最好地减少顺序耦合,这货物崇拜编程(使用状态/策略/工厂模式也可以工作),因为必须始终减少这种假设。顺序耦合通常仅是对某些事物进行非常精细的控制的结果,这仅是权衡取舍。那仍然不代表我不能编写没有使用组合顺序耦合的包装器。实际上,这使它变得容易得多。
back2dos

4
Java现在具有接口的默认实现。这会改变答案吗?
raptortech97 2014年

15

存在一个抽象方法,因此您可以从基类中调用它,但可以在派生类中实现它。因此,您的基类知道:

public void DoTask()
{
    doSetup();
    DoWork();
    doCleanup();
}

protected abstract void DoWork();

在派生类不了解设置和清除活动的情况下,这是在中间模式中实现漏洞的一种相当不错的方法。没有抽象方法,你必须依靠派生类中实现DoTask并记住调用base.DoSetup()base.DoCleanup()所有的时间。

编辑

另外,感谢deadalnix发布了到模板方法模式的链接,这是我在上面描述的,而实际上并未知道其名称。:)


2
+1,我更喜欢您的死神答案。请注意,您可以使用委托(在C#中)以更直接的方式实现模板方法:public void DoTask(Action doWork)
Joh

1
@Joh-是的,但这不一定一样好,具体取决于您的API。如果您的基类是Fruit,派生类是Apple,则您要调用myApple.Eat(),而不是myApple.Eat((a) => howToEatApple(a))。另外,您也不必Apple致电base.Eat(() => this.howToEatMe())。我认为仅重写抽象方法会更清洁。
Scott Whitlock

1
@Scott Whitlock:您使用苹果和水果的示例与实际情况有点过分脱离,无法判断继承通常是否比委托更可取。显然,答案是“视情况而定”,因此它为辩论留下了足够的空间……无论如何,我发现模板方法在重构期间会发生很多事情,例如在删除重复代码时。在这种情况下,我通常不想弄乱类型层次结构,而我宁愿远离继承。使用lambdas更容易进行这种手术。
荷兰Joh

这个问题不仅仅说明了模板方法模式的简单应用-公共/非公共方面在C ++社区中被称为非虚拟接口模式。通过具有公共非虚拟功能,可以包装虚拟功能-即使最初前者什么也不做,只能调用后者-您留下了定制点,以进行设置/清除,日志记录,性能分析,安全检查等,以后可能需要。
托尼

10

不,它们不是过时的。

实际上,抽象类/方法和接口之间存在模糊但根本的区别。

如果必须使用其中一个的一组类具有它们共享的共同行为(我是说相关的类),那么请使用Abstract类/方法。

示例:文员,干事,总监-所有这些类共有CalculateSalary()并使用抽象基类.CalculateSalary()可以以不同的方式实现,但某些其他类似GetAttendance()的示例在基类中具有通用定义。

如果您的类之间没有共同点(在选择的上下文中,不相关的类),但是在实现上有很大不同,那么请使用Interface。

示例:与牛,长凳,汽车,伸缩式无关的类,但是Isortable可以在那里对它们进行数组排序。

从多态性角度来看,通常会忽略这种差异。但我个人认为,由于上述原因,在某些情况下一个人比​​另一个人更合适。


6

除了其他好的答案之外,接口和抽象类之间还存在一个根本的差异,没有人特别提到过,即接口的可靠性远低于抽象类,因此带来了更大的测试负担。例如,考虑以下C#代码:

public abstract class Frobber
{
    private Frobber() {}
    public abstract void Frob(Frotz frotz);
    private class GreenFrobber : Frobber
    { ... }
    private class RedFrobber : Frobber
    { ... }
    public static Frobber GetFrobber(bool b) { ... } // return a green or red frobber
}

public sealed class Frotz
{
    public void Frobbit(Frobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

我保证只有两条代码路径需要测试。Frobbit的作者可以依靠Frobber是红色或绿色的事实。

如果相反,我们说:

public interface IFrobber
{
    void Frob(Frotz frotz);
}
public class GreenFrobber : IFrobber
{ ... }
public class RedFrobber : Frobber
{ ... }

public sealed class Frotz
{
    public void Frobbit(IFrobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

现在我知道绝对没有关于呼叫FROB存在的影响。我需要确保Frobbit中的所有代码对于IFrobber的任何可能实现都是健壮的,甚至包括那些不称职(不好)或对我或我的用户充满敌意(更糟糕)的人的实现。

抽象类使您可以避免所有这些问题。使用它们!


1
您所说的问题应该通过使用代数数据类型来解决,而不是将类弯曲到违反开放/闭合原则的程度。
back2dos

1
首先,我从来没有说过这是好事道德。你把它放在我的嘴里。其次(根据我的实际观点):这只是缺少语言功能的可怜借口。最后,有一个没有抽象类的解决方案:pastebin.com/DxEh8Qfz。与此相反,您的方法使用了嵌套类和抽象类,将需要安全性的所有代码(除了需要安全的代码)扔到了一起。没有充分的理由将RedFrobber或GreenFrobber绑定到您要强制执行的约束。它增加了耦合并锁定了许多决策,而没有任何好处。
back2dos

1
毫无疑问,说抽象方法过时是错误的,但另一方面,声称抽象方法比接口更可取却是错误的。它们只是解决接口以外的其他问题的工具。
Groo

2
“与抽象类相比,接口的根本区别远不及可靠”。我不同意,您在代码中说明的差异取决于访问限制,我认为没有任何理由在接口和抽象类之间有显着差异。在C#中可能是这样,但是问题与语言无关。
荷兰Joh

1
@ back2dos:我想指出的是,Eric的解决方案确实使用了代数数据类型:抽象类是求和类型。调用抽象方法与在一组变体上进行模式匹配相同。
罗德里克·查普曼

4

您自己说:

您需要一个带有一些实现的抽象类吗?创建一个接口,创建一个类。继承类,实现接口

与“继承抽象类”相比,这听起来需要做很多工作。您可以通过从“纯粹”的角度处理代码来自己完成工作,但是我发现我已经可以做很多事而无需尝试增加我的工作量,而没有任何实际好处。


更不用说如果该类不继承该接口(应该没有提到),则必须用某些语言编写转发功能。
Sjoerd

嗯,您提到的其他“大量工作”是您创建了一个接口并让类实现了它-看起来真的很繁重吗?当然,最重要的是,该接口使您能够从新的层次结构实现该接口,而该层次结构不适用于抽象基类。
Bill K

4

正如我在@deadnix帖子上评论的那样:部分实现是一种反模式,尽管事实上模板模式已将其正式化。

这个Wikipedia示例模板模式的干净解决方案:

interface Game {
    void initialize(int playersCount);
    void makePlay(int player);
    boolean done();
    void finished();
    void printWinner();
}
class GameRunner {
    public void playOneGame(int playersCount, Game game) {
        game.initialize(playersCount);
        int j = 0;
        for (int i = 0; !game.finished(); i++)
             game.makePlay(i % playersCount);
        game.printWinner();
    }
} 
class Monopoly implements Game {
     //... implementation
}

此解决方案更好,因为它使用composition而不是继承。模板模式在垄断规则的实现与游戏运行方式之间引入了依赖关系。但是,这是两个完全不同的职责,没有充分的理由将它们耦合在一起。


+1。附带说明:“部分实现是一种反模式,尽管模板模式已将其正式化。” 维基百科上的描述清楚地定义了模式,只有代码示例是“错误的”(在某种意义上,它在不需要时使用继承,并且存在一个更简单的替代方法,如上所示)。换句话说,我不认为应该指责模式本身,而只是人们倾向于实施它的方式。
荷兰Joh

2

不会。甚至您提出的替代方案都包括使用抽象类。另外,由于您没有指定语言,因此我将继续说通用代码比脆性继承更好。与接口相比,抽象类具有明显的优势。


-1。我不明白“通用代码与继承”与这个问题有什么关系。您应该说明或证明“抽象类相对于接口具有明显优势”的原因。
荷兰Joh

1

抽象类不是接口。它们是无法实例化的类。

您需要一个完全抽象的类吗?创建一个接口。您需要一个带有一些实现的抽象类吗?创建一个接口,创建一个类。继承该类,实现接口。另一个好处是某些类可能不需要父类,而只实现接口。

但是,那么您将获得一个非抽象的无用类。需要抽象方法来填充基类中的功能漏洞。

例如,给定此类

public abstract class Frobber {
    public abstract void Frob();

    public abstract boolean IsFrobbingNeeded { get; }

    public void FrobUntilFinished() {
        while (IsFrobbingNeeded) {
            Frob();
        }
    }
}

您如何在既Frob()没有类又没有类的类中实现此基本功能IsFrobbingNeeded


1
接口也不能实例化。
Sjoerd

@Sjoerd:但是您需要一个具有共享实现的基类。不能是接口。
配置器

1

我是Servlet框架的创建者,在该框架中抽象类起着至关重要的作用。我想说的更多,当一种方法在50%的情况下需要重写时,我需要半抽象方法,并且我希望看到编译器警告该方法没有被重写。我解决了添加注释的问题。回到您的问题,有两种不同的抽象类和接口用例,到目前为止,还没有人过时。


0

我认为接口不会让它们过时,但是策略模式可能会使它们过时。

抽象类的主要用途是推迟实现的一部分。这样说,“可以将类的实现的这一部分设置为不同的”。

不幸的是,一个抽象类迫使客户通过继承来做到这一点。策略模式将使您无需继承即可达到相同的结果。客户端可以创建类的实例,而不必总是定义自己的实例,并且类的“实现”(行为)可以动态变化。策略模式具有额外的优点,即行为不仅可以在设计时在运行时更改,而且在涉及的类型之间的耦合也很弱。


0

与ABC相比,与ABC相比,与纯接口相关的维护问题通常要多得多,甚至包括用于多重继承的ABC。YMMV-不知道,也许我们的团队没有充分利用它们。

就是说,如果我们使用现实世界的类比,那么完全没有功能和状态的纯接口有多少用途?如果以USB为例,那是一个相当稳定的接口(我认为我们现在使用的是USB 3.2,但它也保持了向后兼容性)。

但这不是一个无状态的接口。它并非没有功能。它更像是抽象基类,而不是纯接口。实际上,它更接近于具有非常特定的功能和状态要求的具体类,唯一的抽象是插入端口的内容是唯一可替换的部分。

否则,这将只是计算机中的“漏洞”,具有标准化的外形尺寸和更宽松的功能要求,直到每个制造商都想出自己的硬件来使该漏洞起作用为止,它自己不会做任何事情它变成了一个弱得多的标准,仅是一个“漏洞”和它应该做什么的规范,但没有关于如何做到的中央规定。同时,在所有硬件制造商尝试提出自己的方法来将功能和状态附加到该“漏洞”之后,我们可能最终会采用200种不同的方法来执行此操作。

到那时,我们可能会有某些制造商引入了与其他制造商不同的问题。如果我们需要更新规范,我们可能有200种不同的具体USB端口实现,而处理规范的方式完全不同,必须对其进行更新和测试。一些制造商可能会开发他们之间共享的事实上的标准实现(您的类比基类实现了该接口),但不是全部。有些版本可能比其他版本慢。有些可能具有更好的吞吐量,但延迟更糟,反之亦然。有些电池可能比其他电池消耗更多的电量。有些可能无法正常运行,并且不能与应该与USB端口一起使用的所有硬件一起使用。有些人可能需要安装一个核反应堆才能运行,这有可能使用户感到辐射中毒。

这就是我个人使用纯接口所发现的。在某些情况下,它们可能是有道理的,例如仅针对CPU机箱模拟主板的外形尺寸。实际上,与类似的“漏洞”一样,形状因数类比实际上几乎是无状态的并且没有功能。但是我经常认为团队认为在所有情况下都具有某种优势而不是接近优势是一个巨大的错误。

相反,我认为,如果是这两种选择,那么ABC会比接口更好地解决更多情况,除非您的团队如此庞大,以至于实际上需要类比超过200个竞争USB实现而不是一个中心标准,保持。在我曾任职的前团队中,我实际上不得不奋斗只是为了放松编码标准以允许ABC和多重继承,并且主要是为了应对上述这些维护问题。

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.