web-dev-qa-db-ja.com

Liskov Substitution Principleを良いC#の例で説明できますか?

Liskov Substitution Principle(SOLIDの「L」)を、原則のすべての側面を簡単な方法でカバーする良いC#の例で説明できますか?本当に可能であれば。

90
pencilCake

(この回答は2013-05-13に書き換えられました。コメントの下部にある議論を読んでください)

LSPは基本クラスの契約に従うことです。

たとえば、サブクラスで新しい例外をスローしないようにすることができます。これは、基本クラスを使用する例外では予期されないためです。引数が欠落している場合に基本クラスがArgumentNullExceptionをスローし、サブクラスが引数をnullにすることを許可する場合も同様です(LSP違反)。

LSPに違反するクラス構造の例を次に示します。

public interface IDuck
{
   void Swim();
   // contract says that IsSwimming should be true if Swim has been called.
   bool IsSwimming { get; }
}

public class OrganicDuck : IDuck
{
   public void Swim()
   {
      //do something to swim
   }

   bool IsSwimming { get { /* return if the duck is swimming */ } }
}

public class ElectricDuck : IDuck
{
   bool _isSwimming;

   public void Swim()
   {
      if (!IsTurnedOn)
        return;

      _isSwimming = true;
      //swim logic            
   }

   bool IsSwimming { get { return _isSwimming; } }
}

そして呼び出しコード

void MakeDuckSwim(IDuck duck)
{
    duck.Swim();
}

ご覧のとおり、アヒルには2つの例があります。 1匹の有機鴨と1匹の電動鴨。電気カモは、電源が入っている場合にのみ泳ぐことができます。 IsSwimming(コントラクトの一部でもある)が基本クラスのように設定されないため、泳ぐことができるようにする必要があるため、これはLSPの原則を破ります。

もちろん、このようなことをすることで解決できます

void MakeDuckSwim(IDuck duck)
{
    if (duck is ElectricDuck)
        ((ElectricDuck)duck).TurnOn();
    duck.Swim();
}

しかし、これはOpen/Closedの原則を破り、どこにでも実装する必要があります(したがって、依然として不安定なコードが生成されます)。

適切な解決策は、Swimメソッドでアヒルを自動的にオンにし、そうすることで、電気アヒルがIDuckインターフェイスで定義されたとおりに動作するようにすることです。

更新

誰かがコメントを追加して削除しました。私が対処したい有効なポイントがありました:

Swimメソッド内でアヒルをオンにすると、実際の実装(ElectricDuck)を操作するときに副作用が生じる可能性があります。しかし、それは explicit interface implementation を使用することで解決できます。 Swimインターフェースを使用すると泳ぐことが期待されるため、IDuckでオンにしないことで問題が発生する可能性が高い

更新2

より明確にするためにいくつかの部分を言い換えました。

125
jgauffin

LSPの実用的なアプローチ

LSPのC#の例を探すすべての場所で、人々は想像上のクラスとインターフェイスを使用しています。以下は、私たちのシステムの1つで実装したLSPの実用的な実装です。

シナリオ:顧客データを提供する3つのデータベース(住宅ローンの顧客、当座預金の顧客、普通預金口座の顧客)があり、特定の顧客の姓の顧客詳細が必要だとします。これで、これら3つのデータベースから、特定の姓に対して複数の顧客詳細を取得できます。

実装:

ビジネスモデルレイヤー:

public class Customer
{
    // customer detail properties...
}

データアクセス層:

public interface IDataAccess
{
    Customer GetDetails(string lastName);
}

上記のインターフェイスは、抽象クラスによって実装されます

public abstract class BaseDataAccess : IDataAccess
{
    /// <summary> Enterprise library data block Database object. </summary>
    public Database Database;


    public Customer GetDetails(string lastName)
    {
        // use the database object to call the stored procedure to retrieve the customer details
    }
}

この抽象クラスには、3つのデータベースすべてに共通のメソッド「GetDetails」があり、以下に示すように、各データベースクラスによって拡張されます。

住宅ローン顧客データアクセス:

public class MortgageCustomerDataAccess : BaseDataAccess
{
    public MortgageCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetMortgageCustomerDatabase();
    }
}

現在のアカウントのお客様のデータアクセス:

public class CurrentAccountCustomerDataAccess : BaseDataAccess
{
    public CurrentAccountCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetCurrentAccountCustomerDatabase();
    }
}

アカウントの顧客データへのアクセスの節約:

public class SavingsAccountCustomerDataAccess : BaseDataAccess
{
    public SavingsAccountCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetSavingsAccountCustomerDatabase();
    }
}

これらの3つのデータアクセスクラスを設定したら、クライアントに注意を向けます。ビジネス層には、顧客の詳細をクライアントに返すCustomerServiceManagerクラスがあります。

ビジネスレイヤー:

public class CustomerServiceManager : ICustomerServiceManager, BaseServiceManager
{
   public IEnumerable<Customer> GetCustomerDetails(string lastName)
   {
        IEnumerable<IDataAccess> dataAccess = new List<IDataAccess>()
        {
            new MortgageCustomerDataAccess(new DatabaseFactory()), 
            new CurrentAccountCustomerDataAccess(new DatabaseFactory()),
            new SavingsAccountCustomerDataAccess(new DatabaseFactory())
        };

        IList<Customer> customers = new List<Customer>();

       foreach (IDataAccess nextDataAccess in dataAccess)
       {
            Customer customerDetail = nextDataAccess.GetDetails(lastName);
            customers.Add(customerDetail);
       }

        return customers;
   }
}

既に複雑になっているため、単純にするための依存関係の注入は示していません。

これで、新しい顧客詳細データベースがある場合、BaseDataAccessを拡張し、そのデータベースオブジェクトを提供する新しいクラスを追加できます。

もちろん、参加するすべてのデータベースに同一のストアドプロシージャが必要です。

最後に、CustomerServiceManagerclassのクライアントは、GetCustomerDetailsメソッドを呼び出し、lastNameを渡すだけで、データの送信元と送信元を気にする必要はありません。

これにより、LSPを理解するための実用的なアプローチが得られることを願っています。

8
Yawar Murtaza