4

パブリック継承に関する「Effective C++」の部分を読んだ後、この質問は非常に興味深いと思います。すべての正方形長方形ですが、必ずしもその逆であるとは限りません。ただし、次のコードを検討してください。

void makeBigger(Rectangle& r) { 

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

} 

このコードは a に対してはまったく問題ありませんが、 に渡すとオブジェクトRectangleが壊れてしまいます- その辺が等しくなくなります。SquaremakeBigger


では、どうすればこれに対処できますか? この本は答えを提供していませんでしたが(まだ?)、これを修正する方法をいくつか考えています:

  1. 反対側も調整するクラスのオーバーライドsetWidth()setHeight()メソッド。Square

    欠点: コードの重複、不必要な の 2 つのメンバーSquare

  2. Squareから継承せず、それ自体であるためRectangleに - havesizeなどsetSize()

    欠点: 奇妙な - 正方形は結局のところ長方形です -Rectangle直角などの の機能を再利用するとよいでしょう。

  3. Rectangle(純粋な仮想デストラクタを指定して定義することにより) 抽象化し、正方形ではなく から継承する四角形を表す3 番目のクラスRectangleを作成します。これにより、上記の関数のシグネチャを次のように変更する必要があります。

    void makeBigger(NotSquare& r);

    余分なクラスがあることを除いて、欠点は見られません。


より良い方法はありますか?私は3番目のオプションに傾いています。

4

6 に答える 6

10

これは、OO 設計において誤って扱われることがわかっている重要な原則の 1 つです。Meyer 氏は、あなたが参照している本について議論する素晴らしい仕事をしています。

秘訣は、原則を具体的なユースケースに適用する必要があることを覚えておくことです。継承を使用するときは、そのオブジェクトをオブジェクトとして使用したい場合に「is a」関係がオブジェクトに適用されることが重要であることを覚えておいてください...したがって、正方形が長方形であるかどうかは、何をしようとしているかによって異なります将来的には長方形で。

長方形の幅と高さを個別に設定する場合、いいえ、正方形は (ソフトウェアのコンテキストでは) 数学的には長方形ではありません。したがって、基本オブジェクトで何をするかを検討する必要があります。

あなたが言及した具体的な例では、標準的な答えがあります。makeBigger を矩形の仮想メンバー関数にすると、クラスに適した方法でそれぞれをスケーリングできます。しかし、これは、四角形に適用されるすべての (パブリック) メソッドが四角形に適用される場合にのみ、OO の優れた設計となります。

それでは、これがこれまでの取り組みにどのように当てはまるか見てみましょう。

  1. この種のことは、製品コードでかなり頻繁に見られます。それ以外の場合は優れた設計のギャップを修正するのは、けちとしては許されますが、望ましくありません。しかし、構文的には正しいが意味的には正しくないコードになるため、これは問題です。コンパイルして何かをしますが、意味が正しくありません。長方形のベクトルを繰り返し処理していて、幅を 2 ​​倍、高さを 3 倍にスケーリングするとします。これは、正方形では意味がありません。したがって、「実行時エラーよりもコンパイル時エラーを優先する」という原則に違反しています。

  2. ここでは、コードを再利用するために継承を使用することを考えています。「継承は再利用するためではなく、再利用するために使う」という言葉があります。これが意味することは、継承を使用して、手動の rtti なしで oo コードをそのベース オブジェクトとして別の場所で再利用できるようにすることです。コードを再利用するためのメカニズムは他にもあることに注意してください。C++ では、関数型プログラミングと合成が含まれます。

    正方形と長方形がコードを共有している場合 (たとえば、直角であるという事実に基づいて面積を計算する場合)、合成によってこれを行うことができます (それぞれに共通のクラスが含まれています)。この些細な例では、名前空間レベルで提供される compute_area_for_rectangle(Shape* s){return s.GetHeight() * s.GetWidth());} などの関数を使用したほうがよいでしょう。

    したがって、Square と Rectangle の両方が基本クラスの Shape から継承され、Shape が次のパブリック メソッドを持つ場合: draw()、scale()、getArea() ...、これらはすべて、形状が何であれ意味的に意味があり、一般的な式は名前空間レベルの関数を介して共有されます。

  3. この点について少し熟考すると、3 番目の提案には多くの欠陥があることがわかると思います。

    oo 設計の観点について: icbytes が述べたように、3 番目のクラスを作成する場合、このクラスが共通の用途を意味のある形で表現する共通のベースであることがより理にかなっています。形は大丈夫です。主な目的がオブジェクトを描画することである場合、Drawable は別の良いアイデアかもしれません。

    あなたがアイデアを表現した方法には、他にもいくつかの欠陥があります。これは、仮想デストラクタの誤解と、抽象的であることの意味を示している可能性があります。別のクラスがオーバーライドできるようにクラス virtual のメソッドを作成するときはいつでも、デストラクタ virtual も宣言する必要があります (SM はこれについてEffective C++ で説明しているので、自分でこれを見つけると思います)。これはそれを抽象化するものではありません。メソッドの少なくとも 1 つを純粋に仮想として宣言すると、抽象化されます。つまり、実装がありません
    。 virtual void foo() = 0; // たとえばこれは、問題のクラスをインスタンス化できないことを意味します。明らかに、少なくとも 1 つの仮想メソッドがあるため、デストラクタも virtual と宣言する必要があります。

