web-dev-qa-db-ja.com

誰かが車両を使用したリスコフ代替原則(LSP)の例を提供できますか?

Liskov置換原則では、サブタイプはそのタイプの代わりに使用できる(プログラムの正確性を変更することなく)べきであると述べています。

  • 誰かがこの原則の例を車両(自動車)の領域で提供できますか?
  • 誰かが、車両の領域でこの原則の違反の例を提供できますか?

四角形/四角形の例を読みましたが、車両を使用した例の方がコンセプトをよく理解できると思います。

37
random512

私にとって、これは 1996の引用 fromUncle BobRobert C Martin )がLSPを最もよく要約しています:

基本クラスへのポインターまたは参照を使用する関数は、それを知らなくても派生クラスのオブジェクトを使用できる必要があります。

最近では、(通常は抽象)ベース/スーパークラスからのサブクラス化に基づく継承抽象化の代替として、多相抽象化にinterfacesを使用することもよくあります。 LSPは、コンシューマと抽象化の実装の両方に影響を与えます。

  • クラスまたはインターフェースの抽象化を使用するコードは、定義された抽象化を超えて、クラスについて他に何も想定してはなりません。
  • スーパークラスのサブクラス化または抽象化の実装は、抽象化へのインターフェースの要件と規則に準拠する必要があります。

LSPコンプライアンス

これは、複数の実装を持つことができるインターフェイスIVehicleを使用した例です(または、いくつかのサブクラスを持つ抽象基本クラスの代わりにインターフェイスを使用できます-同じ効果)。

_interface IVehicle
{
   void Drive(int miles);
   void FillUpWithFuel();
   int FuelRemaining {get; } // C# syntax for a readable property
}
_

IVehicleのコンシューマーのこの実装は、LSPの境界内にとどまります。

_void MethodWhichUsesIVehicle(IVehicle aVehicle)
{
   ...
   // Knows only about the interface. Any IVehicle is supported
   aVehicle.Drive(50);
 }
_

Glaring Violation-ランタイムタイプの切り替え

RTTIを使用してからダウンキャストするLSPの違反の例を次に示します。ボブおじさんはこれを「明白な違反」と呼んでいます。

_void MethodWhichViolatesLSP(IVehicle aVehicle)
{
   if (aVehicle is Car)
   {
      var car = aVehicle as Car;
      // Do something special for car - this method is not on the IVehicle interface
      car.ChangeGear();
    }
    // etc.
 }
_

違反メソッドは、縮小されたIVehicleインターフェースを超えて、インターフェースの既知の実装(または、インターフェースの代わりに継承を使用している場合はサブクラス)の特定のパスをハッキングします。ボブおじさんはまた、新しいサブクラスに対応するために関数の継続的な変更が必要になるため、タイプ切り替え動作を使用したLSP違反は通常 オープンおよびクローズの原則 にも違反すると説明しています。

違反-事前条件はサブタイプによって強化されます

別の違反例は、 "前提条件がサブタイプによって強化されている" の場合です。

_public abstract class Vehicle
{
    public virtual void Drive(int miles)
    {
        Assert(miles > 0 && miles < 300); // Consumers see this as the contract
    }
 }

 public class Scooter : Vehicle
 {
     public override void Drive(int miles)
     {
         Assert(miles > 0 && miles < 50); // ** Violation
         base.Drive(miles);
     }
 }
_

ここで、Scoooterサブクラスは、基本クラスDriveメソッドの前提条件を_miles < 300_に強化(さらに制約)しようとするときに、LSPを違反しようとしますが、最大で50マイル未満になりました。 Vehicleの契約定義では300マイルが許可されているため、これは無効です。

同様に、事後条件はサブタイプによって弱められない(つまり、緩和されない)場合があります。

(C#の Code Contracts のユーザーは、前提条件と事後条件は ContractClassForを介してインターフェースに配置する必要があることに注意してください クラスであり、実装クラス内に配置できないため、違反を回避できます)

微妙な違反-サブクラスによるインターフェース実装の悪用

_more subtle_違反(また、ボブおじさんの用語)は、インターフェイスを実装する疑わしい派生クラスで示すことができます。

_class ToyCar : IVehicle
{
    public void Drive(int miles) { /* Show flashy lights, make random sounds */ }
    public void FillUpWithFuel() {/* Again, more silly lights and noises*/}
    public int FuelRemaining {get {return 0;}}
}
_

ここでは、ToyCarがどこまで駆動されるかに関係なく、残りの燃料は常にゼロであり、IVehicleインターフェースのユーザーにとっては驚くべきことです(つまり、無限MPG消費-永久運動?)。この場合、問題は、ToyCarがインターフェイスのすべての要件を実装しているにもかかわらず、ToyCarは本質的に実際のIVehicleではなく、単に「ゴム印」ではないということです。インターフェース。

インターフェイスまたは抽象基本クラスがこのように悪用されるのを防ぐ1つの方法は、すべての実装が期待(および想定)を満たしているかどうかをテストするために、インターフェイス/抽象基本クラスで適切なユニットテストセットを利用できるようにすることです。単体テストは、典型的な使用法を文書化するのにも優れています。例えばこの_NUnit Theory_は、ToyCarが本番用コードベースにすることを拒否します。

_[Theory]
void EnsureThatIVehicleConsumesFuelWhenDriven(IVehicle vehicle)
{
    vehicle.FillUpWithFuel();
    Assert.IsTrue(vehicle.FuelRemaining > 0);
    int fuelBeforeDrive = vehicle.FuelRemaining;
    vehicle.Drive(20); // Fuel consumption is expected.
    Assert.IsTrue(vehicle.FuelRemaining < fuelBeforeDrive);
}
_

編集、再:OpenDoor

ドアを開けることはまったく別の問題のように聞こえるので、それに応じて分離する必要があります(つまり、SOLIDでは "S" および "I" )。

  • 新しいインターフェイスIVehicleWithDoors、これはIVehicleを継承できます
  • またはIMOの方がいいですが、別のインターフェースIDoorにすると、CarTruckのような車両はIVehicleIDoorの両方のインターフェースを実装します、しかしScooterMotorcycleはそうしません。
  • または、3つのインターフェイス、IVehicleDrive())、IDoorOpen())およびIVehicleWithDoorsは、これらの両方を継承します。

