Should the Square class publicly inherit from the Rectangle class?

I find this question very interesting after reading the part of Effective C ++ on public inheritance. It would have been wise for me to say yes, because each square is a rectangle , but not necessarily in a different way. However, consider this code:

void makeBigger(Rectangle& r) { r.setWidth(r.width() + 10); } 

This code is great for Rectangle , but the Square object will be broken if we pass it to makeBigger - its sides will become unequal.


So how can I handle this? There was no answer in the book (yet?), But I am thinking of several ways to do this:

  • Override the setWidth() and setHeight() methods in the Square class to adjust the other side.

    Disadvantage: code duplication, unnecessary 2 members of Square .

  • For Square do not inherit from Rectangle and be independent - have size , setSize() , etc.

    Disadvantage: weird squares - these are, after all, rectangles - it would be useful to reuse Rectangle functions such as right angles, etc.

  • Make a Rectangle abstract (giving it a clean virtual destructor and defining it) and get a third class that represents rectangles that are not squares and inherit from Rectangle . This will force us to change the above function signature:

    void makeBigger(NotSquare& r);

    It is impossible to see the flaws, except for the presence of an additional class.


Is there a better way? I am inclined to the third option.

+4
source share
6 answers

This is one of the key principles in the design of OO, which, it seems to me, is not processed correctly. Mr. Meyer does an excellent job of discussing this book that you are talking about.

The trick is to remember that principles should apply to specific use cases. When using inheritance, remember that the key is that the "is" relation refers to the object when you want to use this object as ... So, regardless of whether the square is a rectangle or not, it depends on what you are going to do with rectangles in the future.

If you set the width and height of the rectangle independently, then no, the square is not a rectangle (in the context of your software), although it is mathematically. Therefore, you must consider what you will do with the base objects.

In the specific example you mentioned, there is a canonical answer. If makeBigger makes a virtual member function of the rectangle, then each of them can be scaled in a way that suits the class. But this is only a good OO design if all square (public) methods are applied to the rectangle.

So, let's see how this relates to your efforts so far:

  • I often see this in production code. It is excusable, like kludge, to correct a gap in an otherwise good design, but it is undesirable. But this is a problem, because it leads to syntactically correct code, but is semantically incorrect. It will compile and do something, but the value is incorrect. Suppose you repeat the vector of rectangles, and you scale the width by 2, and the height by 3. This is semantically meaningless for a square. Thus, it violates the preference "prefer compile-time errors to run-time errors."

  • Here you are thinking of using inheritance to reuse code. It says "use inheritance for reuse, not reuse." This means that you want to use inheritance to make sure that the oo code can be reused elsewhere as its base object, without any manual rtti. Remember that there are other mechanisms for reusing code: in C ++, they include functional programming and composition.

    If the square and the rectangles have a common code (for example, they calculate the area based on the fact that they have right angles), you can do this by composition (each of them contains a common class). In this trivial example, you will probably be better off working with a function, for example: compute_area_for_rectangle (Shape * s) {return s.GetHeight () * s.GetWidth ());} provided at the namespace level.

    So, if both squares and the Rectangle are inherited from the base class Shape, Shape has the following public methods: draw (), scale (), getArea () ..., all of them would be semantically significant for any shape and general formulas can be shared via functions namespace level.

  • I think that if you ponder this question a little, you will find a number of flaws with your third sentence.

    Regarding the design point oo: as mentioned icbytes, if you have a third class, it is more reasonable that this class is a common base that meaningfully expresses common usage. The form is approved. If the main goal is to draw objects than Drawable, it might be another good idea.

    There are a couple of other flaws in how you expressed the idea of ​​what might indicate a misunderstanding on the part of virtual destructors and what it means to be abstract. Whenever you make a class method virtual so that another class can override it, you must also declare the destructor virtual (SM discusses this in Effective C ++, so I think you will find it yourself). This does not make it abstract. It becomes abstract when you declare at least one of the methods purely virtual - i.e. Having no implementation virtual void foo () = 0; // for example This means that this class cannot be created. Obviously, since it has at least one virtual method, it must also have a destructor declared virtual.

Hope this helps. Keep in mind that inheritance is only one method by which code can be reused. A good design comes out of the optimal combination of all methods.

For further reading, I highly recommend Sutter and Alexandrescu "C ++ Coding Standards", especially the section "Design and Class Inheritance". Paragraphs 34 “Prefer composition for inheritance” and 37 “Inheritance of inheritance is interchangeable. Inherit, not repeat, but reuse.

