我知道问题一直是discussed before,但似乎总是假设继承至少有时比组合更可取。我想挑战这个假设,希望获得一些理解。
我的问题是:从开始,您可以使用经典继承来完成任何具有对象组合的内容,并且因为经典继承经常被滥用[1] 并且由于对象组合使您可以灵活地更改委托对象运行时,为什么曾使用经典继承?
我可以理解为什么你会推荐一些语言(如Java和C ++)的继承,这些语言不能提供方便的委托语法。在这些语言中,只要不明显不正确,就可以使用继承来节省大量的输入。但是像Objective C和Ruby这样的其他语言为委托提供了经典的继承和非常方便的语法。 Go编程语言是我所知道的唯一一种语言,它认为经典继承比它的价值更麻烦,并且仅支持代码重用的委托。
另一种陈述我的问题的方法是:即使您知道经典继承对于实现某个模型并不是错误的,那么这个理由是否足以使用它而不是组合?
[1]许多人使用经典继承来实现多态,而不是让他们的类实现接口。继承的目的是代码重用,而不是多态。此外,有些人使用继承来模拟他们对“is-a”关系which can often be problematic的直观理解。
更新
我只想在谈到继承时澄清我的意思:
我在谈论the kind of inheritance whereby a class inherits from a partially or fully implemented base class。我不谈论从纯粹抽象的基类继承,这与实现接口相同,我记录的并不反对。
更新2
我知道继承是实现C ++多态性的唯一方法。在这种情况下,显而易见的是你必须使用它。所以我的问题仅限于Java或Ruby等语言,它们提供了实现多态的不同方法(分别是接口和鸭子类型)。
答案 0 :(得分:46)
继承的目的是代码重用,而不是多态。
这是你的根本错误。几乎完全恰恰相反。 (公共)继承的主要目的是对相关类之间的关系建模。多态性是其中很大一部分。
如果使用得当,继承不是重用现有代码。相反,它是关于 by 现有代码。也就是说,如果现有的代码可以使用现有的基类,那么从现有的基类派生新类时,其他代码现在也可以自动使用新的派生类。
可以使用继承来重用代码,但是当/如果这样做,它通常应该是私有继承而不是公共继承。如果您使用的语言支持委派,那么您很少有充分的理由使用私有继承。 OTOH,私有继承确实支持委托(通常)没有的一些事情。特别是,即使在这种情况下多态性是一个明确的次要问题,它仍然是一个问题 - 即,对于私有继承,你可以从几乎你想要什么,并且(假设它允许)覆盖不太正确的部分。
使用委托,您唯一真正的选择是完全按照现有的类使用它。如果它没有达到您想要的效果,那么您唯一真正的选择就是完全忽略该功能,并从头开始重新实现它。在某些情况下,这并没有损失,但在其他情况下则相当可观。如果基类的其他部分使用多态函数,则私有继承允许您覆盖仅多态函数,其他部分将使用重写函数。使用委托,您无法轻松插入新功能,因此现有基类的其他部分将使用您已覆盖的内容。
答案 1 :(得分:16)
使用继承的主要原因是不作为一种组合形式 - 这样你就可以获得多态行为。如果你不需要多态,你可能不应该使用继承,至少在C ++中。
答案 2 :(得分:10)
如果您将未显式覆盖的所有内容委托给实现相同接口的其他对象(“基础”对象),那么您基本上在组合的基础上进行Greenspunned继承,但是(在大多数语言中)具有更多冗长和样板。使用组合而不是继承的目的是,您只能委派要委派的行为。
如果希望对象使用基类的所有行为,除非显式重写,那么继承是表达它的最简单,最简单,最简单的方法。
答案 3 :(得分:7)
每个人都知道多态性是继承的一大优势。我在继承中发现的另一个好处是有助于创建现实世界的复制品。例如,在付费滚动系统中,如果我们使用超类Employee继承所有这些类,我们会处理经理开发人员,办公室男生等。它使我们的程序在现实世界的背景下更容易理解,所有这些类基本上都是员工。还有一个类不仅包含它们还包含属性的方法。因此,如果我们在Employee中包含员工的通用属性 类似于社会安全号码年龄等,它将提供更多的代码重用和概念清晰度,当然还有多态性。然而,在使用继承的时候,我们应该牢记的是基本的设计原则“确定应用程序的各个方面,这些方面会有所不同,并将它们从那些变化的方面分开”。您永远不应该实现通过继承而是使用组合更改的应用程序方面。对于那些不可改变的方面,如果明显的“是一种”关系存在,你应该使用继承。
答案 4 :(得分:6)
如果符合以下情况,则首选继承:
我的结论:委托是编程语言中的错误的解决方法。
答案 5 :(得分:4)
在使用继承之前我总是三思而后行,因为它很快就会变得棘手。话虽如此,很多情况下它只会产生最优雅的代码。
答案 6 :(得分:3)
接口只定义对象可以做什么而不是如何做。所以简单来说,接口只是合同。实现该接口的所有对象都必须定义自己的合同实现。在实际的世界中,这会给你separation of concern
。想象一下,你自己编写的应用程序需要事先处理你不了解它们的各种对象,你仍然需要处理它们,只有你知道的是这些对象应该做的所有不同的事情。因此,您将定义一个接口并提及合同中的所有操作。现在,您将针对该界面编写应用程序。之后,无论谁想要利用您的代码或应用程序,都必须在对象上实现接口,以使其适用于您的系统。您的界面将强制其对象定义合同中定义的每个操作应该如何完成。通过这种方式,任何人都可以编写实现您的界面的对象,以便让它们完美地适应您的系统,您所知道的就是需要完成的任务,并且需要定义它是如何完成的。
在现实世界的发展中 练习通常被称为
Programming to Interface and not to Implementation
。接口只是合同或签名,他们不知道 关于实施的任何事情。
对接口进行编码意味着,客户端代码始终保存由工厂提供的Interface对象。工厂返回的任何实例都是Interface类型,任何工厂候选类必须已实现。这样客户端程序就不会担心实现,接口签名决定了所有操作都可以完成。这可用于在运行时更改程序的行为。它还可以帮助您从维护的角度编写更好的程序。
这是一个基本的例子。
public enum Language
{
English, German, Spanish
}
public class SpeakerFactory
{
public static ISpeaker CreateSpeaker(Language language)
{
switch (language)
{
case Language.English:
return new EnglishSpeaker();
case Language.German:
return new GermanSpeaker();
case Language.Spanish:
return new SpanishSpeaker();
default:
throw new ApplicationException("No speaker can speak such language");
}
}
}
[STAThread]
static void Main()
{
//This is your client code.
ISpeaker speaker = SpeakerFactory.CreateSpeaker(Language.English);
speaker.Speak();
Console.ReadLine();
}
public interface ISpeaker
{
void Speak();
}
public class EnglishSpeaker : ISpeaker
{
public EnglishSpeaker() { }
#region ISpeaker Members
public void Speak()
{
Console.WriteLine("I speak English.");
}
#endregion
}
public class GermanSpeaker : ISpeaker
{
public GermanSpeaker() { }
#region ISpeaker Members
public void Speak()
{
Console.WriteLine("I speak German.");
}
#endregion
}
public class SpanishSpeaker : ISpeaker
{
public SpanishSpeaker() { }
#region ISpeaker Members
public void Speak()
{
Console.WriteLine("I speak Spanish.");
}
#endregion
}
答案 7 :(得分:2)
模板方法模式怎么样?假设您有一个基础类,其中包含大量可用于自定义策略的分数,但策略模式至少有以下原因之一是没有意义的:
可自定义的策略需要了解基类,只能与基类一起使用,并且在任何其他上下文中都没有意义。使用策略是可行的,但是PITA是可行的,因为基类和策略类都需要相互引用。
这些政策是相互耦合的,因为自由地混合和匹配它们是没有意义的。它们只在所有可能组合的非常有限的子集中有意义。
答案 8 :(得分:0)
您写道:
[1]很多人使用古典音乐 继承实现多态 而不是让他们的课程 实现一个接口。的目的 继承是代码重用,而不是 多态性。还有一些人 使用继承来建模他们的 直观地理解“is-a” 通常可以的关系 有问题的。
在大多数语言中,“实现接口”和“从另一个接收类”之间的界限非常薄。实际上,在像C ++这样的语言中,如果你从类A派生出一个B类,而A是一个只包含纯虚方法的类,那么 实现一个接口。
继承是关于接口重用,而不是实现重用。关于代码重用,不,如上所述。
正如你正确指出的那样,继承是为了模拟一个IS-A关系(很多人弄错了这个事实与继承本身无关)。你也可以说'BEHAVES-LIKE-A'。然而,仅仅因为某些东西与其他东西有IS-A关系并不意味着它使用相同(甚至类似)的代码来实现这种关系。
比较这个实现不同输出数据方式的C ++示例;两个类使用(公共)继承,以便它们可以多态访问:
struct Output {
virtual bool readyToWrite() const = 0;
virtual void write(const char *data, size_t len) = 0;
};
struct NetworkOutput : public Output {
NetworkOutput(const char *host, unsigned short port);
bool readyToWrite();
void write(const char *data, size_t len);
};
struct FileOutput : public Output {
FileOutput(const char *fileName);
bool readyToWrite();
void write(const char *data, size_t len);
};
现在想象一下,如果这是Java。 '输出'不是结构,而是'界面'。它可能被称为“可写”。而不是'公共输出',你会说'实现可写'。就设计而言,有什么区别?
无。
答案 9 :(得分:0)
当你问:
即使您知道经典继承对于实现某个模型并不是错误的,那么这个理由是否足以使用它而不是组合?
答案是否定的。如果模型不正确(使用继承),那么无论使用什么都是错误的。
以下是我看到的继承问题:
答案 10 :(得分:0)
经典继承的主要用途是,如果你有许多相关的类,它们对于操作实例变量/属性的方法具有相同的逻辑。
有三种方法可以解决这个问题:
现在,可能会滥用继承。例如,Java具有类InputStream
和OutputStream
。这些子类用于读/写文件,套接字,数组,字符串,还有几个用于包装其他输入/输出流。根据他们的工作,这些应该是接口而不是类。
答案 11 :(得分:0)
我认为使用继承的最有用的方法之一是在GUI对象中。
答案 12 :(得分:0)
完全不是OOP但是,组合通常意味着额外的缓存缺失。它确实依赖,但让数据更接近是一个优势。
一般来说,我拒绝参加一些宗教斗争,使用自己的判断和风格是你能得到的最好的。