When I read about inheritance I'm always confused about a certain example.

Usually there's an example similar to the example below.

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(); 

My question is, why is it the Shape's responsibility to draw itself? Shouldn't it be the responsibility of a renderer class to know how to draw a shape and instead provide the shape to the renderer? What if we wanted to record changes in dimensions? Etc? Would we have a method for each of these different tasks inside of Shape?

Seeing numerous examples like these sometimes make me wonder about my ability to assign responsibilities to classes. Is there something I'm not understanding about classes only having one responsibility?

有帮助吗?

解决方案 4

A Shape should have no knowledge about how it's drawn. The larger the project being designed, the more critical this decision is.

For me, it all boils down to circular dependencies which, in all but the edgiest of cases, causes nothing but headaches.

A basic tenet of Model View Controller is that the things that you do (the verbs, or "view") are clearly separated from the things (the nouns, or "controller") that are being manipulated or analyzed: Separation of presentation and logic. The "model" is the middle man.

It's also the single responsibility principle: "...every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class"

The reason behind it is this: A circular dependency means that any change to anything, affects everything.

Another (edited-for-brevity) quote from the single responsibility principle: "A class or module should have one, and only one, reason to change. The single responsibility principle says that substantive and cosmetic aspects of a problem are really two separate responsibilities, and should therefore be in separate classes or modules. It would be a bad design to couple two things that change for different reasons at different times." (emphasis mine)

Finally, the concept of separation of concerns: "The goal is to design systems so that functions can be optimized independently of other functions, so that failure of one function does not cause other functions to fail, and in general to make it easier to understand, design and manage complex interdependent systems." (emphasis mine)


This is not just a programming-design issue.

Consider the development of a website, where the "content" team has to place their words and formatting and colors and pictures, very gently around some scripting (as created by the "development" team), just so, or everything breaks. The content team wishes to see no scripting at all--they don't want to have to learn how to program, just so they can change some words or tweak an image. And the development team doesn't want to have to worry that every minor visual change, made by people who don't know how to code, has the potential to break their stuff.

I think about this concept every day when working on my own projects. When two source files import each other, a change in either one requires that both be re-compiled--and at the same time. With larger projects, this could mean a trivial change requires the re-compilation of hundreds or thousands of classes. In the three major projects I'm currently involved in, in which there are about a thousand different source-code files, there is exactly one circular dependency of this kind.

Whether teams in a buisiness, source-code files, or designing programming objects, circular dependencies is something I recommend avoiding unless absolutely necessary.


So, at least, I would not put the draw function in Shape. Although very dependant on the type and size of the project being designed, the rendering could be done by a RenderingUtils class, containing nothing but public static functions that do the bulk of the work.

Were the project a moderately large one, I would go further and create a Renderable interface as the model layer. A Shape implements Renderable, and would therefore not know or care how it's drawn. And whatever does the drawing needs to know nothing about a Shape.

This gives you the flexibility to completely change how the rendering is done, without affecting (or having to recompile!) Shapes, and it also allows you to render something wildly different from a Shape, without having to change the drawing code.

其他提示

I often find that these simple textbook examples fail to explain the reason sufficiently because they are overly simplistic. There are lots of things we could give a Shape class the responsibility of doing: drawing itself, computing its area, working out whether a given point lies within its bounds, working out what shape result from the intersection of another shape, remembering how many people count it as their favourite shape... the list is only as long as your imagination and which responsibilities you give it depends on the goals of your program and how you choose to build it.

Assuming you want to be able to draw the shapes in a generic, polymorphic way consider how you might actually implement that. Onto what exactly will the shape be drawn? Will the shape know the canvas works? Should it know that it needs to pick up a paintbrush, dip it in some paint and then draw itself? Should it know how your display driver works? Set bits to turn on pixels in the right place so that your monitor shows the right shape?

Clearly going down to this level is giving the shape far too much responsibility, so instead you define a set of graphical primitives (e.g.: points and lines) and build a graphics API that can render these. A Shape can then use the primitives to tell the API what to draw. The graphics API doesn't know it's drawing a square, but by telling it to draw four lines hey presto it's draw a square. All this leaves a Shape with the single responsibility of knowing its points and the lines that define it.

It's always difficult to see the benefits of certain design patterns when you take classes in isolation and that is because building software is about getting things to work together; nothing ever works in isolation.

The choice to make Draw() a method of the base class depends on the context--the specific problem being solved. To make the problem slightly more clear here is another example which I regularly used while interviewing for OO skills.