+8
source

It turns out that an easier solution

 Rectangle makeBigger(Rectangle r) { r.setWidth(r.width() + 10); return r; } 

Works well on squares and correctly returns a rectangle even in this case.

[edit] The comments indicate that the real challenge is the main setWidth call. This can be fixed in the same way:

 Rectangle Rectangle::setWidth(int newWidth) { Rectangle r(*this); r.m_width = newWidth; return r; } 

Again, changing the width of the square gives you a rectangle. The previous function is now even easier:

 Rectangle makeBigger(Rectangle const& r) { return r.setWidth(r.width() + 10); } 
+4
source

If you want your Square be a Rectangle , it must publicly inherit it. However, this means that any public methods that work with Rectangle must be appropriately specialized for Square . In this context

 void makeBigger(Rectangle& r) 

It should not be a standalone function, but a virtual Rectangle member that is redefined in Square (by providing its own) or hidden (via using makeBigger in the private section).


Regarding the question that some of the things you can do with a Rectangle cannot be done with Square . This is a common design dilemma, and C ++ is not design. If someone has a link (or pointer) to a Rectangle , which is actually a Square , and you want to perform an operation that does not make sense for Square , then you should handle it. There are several options:

1 use public inheritance and throw a Square exception if the operation is running, which is impossible for Square

 struct Rectangle { double width,height; virtual void re_scale(double factor) { width*=factor; height*=factor; } virtual void change_width(double new_width) // makes no sense for a square { width=new_width; } virtual void change_height(double new_height) // makes no sense for a square { height=new_height; } }; struct Square : Rectangle { double side; void re_scale(double factor) { side *= factor; } // fine void change_width(double) { throw std::logic_error("cannot change width for Sqaure"); } virtual void change_height(double) { throw std::logic_error("cannot change height for Sqaure"); } }; 

This is really inconvenient and not suitable if change_width() or change_height() are integral parts of the interface. In this case, consider the following.

2 you can have one class Rectangle (which may turn out to be square) and, optionally, a separate class Square that can be converted ( static_cast<Rectangle>(square) ) to Rectangle and therefore act like a rectangle, but you should not change how a Rectangle

 struct Rectangle { double width,height; bool is_square() const { return width==height; } Rectangle(double w, double h) : width(w), height(h) {} }; // if you still want a separate class, you can have it but it not a Rectangle // though it can be made convertible to one struct Square { double size; Square(Rectangle r) : size(r.width) // you may not want this throwing constructor { assert(r.is_square()); } operator Rectangle() const // conversion to Rectangle { return Rectangle(size,size); } }; 

This option is the right choice if you allow changes to the Rectangle that can turn it into Square . In other words, if your Square not a Rectangle , as it is implemented in your code (with independently adjustable width and height). However, since Square can be statically added to a Rectangle , any function that takes a Rectangle argument can also be called using Square .

0
source

Except that you have an extra class, there are no serious flaws in your third solution (the so-called Factor from modifiers ). The only thing I can think of:

  • Suppose I have a Rectangle derived class with one edge that is half the other called, for example, HalfSquare. Then, according to your third solution, I would have to define another class called NotHalfSaquare.

  • If you need to introduce a more class, then let it be rather the class Shape and Rectangle, Square and HalfSquare, obtained from

0
source

You say, "because every square is a rectangle," and this is where the problem lies. Paraphrase of the famous Bob Martin quote:

Relations between objects are not shared by their representatives.

(original explanation here: http://blog.bignerdranch.com/1674-what-is-the-liskov-substitution-principle/ )

Thus, each square is a rectangle, but this does not mean that the class / object representing the square is a "class / object representing the rectangle."

The most common real, less abstract and intuitive example: if two lawyers fight in court, representing the husband and wife in the context of a divorce, then, despite the fact that the lawyers represent people during the divorce and are currently married, they are not married and are not at the time of the divorce.

0
source

My idea: You have a superclass called Shape. Area inherits from Shape. It has a resize method (int size). Rectangle is a Rectangle class that inherits from Shape but implements the IRecangle interface. IRectangle has a resize_rect (int sizex, int size y) method.

C ++ interfaces create the use of so-called pure virtual methods. This is not well implemented, as in C #, but for me it is even a better solution than the third option. Any opinions?

-3
source

Source: https://habr.com/ru/post/1502973/


All Articles