それが役立つことを願っています。継承は、コードを再利用できる唯一の方法であることに注意してください。良いデザインは、すべての方法の最適な組み合わせから生まれます。

さらに読むには、Sutter と Alexandrescu の「C++ Coding Standards」、特にクラスの設計と継承に関するセクションを強くお勧めします。項目34「継承より合成を優先」と項目37「公開継承は代用性。継承する、再利用するのではなく、再利用する。

于 2013-09-19T08:29:49.730 に答える
5

簡単な解決策は次のとおりです。

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

正方形で完全に機能し、その場合でも長方形を正しく返します。

[編集] コメントは、本当の問題は根本的な呼び出しにあることを指摘していますsetWidth。これは同じ方法で修正できます。

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

繰り返しますが、正方形の幅を変更すると長方形になります。そして、const示されているようにRectangle、既存の Rectangle を変更せずに新しいものを提供します。以前の関数はさらに簡単になりました。

Rectangle makeBigger(Rectangle const& r)
{
    return r.setWidth(r.width() + 10); 
}
于 2013-09-19T08:34:58.310 に答える
0

余分なクラスを持つことを除いて、3 番目のソリューション ( Factor out モディファイアとも呼ばれます) の重大な欠点はありません。私が考えることができるのは次のとおりです。

  • たとえばHalfSquareと呼ばれる、一方のエッジが他方の半分である派生Rectangleクラスがあるとします。次に、3 番目のソリューションによると、NotHalfSaquare という名前のクラスをもう 1 つ定義する必要があります。

  • より多くのクラスを導入する必要がある場合は、Rectangle、Square、および HalfSquare の両方が派生する Shape クラスにします。

于 2013-09-19T07:53:50.897 に答える
0

「すべての正方形は長方形だから」とあなたは言いますが、ここに問題があります。有名なボブ・マーティンの引用の言い換え:

オブジェクト間の関係は、代表者によって共有されません。

(元の説明はこちら: http://blog.bignerdranch.com/1674-what-is-the-liskov-substitution-principle/ )

したがって、確かにすべての正方形は長方形ですが、これは、正方形を表すクラス/オブジェクトが長方形を表すクラス/オブジェクト「である」という意味ではありません。

最も一般的な現実世界の、あまり抽象的ではなく直感的な例は次のとおりです。2 人の弁護士が離婚の文脈で夫と妻を代表して法廷で争う場合、弁護士は離婚中に人々を代表し、現在結婚しているにもかかわらず、彼らは自身は結婚しておらず、離婚もしていません。

于 2013-09-19T15:32:21.523 に答える
0

にしたい場合は、パブリックに継承する必要がありますSquare Rectangleただし、これは、 で機能するすべてのパブリック メソッドRectangleが に適切に特化されている必要があることを意味しますSquare。この文脈では

void makeBigger(Rectangle& r)

スタンドアロン関数ではなく、(独自のものを提供することによって) オーバーライドされるか 、 (セクションによって) 非表示になる仮想メンバーでRectangleあってはなりません。Squareusing makeBiggerprivate


Rectangleでできることが でできないという問題についてSquare。これは一般的な設計上のジレンマであり、C++ は設計に関するものではありません。誰かがRectangle実際には であるへの参照 (またはポインタ) を持っSquareていて、 に対して意味のない操作を実行したい場合はSquare、それに対処する必要があります。いくつかのオプションがあります:

1 公開継承を使用しSquare、実行できない操作が試行された場合に例外をスローします。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"); }
};

change_width()またはchange_height()インターフェイスの不可欠な部分である場合、これは本当に扱いにくく、適切ではありません。このような場合は、次のことを考慮してください。

2 1 つclass Rectangle(たまたま正方形である可能性があります) と、オプションで、に変換 ( )class Squareできる別のものを使用できます。したがって、長方形として機能しますが、のように変更することはできません。static_cast<Rectangle>(square)RectangleRectangle

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

Rectangleに変更できるの場合、このオプションは正しい選択ですSquare。つまり、コードに実装されているように(幅と高さを個別に変更可能) 、Square が でない場合。 ただし、は に静的にキャストできるため、引数を取る関数は . で呼び出すこともできます。RectangleSquareRectangleRectangleSquare

于 2013-09-19T07:47:02.303 に答える
-3

私の考え: Shape と呼ばれるスーパークラスがあります。Square は Shape を継承しています。メソッド resize(int size ) があります。Rectangle は ClassRectangle であり、Shape から継承していますが、インターフェイス IRecangle を実装しています。IRectangle にはメソッド resize_rect(int sizex, int size y ) があります。

C++ では、いわゆる純粋仮想メソッドを使用してインターフェイスが作成されます。C# のように完全に実装されているわけではありませんが、私にとっては、これは 3 番目のオプションよりも優れたソリューションです。ご意見はありますか?

于 2013-09-19T07:46:41.313 に答える