抽象类上的接口


30

我和我的同事对基类和接口之间的关系有不同的看法。我相信一个类不应实现接口,除非在需要实现接口时可以使用该类。换句话说,我喜欢看这样的代码:

interface IFooWorker { void Work(); }

abstract class BaseWorker {
    ... base class behaviors ...
    public abstract void Work() { }
    protected string CleanData(string data) { ... }
}

class DbWorker : BaseWorker, IFooWorker {
    public void Work() {
        Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
    }
}

DbWorker是获得IFooWorker接口的对象,因为它是该接口的可实例化实现。它完全履行了合同。我的同事更喜欢几乎相同的东西:

interface IFooWorker { void Work(); }

abstract class BaseWorker : IFooWorker {
    ... base class behaviors ...
    public abstract void Work() { }
    protected string CleanData(string data) { ... }
}

class DbWorker : BaseWorker {
    public void Work() {
        Repository.AddCleanData(base.CleanData(UI.GetDirtyData()));
    }
}

基类在何处获得接口,因此,基类的所有继承者也都属于该接口。这给我带来了麻烦,但是我无法提出具体的原因,为什么“基类不能独立实现接口的实现”。

他的方法与我的方法有什么优缺点,为什么要在另一个方法上使用一个方法?


您的建议与钻石继承非常相似,这可能会导致很多混乱。
Spoike 2012年

@Spoike:怎么看?

@Niing我在评论中提供的链接有一个简单的类图和一个简单的一线解释。之所以称其为“钻石”问题,是因为继承结构基本上像菱形一样绘制(即♦️)。
Spoike

@Spoike:为什么这个问题会导致钻石继承?

1
@Niing:Work钻石继承问题是使用称为的相同方法继承BaseWorker和IFooWorker (在这种情况下,它们都在该Work方法上履行了合同)。在Java中,您需要实现接口的方法,这样Work可以避免程序应使用哪种方法的问题。但是,诸如C ++之类的语言不会为您消除歧义。
Spoike

Answers:


24

我相信一个类不应实现接口,除非在需要实现接口时可以使用该类。

BaseWorker满足该要求。仅仅因为您不能直接实例化一个BaseWorker对象并不意味着您就不能拥有实现合同的BaseWorker 指针。确实,这几乎是抽象类的全部要点。

另外,很难从您发布的简化示例中看出来,但是部分问题可能是接口和抽象类是多余的。除非您有其他不能从派生的类实现IFooWorker,否则根本不需要接口。接口只是抽象类,具有一些其他限制,使多重继承更加容易。BaseWorker

从一个简化的示例中很难看出,使用受保护的方法(显式地引用派生类的基类)以及缺少明确的位置来声明接口实现的情况都是警告信号,指示您在不适当地使用继承而不是继承。组成。没有这种继承,您的整个问题将变得毫无意义。


1
+1指出仅仅因为BaseWorker不能直接实例化,并不意味着一个给定的BaseWorker myWorker不能实现IFooWorker
Avner Shahar-Kashtan'9

2
我不同意接口只是抽象类。抽象类通常定义一些实现细节(否则将使用接口)。如果您不喜欢这些详细信息,则现在必须将基类的每种用法更改为其他用法。界面较浅;他们定义了依赖和依赖之间的“即插即用”关系。这样,与任何实现细节的紧密耦合不再是问题。如果您不喜欢继承该接口的类,请删除它,然后完全放入其他内容;您的代码可能会少一些。
KeithS 2012年

5
它们都具有优势,但是通常无论是否有定义基本实现的抽象类,都应始终将接口用于依赖项。接口和抽象类的结合使两者兼具。接口保持浅层表面的插拔关系,而抽象类提供有用的通用代码。您可以随意删除该接口下的任何级别的层并进行替换。
KeithS 2012年

通过“指针”你的意思是一个对象的实例(其中一个指针将引用)?接口不是类,而是类型,可以作为多继承的不完全替代品,而不是替代品,也就是说,它们“在类中包含来自多个来源的行为”。
samis

46

我必须同意你的同事。

在您给出的两个示例中,BaseWorker都定义了抽象方法Work(),这意味着所有子类都能够满足IFooWorker的合同。在这种情况下,我认为BaseWorker应该实现该接口,并且该实现将由其子类继承。这将使您不必显式指示每个子类确实是IFooWorker(DRY原理)。

如果您没有将Work()定义为BaseWorker的方法,或者IFooWorker拥有BaseWorker子类不需要或不需要的其他方法,那么(显然)您必须指出哪些子类实际实现了IFooWorker。


