继承和责任

时间:2013-08-08 19:56:09

标签: oop inheritance single-responsibility-principle

当我读到关于继承时,我总是对某个例子感到困惑。

通常有一个类似于下面例子的例子。

class Shape
{
public:
    Shape() {}
    virtual ~Shape  () {}
    virtual void Draw() = 0;
};

class Cube : public Shape
{
public:
   Cube(){}
   ~Cube(){}
   virtual void Draw();
};

Shape* newCube = new Cube();
newCube->Draw(); 

我的问题是,为什么Shape有责任自拔?难道渲染器类不应该知道如何绘制形状而是为渲染器提供形状吗?如果我们想记录维度的变化怎么办?等等?我们是否可以为Shape内的每个不同任务设置方法?

看到这样的大量例子有时让我想知道我将责任分配给班级的能力。有什么我不理解只有一个责任的课程吗?

7 个答案:

答案 0 :(得分:4)

OOP宣传发送消息,而不是“询问”某些外部数据然后处理的程序代码。

如果将draw方法放在渲染器中,则会破坏类Shape封装,因为它肯定需要访问其内部(如坐标) (x,y)等..)。

通过让Shape绘制“本身”,您可以保持封装,从而提高内部变更的灵活性。

解决方案实际上取决于复杂性 通过从Shape中提取绘制方法,您的形状将需要公开其数据 保持它,你保持封装。

因此,如果您的抽奖很复杂,则更愿意将其视为渲染器或图形所承担的另一项全部责任,从而与您的建议相对应。

答案 1 :(得分:4)

我经常发现这些简单的教科书例子无法充分解释原因,因为它们过于简单化。我们可以给Shape类做很多事情:绘制自己,计算它的面积,确定给定点是否在其范围内,计算出从另一个形状的交点得到的形状,记住有多少人认为它是他们最喜欢的形状...只要您的想象力和您给予的责任取决于您的计划的目标以及您如何选择构建它。

假设您希望能够以通用的多态方式绘制形状,请考虑如何实际实现它。在什么样的形状上绘制?形状是否会知道画布的作用?它是否应该知道它需要拿起画笔,将它浸入一些油漆然后自己绘制?它应该知道您的显示驱动程序如何工作?设置位以在正确的位置打开像素,以便显示器显示正确的形状?

显然,降低到这个级别会给形状带来太大的责任,所以你可以定义一组图形基元(例如:点和线)并构建一个可以渲染它们的图形API。然后Shape可以使用基元来告诉API要绘制什么。图形API不知道它正在绘制一个正方形,但是通过告诉它绘制四条线而不是它绘制一个正方形。所有这一切都留下了Shape,其唯一的责任是了解它的点和定义它的线。

当你单独上课时,总是很难看到某些设计模式的好处,因为构建软件是为了让事情协同工作;没有什么能孤立地工作。

答案 2 :(得分:3)

Shape应该不知道它是如何绘制的。设计的项目越大,这个决定就越重要。

对我来说,这一切归结为circular dependencies,除了最严重的情况之外,除了头痛外,其他所有事情都会引起头痛。

