web-dev-qa-db-ja.com

このコードは、四角形/長方形のリスコフ置換原理の例を解決しますか?

私はLSPを正しく理解していて、それを解決できることを確認したかっただけです。私は古典的な長方形/正方形の問題を取り、解決策を試みています:

class Rectangle{
    public $width;
    public $height;

    function setWidth($width){
        $this->width = $width;
    }

    function setHeight($height){
        $this->height = $height;
    }
}

class Square extends Rectangle{

    function setWidth($width){
        $this->width = $width;
        $this->height = $width;
    }

    function setHeight($height){
        $this->height = $height;
        $this->width = $height;
    }
}

次のようなコードがある場合:

function changeSize(Rectangle $rect){
  $rect->setWidth(10);
  $rect->setHeight(30);
  $this->assertEquals(10,$rect->width);
  $this->assertEquals(30,$rect->height);
}

すると、四角形は親クラスに制約を導入するため、四角形と四角形は交換可能ではありません。したがって、正方形は長方形から継承すべきではありません。

しかし、確かに、正方形と長方形の両方が4辺の形であることに同意できますか?これは、この前提に基づいた、私の提案するソリューションです。

abstract class AFourSidedShape{
    public $width;
    public $height;

    abstract public function __construct($width,$height);

    public function scaleUp($percentage){
        $this->height = $this->height + (($this->height / 100) * $percentage);
        $this->width = $this->width + (($this->width / 100) * $percentage);
    }

    public function scaleDown($percentage){
        $this->height = $this->height - (($this->height / 100) * $percentage);
        $this->width = $this->width - (($this->width / 100) * $percentage);
    }
}

class Rectangle extends AFourSidedShape{
    function __construct($width, $height){
        $this->width = $width;
        $this->height = $height;
    }
}

class Square extends AFourSidedShape{
    function __construct($width, $height){
        if($width != $height){
            throw new InvalidArgumentException('Sides must be equal');
        }else{
            $this->width = $width;
            $this->height = $height;
        }
    }
}

クライアントコードを次のように変更する必要があります。

function changeSize(AFourSidedShape $shape){
  $origWidth = $shape->width;
  $origHeight = $shape->height;
  $shape->scaleUp(10);
  $this->assertEquals($origWidth + (($origWidth/100) * 10),$shape->width);
  $this->assertEquals($origHeight + (($origHeight/100) * 10),$shape->height);
}

私の理論は次のとおりです。四角形と四角形は実際には両方とも四辺形なので、四辺形の抽象クラスからの継承に問題はないはずです。正方形がコンストラクターに追加の制約を追加している間(つまり、辺が等しくない場合はエラーをスローします)、抽象親クラスにコンストラクターを実装していないため、これは問題にならず、クライアントコードとにかくそれに渡すことができる/できないものについての仮定を行うべきではありません。

私の質問は次のとおりです。LSPを理解しましたか。この新しい設計は、四角形/長方形のLSP問題を解決しますか?

提案されているようにインターフェースを使用する場合:

interface AFourSidedShape{
    public function setWidth($width);
    public function setHeight($height);
    public function getWidth();
    public function getHeight();
}

class Rectangle implements AFourSidedShape{
    private $width;
    private $height;

    public function __construct($width,$height){
        $this->width = $width;
        $this->height = $height;
    }

    public function setWidth($width){
        $this->width = $width;
    }

    public function setHeight($height){
        $this->height = $height;
    }

    //getwidth, getheight
}

class Square implements AFourSidedShape{
    private $width;
    private $height;

    public function __construct($sideLength){
        $this->width = $sideLength;
        $this->height = $sideLength;
    }

    public function setWidth($width){
        $this->width = $width;
        $this->height = $width;
    }

    public function setHeight($height){
        $this->height = $height;
        $this->width = $height;
    }

    //getwidth, getheight
}
6
user1578653

LSPウィキペディアの記事の " Typical Violation "セクションを解決しようとしていると想定します。それが事実である場合、あなたはそれを解決しておらず、セクションはその理由を明確に述べています。具体的には、「幅と高さの両方にゲッターメソッドとセッターメソッドが存在すると想定して、Rectangleクラスから派生する正方形クラス」の設定から始めます。 LSPは、サブクラスがスーパークラスに代わることができるべきであると述べています。