すべての場合において、LSPへの違反を回避するために、これらのインターフェイスのオブジェクトを必要とするコードは、追加の機能にアクセスするためにインターフェイスをダウンキャストしないでください。コードは、必要な適切な最小限のインターフェイス/(スーパー)クラスを選択し、そのインターフェイスの縮小された機能のみを使用する必要があります。

58
StuartLC

画像引っ越しの際にレンタカーを借りたいです。採用会社に電話をかけて、どのモデルを持っているか聞いてみます。彼らは私にちょうど利用できるようになる次の車が与えられると私に伝えます:

public class CarHireService {
    public Car hireCar() {
        return availableCarPool.getNextCar();
    }
}

しかし、彼らは私にすべてのモデルがこれらの機能を備えていることを私に告げるパンフレットを私に与えました:

public interface Car {
    public void drive();
    public void playRadio();
    public void addLuggage();
}

それはまさに私が探しているもののように聞こえるので、私は車を予約して幸せに行きます。引っ越しの日、私の家の外にF1車が現れます。

public class FormulaOneCar implements Car {
    public void drive() {
        //Code to make it go super fast
    }

    public void addLuggage() {
        throw new NotSupportedException("No room to carry luggage, sorry."); 
    }

    public void playRadio() {
        throw new NotSupportedException("Too heavy, none included."); 
    }
}

私は基本的に彼らのパンフレットに嘘をついていたので、私は幸せではありません。F1の車に荷物を収納できるような偽のブーツがあるかどうかは関係ありません。開かない、引っ越しには無用!

「これが私たちのすべての車がすることです」と言われたら、与えられたどの車もこのように動作するはずです。彼らのパンフレットの詳細を信用できないなら、それは役に立たない。それがLiskov Substitution Principleの本質です。

25
anotherdave

Liskov置換原則では、特定のインターフェイスを持つオブジェクトは、元のプログラムの正確性をすべて維持しながら、同じインターフェイスを実装する別のオブジェクトで置き換えることができると述べています。つまり、インターフェイスがまったく同じ型である必要があるだけでなく、動作も正しいままである必要があります。

車両では、部品を別の部品に交換できれば、自動車は機能し続けます。古いラジオにデジタルチューナーがありませんが、HDラジオを聴いて、HDレシーバーを備えた新しいラジオを購入するとします。インターフェースが同じであれば、古い無線機を取り外して新しい無線機に接続できるはずです。表面的には、これは、ラジオを車に接続する電気プラグが、新しいラジオでも古いラジオと同じ形状でなければならないことを意味します。車のプラグが長方形で15ピンの場合、新しいラジオのジャックも長方形で15ピンにする必要があります。

しかし、機械的な適合以外に他の考慮事項があります。プラグの電気的動作も同じでなければなりません。古い無線のコネクタのピン1が+ 12Vの場合、新しい無線のコネクタのピン1も+ 12Vにする必要があります。新しいラジオのピン1が「左スピーカー出力」ピンだった場合、ラジオがショートしたり、ヒューズが飛んだりすることがあります。それは明らかにLSPの違反です。

また、ダウングレードの状況を検討することもできます。たとえば、高価な無線が停止し、AM無線しか購入できないとします。ステレオ出力はありませんが、既存のラジオと同じコネクタを備えています。仕様では、ピン3がスピーカーの左側、ピン4がスピーカーの右側になっているとします。 AMラジオが3番ピンと4番ピンの両方からモノフォニック信号を再生する場合、その動作は一貫しており、これは許容可能な代替手段です。しかし、新しいAMラジオがピン3でのみオーディオを再生し、ピン4で何も再生しない場合、サウンドは不均衡になり、それはおそらく許容できる代替ではありません。この状況は、LSPにも違反します。これは、音が聞こえ、ヒューズが飛ぶことはないが、無線がインターフェイスの完全な仕様を満たしていないためです。

3
John Deters

まず、車両と自動車とは何かを定義する必要があります。 Googleによると(あまり完全な定義ではありません):

車両:
人や物を輸送するために使用されるもの、特に車、トラック、カートなどの陸上。

自動車:
通常は4つの車輪を備え、内燃機関または電気で動く道路車両
モーターと少数の人々を運ぶことができる

つまり、自動車は車両ですが、車両は自動車ではありません。

2
user2810910