Imagine a Document class and Printer class. Where should the print function go? There are two obvious choices:

document.print(Printer &p);

or

printer.print(Document &d);

Which is the Right one? The answer is: it depends on where you want polymorphic behavior--in the document or in the printer. If we assume that all printers have identical functionality (a myth operating systems attempt to promote), the clearly the polymorphic behavior should be in the Document object. However, if we imagine that all documents are approximately the same (or at least the ones we care about) and that printers are vastly different (this used to be the case--consider: plotters, line printers, laser printers, daisy wheel printers, etc.) then it makes more sense to let the printer decide how best to render the document.

One could argue that Print() should be part of neither object since polymorphic behavior may be desire from a combination of both printer and document. In this case you need double dispatch.

OOP promotes sending messages, on the contrary of procedural code that "asks" for some external data and then process.

If you place the draw method in the renderer, you would break the encapsulation of the class Shape, since it certainly would need to access its internals (like coordinates (x,y) etc..).

By letting Shape draw "itself", you keep encapsulation promoting flexibility regarding internal changes.

The solution really depends of the complexity.
By extracting the draw method from Shape, your shape would need to expose its data.
By keeping it, you keep encapsulation.

Thus, if your draw is complex, prefer considering it as another whole responsibility carried by a Renderer or Graphics, thus corresponding to your suggestion.

Only an object truly knows how to draw itself.

Imagine a locksmith... hes can pick 1000 different lock types. I can go to the store, buy any lock and give it too him and he can pick it, because he is familiar with lock-tech.

Now imagine I am an inventor and I start making my own locks, unique in design, revolutionary. Is he going to be able to open them? Perhaps, but perhaps not... It depends what I did in side in the lock,.. am I using tech he/she is aware of, etc?

Your shape objects are the same,... depending how they are implemented inside determines whether they can be rendered by some generic render engine or not. If you ask each object to draw itself then you do not have to worry about this.

The above answers seem entirely over-complicated to me.

The purpose of the shape and circle example is to delineate the difference between interface (how it's expected to talk to the outside world) and implementation (how it's expected to behave.)

The problem with the example you gave is that it's been truncated. It makes more sense when there are more shapes involved.

Consider the case of having a circle, a triangle, and a rectangle. Now how is Shape going to draw itself? It doesn't know which kind it is, or as such, what to do.

Now consider a container of shapes. They all have a draw method; the parent class enforces that. So, you can have a container of shapes that's homogenous, even though the implementations of their various drawing methods are essentially unrelated.

Why does Circle draw itself, rather than shape? Because it knows how to.

Pure virtual functions are used for when the behavior of the algorithm isn't clearly defined for a set, but the existence of the algorithm is clearly defined for a set.

I hope this isn't too much to digest, but perhaps a lesson from functional analysis is relevant. I digress about the set theoretical implications of virtual functions.

Let the family of sets A have the property {x : P(x)}
Let A be an element of the family A
Let A' also be an element of the family A

A and A' could fall under one of the following three catagories.
(1) A and A' are equivalent
For all a elements of A, a is an element of A'
AND for all b elements of ~A, b is an element of ~A'

(2) A and A' intersect
There exists a elements of A where a is an element of A'
There also exists b elements of A where b is an element of ~A'

(3) A and A' are disjoint
There does not exist an element a of A that is also an element of A'

WHERE ~X refers to all x that are not elements of the set X

In case (1) we would define a non-abstract behavior iff U being an element of the family A implies the existence of a single value u such that u = P(U) for all U that are elements of the family A

In case (2) we would define a virtual behavior if U being an element of the family A implies the existence of a single value u such that u = P(U') where U' is a subset of U.

And in case (3) we would define a pure virtual behavior because A and A' are only similar in that they are both members of the family A, such that the intersection of A and A' is the empty set, implying that there does not exist any common elements of A and A'

Think about what the syntax means in terms of the logical definitions and you'll be able to answer the following:

(1) Does the function need to be abstract? (no for case 1, yes for case 2 and 3) (2) Does the function need to be pure virtual? (no for case 1 and 2, yes for case 3)

In case 2, it also depends on where the behavior's necessary information is being held -- in the base class or in the derived class.

You can't render a SHAPE from a DISPLAY, without the DISPLAY looking for information that isn't necessarily part of the definition of SHAPE. Inasmuch as DISPLAY can't see into the definition of a type derived from SHAPE, beyond what is defined for SHAPE. So any functionality that depends on information contained in a derived type must be defined for the abstracted function within the derived class.

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top