しかし、それだけではありません!それはあなたとあなたのコメンターが欠けているものです。スワップができるようになるまで(すべてがAFourSidedShapeである)設計を拷問するだけの場合は、単純にいくつかの汎用オブジェクト(形状や動作はありません)を設計し、すべてをオブジェクトから継承させることができます。次に、特定の実装を入れ替えます。それをしたらどうなるか考えてみてください。あなたは常にオブジェクトに問い合わせて、オブジェクトが実行できることを決定したり、投稿の条件に違反したりしています。それが、LSPが嫌うものです。

そのため、あなたの場合、AFourSidedShapeは実際には何も解決しません。ポストの条件が正しいことを確認するために特定の実装が四角形または四角形であるかどうかを常にチェックしているためです。四角形の幅は、更新時に魔法のように変化する可能性がありますその高さは四角形ではありません。 「これらのメソッドは、Rectangleセッターの事後条件を弱めます(違反します)。理想的とは言えない振る舞いから逃れることはできません。それはトレードオフです。 (オブジェクトを不変にしない限り、不変性を保証します!)

気分を悪くしないでください。幅/高さの問題に対するgetter/setterを備えたRectangle/Squareは、簡単な意味では解決できないことを意味しています。これは、LSPが実際に困難である理由の簡単な例です。

21
Scant Roger

LSPはクラスのコントラクトに関するものであり、その継承されたクラスは基本クラスと同じコントラクトを満たす必要があります。コード内のインターフェイスは、通常、そのコントラクトのpartsのみを定義します。主に構文上の部分であり、セマンティクスは、メソッドまたはパラメーターの説明的な名前によって部分的に与えられる場合があります。契約の他の部分は、多くの場合、「アサーションステートメント」を追加するか、特定の言語機能を利用することによって、コメントで単に定義されるか、ユニットテストにエンコードされる場合があります。

したがって、LSP違反を本当に解決したかどうかは、インターフェースの完全なセマンティックコントラクトに依存します。契約が次のようになっている場合(これは、より明白な動作である私見です):

// contract: a four sided shape is an object with two individual, independent
// properties "width" and "height"
interface AFourSidedShape{
    public function setWidth($width);
    public function setHeight($height);
    public function getWidth();
    public function getHeight();
}

その場合、Squareクラスは基本クラスと同じ規約を満たさないため、LSPに違反します。 「ポスト条件」に関してより正式にそれを書くことができ、setHeightを呼び出すたびにgetWidthの値が変化しないことをチェックし、逆も同様です。

ただし、セマンティックコントラクトが次のようになっている場合:

 // contract: a four sided shape is an object with two 
 // properties "width" and "height" which must not 
 // be assumed to be independent; maybe changing one can change the other
 interface AFourSidedShape{
     // ...
 }

その後、LSP違反はなくなります。あなたの質問とコンストラクターの制約を記述する方法から、それはあなたが考えている契約だと思います。ただし、後者は 最小驚きの原則 に違反する可能性があります。これは、一部のオブジェクトでは別の値を変更し、他のオブジェクトでは変更しない「セッター」であり、クラスの平均的なユーザーに気が遠くなる可能性があります。 。 LSPに従うかどうかに関係なく、そのようなセッターを共通インターフェースに配置することは避けてください。

サイドノート:もちろん、元の正方形/長方形の問題は、 SquareがRectangleから派生する実装、またはLSPに違反しない直接その逆の実装。」このように読み取ると、@ ScantRogerは正しくなり、設計は別の問題を解決します。しかし、LSPに違反せずに一般的な方法で長方形と正方形を処理する継承を組み込む方法があるため、この問題の答えは「はい、あなたの提案がその問題を解決します」です。

10
Doc Brown

実際には、それは何度も解決されています。

Inscake、Photoshop、The Gimp、Illustratorなどのようなプログラムでは、パレットに個別の長方形ツールと正方形ツールはありません。長方形と正方形を描画するためのツールは1つだけです。そして、彼らはそれを「四辺形」とは呼ばず、長方形と呼びます。一部の人はそれを「四角形と四角形のツール」と呼んでいるので、混乱したユーザーは四角形を描画できる他のアプリに移行しません。

それで、彼らはそれが彼らのために働く方法を見つけたように私には思えます。そして、私が彼らがしたことは、正方形が同じ高さと幅のちょうど長方形であると仮定しているのではないかと思います。描画時にシフトキーを使用して、正方形を作成できるようにロックすることもできます。

これらのプログラムは、Shiftキーを押すと、四角形オブジェクトから完全に異なる互換性のない正方形のオブジェクトに切り替わるとは思いません。Shiftキーを離すと、互換性のない四角形にフォールバックするだけです。これは、InskscapeやThe Gimpなど、オープンソースのプログラムのソースコードをダウンロードすることで簡単に裏付けられます。

