接口和继承:两全其美?


10

我“发现”了界面,并开始喜欢它们。接口的优点在于它是一个契约,任何需要履行该契约的对象都可以在需要该接口的任何地方使用。

接口的问题是它不能具有默认实现,这对于平凡的属性是一种痛苦,并且会破坏DRY。这也很好,因为它可以使实现与系统保持分离。一方面,继承会保持更紧密的耦合,并且有可能破坏封装。

案例1(与私有成员的继承,良好的封装,紧密耦合)

class Employee
{
int money_earned;
string name;

public:
 void do_work(){money_earned++;};
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work. Oops, can't update money_earned. Unaware I have to call superclass' do_work()*/);

};

void HireNurse(Nurse *n)
{
   nurse->do_work();
)

情况2(只是一个接口)

class IEmployee
{
     virtual void do_work()=0;
     virtual string get_name()=0;
};

//class Nurse implements IEmployee.
//But now, for each employee, must repeat the get_name() implementation,
//and add a name member string, which breaks DRY.

情况3 :(两全其美?)

案例1相似。但是,想象一下(假设),C ++不允许重写方法,只是那些纯virtual方法。

因此,在案例1中,重写do_work()会导致编译时错误。为了解决这个问题,我们将do_work()设置为纯虚拟的,并添加一个单独的方法增量_money_earned()。举个例子:

class Employee
{
int money_earned;
string name;

public:
 virtual void do_work()=0;
 void increment_money_earned(money_earned++;);
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work*/ increment_money_earned(); ); .
};

但这甚至有问题。如果从现在开始的3个月后,Joe Coder创建了一个Doctor Employee,但他忘记在do_work()中调用增量_money_earned(),该怎么办?


问题:

  • 案例3是否优于案例1?是因为“更好的封装”或“更松散地耦合”,还是其他原因?

  • 案例3是否符合DRY,是否优于案例2


2
...您是在重塑抽象类还是什么?
ZJR 2012年

Answers:


10

解决“忘记调用超类”问题的一种方法是将控件交还给超类!我重新摆弄了您的第一个示例,以展示如何操作(并使其进行编译;)。哦,我还假设do_work()in Employeevirtual您的第一个示例。

#include <string>

using namespace std;

class Employee
{
    int money_earned;
    string name;
    virtual void on_do_work() {}

    public:
        void do_work() { money_earned++; on_do_work(); }
        string get_name() { return name; }
};

class Nurse : public Employee
{
    void on_do_work() { /* do more work. Oh, and I don't have to call do_work()! */ }
};

void HireNurse(Nurse* nurse)
{
    nurse->do_work();
}

现在do_work()不能被覆盖。如果要扩展它,则必须通过on_do_work()do_work()进行控制。

当然,如果Employee扩展它,它也可以与第二个示例中的接口一起使用。因此,如果我对您的理解正确,我认为这是案例3,但不必使用假设的C ++!它是干的,并且具有很强的封装性。


3
这就是称为“模板方法”的设计模式(en.wikipedia.org/wiki/Template_method_pattern)。
Joris Timmermans '02

是的,这符合案例3。这看起来很有希望。将详细检查。另外,这是某种事件系统。这个“模式”有名称吗?
MustafaM 2012年

@MadKeithV您确定这是“模板方法”吗?
MustafaM 2012年

@illmath-是的,它是一个非虚拟的公共方法,它将部分实现细节委托给虚拟的受保护/私有方法。
Joris Timmermans'2

@illmath我以前没有将其视为模板方法,但我认为它是其中的一个基本示例。我刚刚找到了这篇文章,您可能想在作者认为应有其专有名称的地方阅读:非虚拟接口
惯用语

1

接口的问题是它不能具有默认实现,这对于平凡的属性是一种痛苦,并且会破坏DRY。

我个人认为,接口应仅具有纯方法-无默认实现。它不会以任何方式破坏DRY原理,因为接口显示了如何访问某些实体。仅供参考,我在这里查看DRY的解释:
“每条知识在系统中必须具有单一,明确,权威的表示形式。”

另一方面,SOLID告诉您每个类都应该有一个接口。

案例3是否优于案例1?是因为“更好的封装”或“更松散地耦合”,还是其他原因?

不,情况3不优于情况1。您必须下定决心。如果要使用默认实现,请这样做。如果您想要一个纯净的方法,那就去吧。

如果从现在开始的3个月后,Joe Coder创建了一个Doctor Employee,但他忘记在do_work()中调用增量_money_earned(),该怎么办?