11
+1会发布相同的内容。抱歉,insta,但是在这种情况下,我认为您的同事也是正确的,如果某项保证可以履行合同,则无论继承是通过接口,抽象类还是具体类来实现,它都应该继承该合同。简而言之:担保人应继承其担保的合同。
Jimmy Hoffa 2012年

2
我通常同意,但是要指出的是,诸如“ base”,“ standard”,“ default”,“ generic”等词是类名中的代码味道。如果抽象基类与接口几乎具有相同的名称,但其中包含其中一些狡猾的词,则通常是不正确抽象的征兆;接口定义的东西是什么,类型继承定义它是什么。如果您有一个IFooand一个,BaseFoo则意味着IFoo不是一个适当的细粒度接口,或者BaseFoo是在使用继承的情况下,组合可能是一个更好的选择。
亚伦诺特,2012年

@Aaronaught好点,尽管可能确实存在某些或所有IFoo实现都需要继承的东西(例如服务的句柄或您的DAO实现)。
马修·弗林

2
那么,是否不应该调用基类MyDaoFooSqlFooHibernateFoo或其他方法来表明它只是实现类的一种可能的树IFoo呢?如果将基类耦合到特定的库或平台,而在名称中却没有提及这一点,那对我来说甚至更难闻……
Aaronaught 2012年

@Aaronaught-是的。我完全同意。
马修·弗林

11

我通常同意您的同事。

让我们以您的模型为例:该接口仅由子类实现,即使基类也强制公开IFooWorker方法。首先,它是多余的;无论子类是否实现该接口,都要求它们重写BaseWorker的公开方法,同样,无论它们是否从BaseWorker获得帮助,任何IFooWorker实现都必须公开该功能。

另外,这会使您的层次结构变得模糊。“所有IFooWorkers都是IFooWorkers”不一定是正确的陈述,“所有BaseWorkers都是IFooWorkers”也不一定。因此,如果您想定义一个实例变量,参数或返回类型,它可以是IFooWorker或BaseWorker的任何实现(利用通用的公开功能,这是首先继承的原因之一),那么这些被保证是无所不包的;某些BaseWorkers无法分配给IFooWorker类型的变量,反之亦然。

您同事的模型更易于使用和替换。现在,“所有BaseWorkers都是IFooWorkers”是正确的说法;您可以将任何BaseWorker实例赋予任何IFooWorker依赖关系,没有问题。相反的陈述“所有IFooWorkers都是BaseWorkers”是不正确的;这样就可以用BetterBaseWorker替换BaseWorker,而您的使用代码(取决于IFooWorker)将不必说出区别。


我喜欢这个答案的声音,但并没有完全遵循最后一部分。您建议BetterBaseWorker是实现IFooWorker的独立抽象类,以便我的假设插件可以简单地说:“哦,男孩!更好的基础工作者终于出来了!” 并将任何对BaseWorker的引用更改为BetterBaseWorker并将其命名为一天(也许玩一些很酷的新返回值,这些值使我可以剥离大量冗余代码,等等)?
安东尼

或多或少,是的。最大的好处是您将减少对BaseWorker的引用数量,而这些引用必须更改为BetterBaseWorkers。大多数代码不会直接引用具体的类,而是使用IFooWorker,因此,当您更改分配给这些IFooWorker依赖项(类的属性,构造函数或方法的参数)的内容时,使用IFooWorker的代码应该不会有任何区别在使用中。
KeithS 2015年

6

我需要在这些答案中添加一些警告。将基类与接口耦合会在该类的结构中产生作用力。在您的基本示例中,将两者结合在一起是不费吹灰之力的,但这可能并非在所有情况下都适用。

以Java的收集框架类为例:

abstract class AbstractList
class LinkedList extends AbstractList implements Queue

Queue契约由LinkedList实现的事实并没有将关注点推到AbstractList中

两种情况有什么区别?始终(通过其名称和接口传达)BaseWorker的目的是在IWorker中实现操作。AbstractListQueue的目的是不同的,但是前者的后裔仍然可以实现后者的契约。


这发生一半的时间。他总是喜欢在基类上实现接口,而我总是喜欢在最终的混凝土上实现接口。您提出的场景经常发生,并且是基础界面困扰我的一部分。
Bryan Boettcher

1
是的,insta,我认为以这种方式使用接口是我们在许多开发环境中看到的继承过度使用的一个方面。就像GoF在其设计模式书中所说的那样,相对于继承而言更喜欢使用组合,而将接口保留在基类之外是促进这一原理的方法之一。
Mihai Danila 2012年

1
我得到了警告,但您会注意到OP的抽象类包括与接口匹配的抽象方法:BaseWorker隐式为IFooWorker。明确表示这一事实更加有用。Queue中包含许多方法,但是AbstractList中没有这些方法,因此,即使可能是AbstractList的子级,它也不是Queue。AbstractList和Queue是正交的。
马修·弗林