Model View Controller的一个基本原则是,你做的事情(动词或&#34;视图&#34;)明确地与事物<分开/ em>(名词,或者#34;控制器&#34;)正在被操纵或分析:表示和逻辑的分离。 &#34;模型&#34;是中间人。

它也是single responsibility principle:&#34; ......每个班级都应该承担一项责任,而且责任应该由班级完全封装起来&#34;

背后的原因是:循环依赖意味着任何更改为任何,影响所有内容

单一责任原则的另一个(简洁编辑)引用:&#34;一个类或模块应该只有一个,而且只有一个要改变的理由。单一责任原则说明问题的 实质性 美容 方面实际上是两个独立的职责,因此应该在单独的类或模块中。 将两件因不同原因而在不同时间发生变化的事情结合起来将是一件糟糕的设计 。&#34; (强调我的)

最后,separation of concerns的概念:&#34;目标是设计系统,以便 函数可以独立于其他函数进行优化,这样一个函数的失败不会导致其他失败的功能 ,通常是为了更容易理解,设计和管理复杂的相互依赖的系统。&#34; (强调我的)


这不仅仅是编程设计问题。

考虑一个网站的开发,其中&#34;内容&#34;团队必须放置他们的文字和格式,颜色和图片,非常温和地围绕一些脚本(由&#34;开发&#34;团队创建),就这样或者一切都破裂了。内容团队希望看不到任何脚本 - 他们不想学习如何编程,只是因为他们可以改变一些单词或调整图像。而开发团队并不想担心,那些不知道如何编码的人所做的每一次微小的视觉变化都有可能打破他们的东西。 / p>

每天在我自己的项目上工作时都会想到这个概念。当两个源文件相互导入时,任何一个中的更改都需要重新编译 - 同时进行。对于较大的项目,这可能意味着一个微不足道的变化需要重新编译数百或数千个类。在我目前参与的三个主要项目中,有大约一千个不同的源代码文件,这种循环依赖有exactly one

无论是商务团队,源代码文件还是设计编程对象,我都建议避免使用循环依赖,除非绝对必要。


所以,至少,我不会把绘制函数放在Shape中。虽然非常依赖于正在设计的项目的类型和大小,但渲染可以由RenderingUtils类完成,只包含完成大部分工作的公共静态函数。

如果项目是一个中等大的项目,我会更进一步,创建一个Renderable接口作为模型层。 Shape实施Renderable,因此无法了解或关注它是如何绘制的。无论绘图需要了解Shape

这使您可以灵活地完全更改渲染的方式,而不会影响(或不必重新编译!)Shape,并且它还允许您渲染与Shape完全不同的内容,无需更改绘图代码。

答案 3 :(得分:3)

使Draw()成为基类方法的选择取决于上下文 - 要解决的具体问题。为了使问题稍微清楚一点,这是我在面试OO技能时经常使用的另一个例子。

想象一下Document类和Printer类。打印功能应该放在哪里?有两个明显的选择:

document.print(Printer &p);

printer.print(Document &d);

哪个是正确的?答案是:它取决于您希望多态行为的位置 - 在文档或打印机中。如果我们假设所有打印机都具有相同的功能(神话操作系统试图推广),那么多态行为应该在Document对象中。但是,如果我们想象所有文件大致相同(或者至少是我们关心的那些文件)并且打印机差别很大(过去就是这种情况) - 请考虑:绘图仪,行式打印机,激光打印机,菊花轮打印机等等,让打印机决定如何最好地呈现文档更有意义。

有人可能认为Print()应该是两个对象的一部分,因为打印机和文档的组合可能需要多态行为。在这种情况下,您需要double dispatch

答案 4 :(得分:1)

只有对象真正知道如何绘制自己。

想象一下锁匠......他可以选择1000种不同的锁类型。我可以去商店,购买任何锁,也可以给他,他可以选择它,因为他熟悉锁技术。

现在想象一下我是一个发明家,我开始制作自己的锁具,设计独特,具有革命性。他能够打开它们吗?也许,但也许不是......这取决于我在锁中做了什么,...我使用他/她知道的技术等等吗?

你的形状对象是相同的,...取决于它们在内部实现的方式决定它们是否可以由某些通用渲染引擎渲染。如果你要求每个物体自己绘制,那么你不必担心这个。

答案 5 :(得分:1)

上述答案对我来说似乎完全过于复杂。

形状和圆形示例的目的是描述界面(预期与外界交谈的方式)与实施(预期如何表现)之间的差异。

您提供的示例的问题是它被截断了。当涉及更多形状时,它会更有意义。

考虑具有圆形,三角形和矩形的情况。现在Shape将如何绘制自己?它不知道它是哪种类型,或者不知道该怎么做。

现在考虑一个形状容器。他们都有绘图方法;父类强制执行。因此,即使各种绘图方法的实现基本上不相关,您也可以拥有一个形状均匀的容器。

为什么Circle会自己绘制而不是塑造? 因为它知道如何

答案 6 :(得分:0)

纯虚函数用于在没有为集合明确定义算法的行为时,但是算法的存在是为集合明确定义的。

我希望这不是要消化太多,但也许功能分析的教训是相关的。我对虚函数的理论含义进行了深入研究。

让集合 A 的族具有属性{x:P(x)}
设A是家庭 A 的一个元素 让A&#39;也是家庭的一个元素 A

A和A&#39;可能属于以下三种类别中的一种。
(1)A和A&#39;相当于   对于A的所有元素,a是A&#39;的元素   AND对于~A的所有b元素,b是~A&#39;

的元素

(2)A和A&#39;相交
  存在A的元素,其中a是A&#39;的元素   还存在A的b元素,其中b是~A&#39;

的元素

(3)A和A&#39;是不相交的   A的元素a也不存在,也是A&#39;的元素
其中~X指的是不是集合X的元素的所有x

在情况(1)中,我们将定义非抽象行为如果U是家族的元素 A 意味着存在单个值u,使得u = P(U)为所有U是家庭 A

的元素

在情况(2)中,如果U是家族的元素,我们将定义虚拟行为 A 意味着存在单个值u,使得u = P(U&#39;)其中U&#39;是U的一个子集。

在情况(3)中,我们将定义纯虚拟行为,因为A和A&#39;只是因为他们都是 A 家族的成员,所以A和A的交集是相似的。是空集,暗示不存在A和A&#39;

的任何共同元素

考虑语法在逻辑定义方面的含义,您将能够回答以下问题:

(1)该功能是否需要抽象? (案例1不是,案例2和案例3是) (2)该功能是否需要纯虚拟? (案例1和案例2都没有,案件3是肯定的)

在案例2中,它还取决于行为所需信息的保存位置 - 在基类或派生类中。

您无法在DISPLAY中呈现SHAPE,而DISPLAY不会查找不必是SHAPE定义的一部分的信息。因为DISPLAY无法看到从SHAPE派生的类型的定义,超出了为SHAPE定义的类型。因此,必须为派生类中的抽象函数定义依赖于派生类型中包含的信息的任何功能。