したがって、スーパークラスの継承からインターフェースの実装に設計を変更すると、「ちょっと」解決したと思います。

継承の場合、正方形は長方形であると述べているので、これは誤りです。一方、インターフェースの実装に取り​​掛かると、正方形は長方形として機能することができます。そのシナリオでは、メソッドが内部で行うことまたは行わないことは、実装の詳細です。正方形のことを完全に忘れた場合は、より良い方法です。同じ辺を持つ長方形を作成することを妨げるものは何もないので、問題ありません。

つまり、それは実際には物理学的問題であり、実際的な問題ではありません。

ウィキペディアの定義

A square...It can also be defined as a rectangle in 
which two adjacent sides have equal length.
5

LSPと継承とは、基本的には、子クラスは親クラスが実行できるすべてのことを実行でき、もう少し実行できることを意味します。

そしてここに問題があります:

  • 正方形の場合:正方形を半分の正方形の三角形に折りたたむことができます。つまり、「makesquaretriangle」というメソッドを作成できます。これはすべての長方形に当てはまるわけではありません。

  • 長方形の場合:高さに係数を掛け、幅にその係数の逆数を掛けても、表面積は同じです。これは、メソッドを作成できることを意味します。「doublemyheightbutkeepsurface」は、高さを2倍にし、幅を自動的に半分にします。
    これは正方形には当てはまりません。

ご覧のとおり、正方形と長方形の両方に他の方法では意味をなさないメソッドがあるため、常に置換することができません。そして、なぜ彼らはお互いにどちらからも継承できないのです。

LSPは、AをBから継承したい場合、またはBをAから継承したいが、できない場合に特に適しています。 BとAの両方を共通の基本クラスから継承する他のケースは、LSPの範囲外です。したがって、ソリューションはLSPとは関係ありません。

1
Pieter B

AFoursidedShapeは、高さと幅の間の依存関係に関する情報を非表示にするため、SquareがRectangleから継承する場合のように、LSPに違反しません。

長方形と正方形のクラスは正方形と長方形の表現であるため、それらが表すものと同じ関係を共有する必要はありません。言い換えると、四角形の四角形は四角形の四角形と関係がありますが、それはクラス四角形が四角形クラスと同じ関係を持つ必要があることを意味しません。それでもSquareをRectangleとして扱いたい場合は、LSPに違反せずにそれを行う別の方法として、SquareクラスとRectangleクラスによって実装されるgetHeightとgetWidthのコントラクトのみを持つARectangleReaderと呼ばれるインターフェイスを使用できます。

それについて考える別の方法:変更可能なSquareは本当に変更可能なRectangleのように動作しますか?

0
Kaveh Hadjari

それは古い質問だと思いますが、2セントあげたいと思います。

1人のユーザーが次のコードを持っている可能性があるため、問題を解決せず、デザインはLSPを尊重しません。

AFourSidedShape square = new Square(5, 5);
... //Later in code.
shape.setWidth(4);
shape.setHeight(3);
... //And later.
shape.getHeight() * shape.getWidth() // The user would expect its area is 12 but its currently 9.

まず、できるだけ一般的な方法で形状を操作しようとします。正方形は長方形なので、正方形を長方形として操作し、他のコードに残りのコードを処理させます。たとえば、Paintプログラムがある場合、ユーザーが正方形を作成できるようにするのが良いでしょう。 、ユーザーがGUIを使用して正方形を作成するときの長方形、実際には、同じ幅と高さの長方形を作成し、UIに表示させます。サイズ変更操作では、幅を拡大して維持する機能も提供できます。 /パーセントで高さ、このアクションのコントローラは一般的な数式を適用し、それは動作します。

2番目:カプセル化とは、オブジェクトを信頼する必要があります。たとえば、インターフェイスに共通のプロパティを配置してみます。

interface Shape { //This interface represents all the 2D shapes.
    double getArea();
    double getPerimeter();
}

class Rectangle implements Shape {
    private width;
    private height;
    ...
    public double getArea() { return width * height; }
    public double getPerimeter() { return width * 2 + height * 2; }
}

次に、指定したクラスにそのメソッドを実装します。これを行うことにより、クラス内の面積と周長の計算をカプセル化します。これは、形状オブジェクトの外部でこれらのプロパティを計算するよりも確実に優れています。あなた、しかしあなたがオブジェクトの外でそれをするとき、あなたはそのオブジェクトを信用せず、この責任を別のオブジェクトに割り当てているように見えます(実際にはあなたはそれを信頼しています)。

0
La VloZ Merrill