感谢您的见解;我确实同意OP的案例属于这一类,但是我感觉到有更多的讨论在寻求更深层次的规则,我想分享我对我所读过的一种趋势的看法(甚至让我自己在短时间内)过度使用继承,也许是因为这是OOP工具箱中的工具之一。
Mihai Danila

堂,已经六年了 :)
Mihai Danila

2

我想问一个问题,当您进行更改IFooWorker(例如添加新方法)时会发生什么?

如果BaseWorker实现该接口,则默认情况下,即使它是抽象方法,也必须声明新方法。另一方面,如果它不实现该接口,则只会在派生类上得到编译错误。

因此,我可以使基类实现该接口,因为我可以在不触摸派生类的情况下实现基类中新方法的所有功能。


1

首先考虑一下什么是抽象基类以及什么是接口。考虑何时使用一个或另一个,何时不使用。

人们通常认为这两个概念非常相似,实际上这是一个常见的面试问题(两者之间的区别是?)

因此,抽象基类为您提供了某些方法无法实现的接口,这些接口是方法的默认实现。(还有其他一些东西,在C#中,您可以在抽象类上(而不是在接口上)使用静态方法)。

例如,IDisposable是该方法的一种常见用法。抽象类实现IDisposable的Dispose方法,这意味着基类的任何派生版本将自动变为可抛弃的。然后,您可以玩几个选项。使Dispose抽象,强制派生类实现它。提供一个虚拟的默认实现,使它们不这样做,或者使其既不是虚拟的也不是抽象的,并使其调用称为例如BeforeDispose,AfterDispose,OnDispose或Dispose之类的虚拟方法。

因此,每当所有派生类需要支持该接口时,它都会在基类上进行。如果只有一个或某些需要该接口,它将进入派生类。

实际上,这简直就是毛骨悚然。另一种选择是让派生类甚至不实现接口,而是通过适配器模式提供它们。我最近看到的一个示例是在IObjectContextAdapter中。


1

我距离完全掌握抽象类和接口之间的区别还有很长的路要走。每次我认为掌握基本概念时,我都会去研究stackexchange,然后退后两步。但是,关于该主题和OP的一些想法是:

第一:

接口有两种常见的解释:

  1. 接口是任何类都可以实现的方法和属性的列表,并且通过实现接口,类可以保证那些方法(及其签名)和那些属性(及其类型)在与该类或接口“接口”时可用。该类的对象。接口是合同。

  2. 接口是抽象类,不做任何事情。它们非常有用,因为您可以实现多个,而不是那些卑鄙的父类。就像,我可能是BananaBread类的对象,并且继承自BaseBread,但这并不意味着我也不能同时实现IWithNuts和ITastesYummy接口。我什至可以实现IDoesTheDishes接口,因为我不只是面包,知道吗?

关于抽象类,有两种常见的解释:

  1. 抽象类,yknow的东西不是什么事情可以做。就像本质一样,根本不是真的。等等,这会有所帮助。船是抽象类,但性感的花花公子游艇将是BaseBoat的子类。

  2. 我读了一本关于抽象类的书,也许您应该读这本书,因为您可能不理解这本书,如果您还没有读过那本书,那就会做错事情。

顺便说一句,即使我仍然困惑不解,《 Citers》一书似乎总是给人留下深刻的印象。

第二:

在SO上,有人问这个问题的简单版本,经典的,“为什么要使用接口?有什么区别?我缺少什么?” 一个答案以空军飞行员为例。它并没有真正地着陆,但引发了一些很棒的评论,其中之一提到了具有诸如takeOff,pilotEject等方法的IFlyable接口。这确实使我感到震惊,因为它是一个现实的示例,说明了为什么接口不仅有用,但至关重要。界面使对象/类直观化,或者至少给出了它的真实感。接口不是为了对象或数据的利益,而是为了需要与该对象进行交互的东西。经典的水果->苹果->富士或形状->三角形-> 继承的等边示例是一个很好的模型,可以根据其后代分类学地了解给定的对象。它告知消费者和加工者其一般质量,行为,对象是否是一堆东西,如果将其放在错误的位置或描述感觉数据,特定数据存储的连接器或将其连接到系统,或进行工资核算所需的财务数据。

特定的Plane对象可能有也可能没有紧急着陆的方法,但是如果我像其他所有可飞行对象一样假设其紧急状态,只是想唤醒被DumbPlane覆盖并得知开发人员使用quickLand,我将大为恼火因为他们认为都是一样的。就像每个螺丝制造商对自己的紧身衣裤都有自己的解释,或者我的电视在音量减小按钮上方没有音量增大按钮一样,我会感到沮丧。

抽象类是建立任何后代对象必须具备的资格的模型。如果您没有鸭子的所有特质,那么实现IQuack接口(您只是一只奇怪的企鹅)也没关系。即使您不确定其他任何事物,接口也是有意义的事情。杰夫·戈德布鲁姆(Jeff Goldblum)和星巴克(Starbuck)都能够飞行外星飞船,因为界面确实可靠。

第三:

我同意您的同事的观点,因为有时您需要从一开始就强制执行某些方法。如果要创建Active Record ORM,则需要使用save方法。这不取决于可以实例化的子类。而且,如果ICRUD接口具有足够的可移植性,而不是专门与一个抽象类耦合,则可以由其他类实现该接口,以使对那些已经熟悉该抽象类的任何后代类的人来说,它们可靠且直观。

另一方面,前面有一个很好的例子,说明何时不跳到将接口与抽象类绑定,因为并非所有列表类型都将(或应该)实现队列接口。您说这种情况发生的时间是一半,这意味着您和您的同事都在一半的时间都错了,因此,最好的办法是辩论,辩论,考虑,如果事实证明是对的,则承认并接受这种耦合。但是,即使这不是当前工作的最佳选择,也不要成为遵循哲学的开发人员。


0

正如您建议的那样,为什么DbWorker还要实施还有另一个方面IFooWorker

对于您的同事的风格,如果由于某种原因发生了以后的重构,并且DbWorker认为该重构不扩展抽象BaseWorker类,则会DbWorker丢失该IFooWorker接口。如果他们希望使用该IFooWorker接口,则可能(但不一定)对使用它的客户端产生影响。


-1

首先,我喜欢不同地定义接口和抽象类:

Interface: a contract that each class must fulfill if they are to implement that interface
Abstract class: a general class (i.e vehicle is an abstract class, while porche911 is not)

在您的情况下,所有工人都必须执行work()合同,因为这是合同所要求的。因此,您的基类Baseworker应显式地或作为虚函数实现该方法。我建议您将这种方法放在基类中,因为所有工作人员都需要这样做work()。因此,您的基类(即使您无法创建该类型的指针)将其作为函数包含也是合乎逻辑的。

现在,DbWorker确实满足了IFooWorker接口的要求,但是如果此类具有某种特定的方式work(),那么它确实需要重载从继承的定义BaseWorker。它不应该IFooWorker直接实现该接口的原因是,这不是特别的DbWorker。如果每次执行类似的类时都这样做,DbWorker那么您将违反DRY

如果两个类以不同的方式实现相同的功能,则可以开始寻找最大的公共超类。大多数时候,您会找到一个。如果没有,要么继续寻找,要么放弃,接受他们没有足够的共同点来构成基类。


-3

哇,所有这些答案都没有指出示例中的接口毫无用处。您不要将继承和接口用于同一方法,这是没有意义的。您使用其中一个。这个论坛上有很多问题,答案解释了每种方法的优点以及要求一种或另一种方法的优点。


3
那是专利虚假。接口是一个合同,系统可以根据该合同来进行操作,而与实现无关。继承的基类是此接口的许多可能实现之一。一个足够大的系统将具有满足各种接口(通常使用泛型关闭)的多个基类,这正是此设置所做的事情以及为什么拆分它们会有所帮助。
Bryan Boettcher

从OOP的角度来看,这是一个非常糟糕的例子。当您说“基类只是一个实现”(接口应该是或没有意义的时候)时,您在说将存在实现IFooWorker接口的非工人类。然后,您将拥有一个专门的fooworker的基类(我们应该假定也可能有barworkers),并且您将有其他甚至不是基本工人的fooworker!那是落后的。以不止一种方式。
马丁·马特

1
也许您被界面中的“工人”一词所困扰。那“数据源”呢?如果我们有一个IDatasource <T>,那么我们就可以简单地拥有一个SqlDatasource <T>,RestDatasource <T>,MongoDatasource <T>。业务决定通用接口的哪些关闭进入抽象数据源的关闭实现。
Bryan Boettcher

仅出于DRY的目的而使用继承,并在基类中放置一些常见的实现是有意义的。但是您不会将任何覆盖的方法与任何接口方法对齐,基类将包含常规支持逻辑。如果他们排队,那将表明继承可以很好地解决您的问题,并且该接口将无济于事。如果继承不能解决问题,则可以使用接口,因为要建模的通用特征不适合singke类树。是的,该示例中的措辞更加错误。
马丁·马特
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.