10

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

通常有一个类似于以下示例的示例。

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画自己是 ' 的责任?渲染器类不应该知道如何绘制形状并将形状提供给渲染器吗?如果我们想记录维度的变化怎么办?ETC?我们会为其中的每个不同任务提供一种方法Shape吗?

看到许多这样的例子有时会让我怀疑我是否有能力为班级分配职责。关于只有一个责任的类,我有什么不明白的吗?

4

7 回答 7

4

OOP 提倡发送消息,与“询问”一些外部数据然后处理的过程代码相反。

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

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

解决方案实际上取决于复杂性。
通过从 Shape 中提取 draw 方法,您的形状将需要公开其数据。
通过保留它,您可以继续封装。

因此,如果您的绘图很复杂,最好将其视为由渲染器或图形承担的另一个整体责任,从而符合您的建议。

于 2013-08-08T20:03:59.123 回答
4

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

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

document.print(Printer &p);

或者

printer.print(Document &d);

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

有人可能会争辩说,这Print()不应该是任何一个对象的一部分,因为多态行为可能是打印机和文档的组合所需要的。在这种情况下,您需要双重调度

于 2014-07-04T08:31:46.520 回答
4

我经常发现这些简单的教科书示例无法充分解释原因,因为它们过于简单化。我们可以让一个Shape类负责做很多事情:绘制自己,计算它的面积,计算一个给定的点是否在它的范围内,计算另一个形状的交集会产生什么形状,记住有多少人计数它是他们最喜欢的形状......这个列表只要你的想象力和你赋予它的责任取决于你的程序的目标和你如何选择构建它。

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

显然,下降到这个级别会给形状带来太多的责任,因此您可以定义一组图形基元(例如:点和线)并构建一个可以渲染它们的图形 API。然后AShape可以使用原语告诉 API 要绘制什么。图形 API 不知道它在画一个正方形,但是通过告诉它画四条线,嘿,它很快就画了一个正方形。所有这一切都留下了一个Shape单一的责任,即了解它的点和定义它的线。

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

于 2014-07-04T21:00:26.007 回答
3

AShape不应该知道它是如何绘制的。正在设计的项目越大,这个决定就越关键。

对我来说,这一切都归结为循环依赖,除了最极端的情况外,它只会引起头痛。

Model View Controller的一个基本原则是,您所做的事情(动词或“视图”)与被操作或分析的事情(名词或“控制器”)明显分开:表示和逻辑分离. “模型”是中间人。

这也是单一职责原则:“......每个类都应该有单一职责,并且该职责应该完全由类封装”

其背后的原因是:循环依赖意味着对任何事物的任何更改都会影响一切

单一责任原则的另一个(为简洁而编辑)引用:“一个类或模块应该有一个,并且只有一个改变的理由。单一责任原则说,问题的实质外观方面实际上是两个独立的责任,因此应该在单独的类或模块中。将两个因不同原因在不同时间发生变化的事物结合起来是一个糟糕的设计。(强调我的)

最后,关注点分离的概念:“目标是设计系统,使功能可以独立于其他功能进行优化,使得一个功能的失败不会导致其他功能失败,并且总体上更容易理解,设计和管理复杂的相互依赖的系统。” (强调我的)


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

考虑一个网站的开发,“内容”团队必须在一些脚本(由“开发”团队创建)周围非常温和地放置他们的文字和格式、颜色和图片,就这样,否则一切都会中断。内容团队不希望看到任何脚本——他们不想学习如何编程,只是为了改变一些文字或调整图像。并且开发团队不想担心每一个小的视觉变化,由不知道如何编码的人所做的,都有可能破坏他们的东西。

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

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


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

如果该项目是一个中等大小的项目,我会更进一步,创建一个Renderable接口作为模型层。A Shapeimplements Renderable,因此不会知道或关心它是如何绘制的。并且无论绘图需要什么都不知道 a Shape

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

于 2014-07-03T04:50:46.960 回答
1

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

想象一下锁匠……他可以挑选 1000 种不同类型的锁具。我可以去商店,买任何锁,给他,他可以摘,因为他熟悉锁技术。

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

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

于 2014-07-04T22:30:19.117 回答
1

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

形状和圆形示例的目的是描述接口(期望如何与外部世界对话)和实现(期望如何表现)之间的区别。

您给出的示例的问题是它已被截断。当涉及更多形状时,它更有意义。

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

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

为什么 Circle 自己绘制而不是形状? 因为它知道怎么做

于 2014-07-05T01:12:35.907 回答
0

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

我希望这不是太多消化,但也许功能分析的教训是相关的。我离题了关于虚函数的集合理论含义。

设集合A的族具有性质 {x : P(x)}
设 A 是族A
的一个元素 设 A' 也是族A的一个元素

A 和 A' 可能属于以下三个类别之一。
(1) A 和 A' 等价
对于 A 的所有 a 元素,a 是 A' 的元素
AND 对于 ~A 的所有 b 元素,b 是 ~A' 的元素

(2) A 和 A' 相交
存在 A 的一个元素,其中 a 是 A' 的一个元素
也存在 A 的 b 个元素,其中 b 是 ~A' 的一个元素

(3) A 和 A' 不相交
不存在 A 的元素 a 也是 A' 的元素

WHERE ~X 指代所有不是集合 X 的元素的 x

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

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

在情况 (3) 中,我们将定义一个纯虚行为,因为 A 和 A' 的相似之处仅在于它们都是 A 族的成员,因此A 和 A' 的交集是空集,这意味着确实存在不存在 A 和 A' 的任何共同元素

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

(1) 函数需要抽象吗?(情况 1 否,情况 2 和 3 是) (2) 函数需要纯虚函数吗?(案例 1 和 2 否,案例 3 是)

在情况 2 中,它还取决于行为的必要信息保存在哪里——在基类中还是在派生类中。

您无法从 DISPLAY 渲染 SHAPE,除非 DISPLAY 查找不一定属于 SHAPE 定义的信息。因为 DISPLAY 无法查看从 SHAPE 派生的类型的定义,超出了为 SHAPE 定义的定义。因此,必须为派生类中的抽象函数定义任何依赖于派生类型中包含的信息的功能。

于 2014-07-04T20:45:03.787 回答