问题描述:

I find this question very interesting after reading the part of "Effective C++" about public inheritance. Before it would be common sense for me to say yes, because every square is a rectangle, but not necessarily other way around. However consider this code:

void makeBigger(Rectangle& r) {

r.setWidth(r.width() + 10);

}

This code is perfectly fine for a Rectangle, but would break the Square object if we passed it to makeBigger - its sides would become unequal.


So how can I deal with this? The book didn't provide an answer (yet?), but I'm thinking of a couple of ways of fixing this:

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

    Drawback: code duplication, unnecessary 2 members of Square.

  2. For Square not to inherit from Rectangle and be on its own - have size, setSize() etc.

    Drawback: weird - squares are rectangles after all - it would be nice to reuse Rectangle's features such as right angles etc.

  3. Make Rectangle abstract (by giving it a pure virtual destructor and defining it) and have a third class that represents rectangles that are not squares and inherits from Rectangle. That will force us to change the above function's signature to this:

    void makeBigger(NotSquare& r);

    Can't see any drawbacks except having an extra class.


Is there a better way? I'm leaning towards the third option.

网友答案:

This is one of the key principles in OO design that I find gets handled incorrectly. Mr Meyer does an excellent job of of discussing it the book you are referring to.

The trick is to remember that the principles must be applied to concrete use cases. When using inheritence, remember that the key is that the "is a" relationship applies to an object when you want to use that object as a ... So whether a square is a rectangle or not depends on what you are going to be doing with rectangles in the future.

If you will be setting width and height of a rectangle independently, then no, a square is not a rectangle (in the context of your software) although it is mathematically. Thus you have to consider what you will be doing with your base objects.

In the concrete example you mention, there is a canonical answer. If you make makeBigger a virtual member function of rectangle, then each one can be scaled in a way that is appropriate to a class. But this is only good OO design if all the (public) methods which apply to a rectangle will apply to a square.

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

  1. I see this kind of thing in production code pretty often. It's excusable as a kludge to fix a gap in an otherwise good design, but it is not desirable. But it's a problem because it leads to code which is syntactically correct, but semantically incorrect. It will compile, and do something, but the meaning is incorrect. Lets say you are iterating over a 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 precept "prefer compile time errors to runtime errors".

  2. Here you are thinking of using inheritance in order to re-use code. There's a saying "use inheritance to be re-used, not to re-use". What this means is, you want to use inheritance to make sure the oo code can be re-used elsewhere, as its base object, without any manual rtti. Remember that there other mechanisms for code re-use: in C++ these include functional programming and composition.

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

    So if both Square and Rectangle inherit from a base class Shape, Shape having the following public methods: draw(), scale(), getArea() ..., all of these would be semantically meaningful for whatever shape, and common formulas could be shared via namespace level functions.

  3. I think if you meditate on this point a little, you'll find a number of flaws with your third suggestion.

    Regarding the oo design perspective: as icbytes mentioned, if you're going to have a third class, it makes more sense that this class be a common base that meaningfully expresses the common uses. Shape is ok. If the main purpose is to draw the objects than Drawable might be another good idea.

    There are a couple other flaws in the way you expressed the idea, which may indicate a misunderstanding on your part of virtual destructors, and what it means to be abstract. Whenever you make a method of a class virtual so that another class may override it, you should declare the destructor virtual as well (S.M. does discuss this in Effective C++, so I guess you would find this out on your own). 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 the class in question cannot be instantiated. Obviously since it has at least one virtual method, it should also have the destructor declared virtual.

I hope that helps. Keep in mind that inheritence is only one method by which code can be re-used. Good design comes out of the optimal combination of all methods.

For further reading I highly recommend Sutter and Alexandrescu's "C++ Coding Standards", especially the section on Class Design and Inheritence. Items 34 "Prefer composition to inheritence" and 37 "Public inheritence is substitutability. Inherit, not to reuse, but to be reused.

网友答案:

It turns out the easier solution is

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

Works perfectly well on squares, and correctly returns a rectangle even in that case.

[edit] The comments point out that the real problem is the underlying call to setWidth. That 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 a square gives you a rectangle. The previous function now becomes even easier:

Rectangle makeBigger(Rectangle const& r)
{
    return r.setWidth(r.width() + 10); 
}
网友答案:

Except from having an extra class there are no serious drawbacks of your 3rd solution (called also Factor out modifiers). The only I can think of are:

  • Suppose I have a derived Rectangle class with one edge being a half of the other, called for example HalfSquare. Then according to your 3rd solution I'd have to define one more class, called NotHalfSaquare.

  • If you have to introduce on more class then let it be rather Shape class both Rectangle, Square and HalfSquare derive from

网友答案:

If you want your Square to be a Rectangle, it should publicly inherit from it. However, this implies that any public methods that work with a Rectangle must be appropriately specialised for a Square. In this context

void makeBigger(Rectangle& r)

should not be a standalone function but a virtual member of Rectangle which in Square is overridden (by providing its own) or hidden (by using makeBigger in the private section).


Regarding the issue that some things you can to do a Rectangle cannot be done to a Square. This is a general design dilemma and C++ is not about design. If somebody has a reference (or pointer) to a Rectangle that actually is a Square and want to do an operation that makes no sense for a Square, then you must deal with that. There are several options:

1 use public inheritance and make Square throw an exception if an operation is attempted that is not possible for a 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 really is awkward and not appropriate if change_width() or change_height() are integral parts of the interface. In such a case, consider the following.

2 you can have one class Rectangle (which may happen to be square) and, optionally, a separate class Square that can be converted (static_cast<Rectangle>(square)) to a Rectangle and hence act as a Rectangle, but not be modified like 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's 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 correct choice if you allow for changes to the Rectangle that can turn it into a Square. In other words, if your Square is not a Rectangle, as implemented in your code (with independently modifiable width and height). However, since Square can be statically cast to a Rectangle, any function taking an Rectangle argument can also be called with a Square.

网友答案:

You say: "because every square is a rectangle" and here the problem lies exactly. Paraphrase of famous Bob Martin's quote:

The relationships between objects are not shared by their representatives.

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

So surely every square is a rectangle, but this doesn't mean that a class/object representing a square "is a" class/object representing a rectangle.

The most common real-world, less abstract and intuitive example is: if two lawyers struggle in the court representing a husband and a wife in the context of a divorce, then despite the lawyers are representing the people during a divorce and being currently married they are not married themselves and are not during a divorce.

网友答案:

My idea: You have a superclass, called Shape. Square inherits from Shape. It has the method resize(int size ). A Rectangle is ClassRectangle, inheriting from Shape but implementing interface IRecangle. IRectangle has method resize_rect(int sizex, int size y ).

In C++ interfaces are created by the usage of so called pure virtual methods. It is not fully well implemented like in c# but for me this is even better solution than third option. Any opinions ?

相关阅读:
Top