然后,Joe Coder应该得到他应得的,可以忽略失败的单元测试。他确实考了这堂课,不是吗?:)

哪种案例最适合可能包含40,000行代码的软件项目?

一种尺寸并不适合所有尺寸。无法判断哪个更好。在某些情况下,一个比另一个更适合。

也许您应该学习一些设计模式,而不是尝试自己发明一些设计模式


我刚刚意识到您正在寻找非虚拟接口设计模式,因为这就是您的case 3类的外观。


感谢您的评论。我已经更新了案例3,以使意图更加清晰。
MustafaM 2012年

1
我要在这里-1。完全没有理由说所有接口都应该是纯接口,或者所有类都应该从接口继承。
DeadMG

@DeadMG ISP
BЈовић

@VJovic:SOLID与“一切都必须从接口继承”之间有很大的区别。
DeadMG

“一种尺寸不能满足所有需求”和“学习一些设计模式”是正确的-其余答案违反了您自己的建议,即一种尺寸不能满足所有需求。
Joris Timmermans 2012年

0

接口可以具有C ++中的默认实现。没有什么可说的是,函数的默认实现不仅仅依赖于其他虚拟成员(和参数),因此不会增加任何形式的耦合。

对于情况2,DRY在这里取代。存在封装可以保护您的程序免受更改,不同实现的影响,但是在这种情况下,您没有不同的实现。因此YAGNI封装。

实际上,运行时接口通常被认为不如它们的编译时等效项。在编译时的情况下,可以有两种情况1 ,并在同一bundle-情况2没有提到它的众多优点。甚至在运行时,您都可以简单地Employee : public IEmployee有效地获得相同的优势。有很多处理此类问题的方法。

Case 3: (best of both worlds?)

Similar to Case 1. However, imagine that (hypothetically)

我停止阅读了。亚尼 C ++就是C ++,而出于绝妙的原因,标准委员会永远也不会实施这种更改。


您说“您没有不同的实现”。但是我愿意。我有Employee的Nurse实现,以后可能还有其他实现(Doctor,Jantor等)。我已经更新了案例3,以使我的意思更加清楚。
MustafaM 2012年

@illmath:但是您没有其他实现get_name。您建议的所有实现都将共享的相同实现get_name。此外,正如我所说,没有理由选择,您可以同时选择两者。同样,情况3毫无价值。您可以覆盖非纯虚拟机,因此请忘记您无法做到的设计。
DeadMG

接口不仅可以具有C ++的默认实现,而且可以具有默认实现,并且仍然是抽象的!即虚拟void IMethod()= 0 {std :: cout <<“ Ni!” << std :: endl; }
Joris Timmermans 2012年

@MadKeithV:我不相信您可以内联定义它们,但是重点仍然是相同的。
DeadMG

@MadKeith:好像Visual Studio曾经是标准C ++的特别准确的表示。
DeadMG

0

案例3是否优于案例1?是因为“更好的封装”或“更松散地耦合”,还是其他原因?

从我在您的实现中看到的情况来看,您的Case 3实现需要一个抽象类,该抽象类可以实现纯虚拟方法,然后可以在派生类中对其进行更改。情况3会更好,因为派生类可以在需要时更改do_work的实现,并且所有派生实例基本上都属于基本抽象类型。

对于可能具有40,000行代码的软件项目,哪种情况最合适。

我会说这完全取决于您的实现设计和您想要实现的目标。抽象类和接口是基于必须解决的问题实现的。

按问题编辑

如果从现在开始的3个月后,Joe Coder创建了一个Doctor Employee,但他忘记在do_work()中调用增量_money_earned(),该怎么办?

可以执行单元测试以检查每个类是否都确认了预期的行为。因此,如果应用了适当的单元测试,则在Joe Coder实现新类时可以避免错误。


0

如果每个实现都是彼此重复的,则使用接口只会破坏DRY。您可以通过应用接口继承来解决这个难题,但是在某些情况下,您可能希望在多个类上实现相同的接口,但是会改变每个类的行为,并且这仍将保持原则DRY。无论您选择使用上述3种方法中的任何一种,都取决于您采用最佳技术来匹配给定情况所需做出的选择。另一方面,随着时间的流逝,您可能会发现您更多地使用了Interfaces,并且仅在希望删除重复的位置应用继承。这并不是说这是唯一的 继承的原因,但是最好是尽量减少使用继承,以使您在以后发现需要更改设计时希望保持选项开放,并且希望将更改的影响最小化对后代类的影响将在父类中引入。

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.