Liskov Substitution Principle(SOLIDの「L」)を、原則のすべての側面を簡単な方法でカバーする良いC#の例で説明できますか?本当に可能であれば。
(この回答は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
より明確にするためにいくつかの部分を言い換えました。
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を拡張し、そのデータベースオブジェクトを提供する新しいクラスを追加できます。
もちろん、参加するすべてのデータベースに同一のストアドプロシージャが必要です。
最後に、CustomerServiceManager
classのクライアントは、GetCustomerDetailsメソッドを呼び出し、lastNameを渡すだけで、データの送信元と送信元を気にする必要はありません。
これにより、LSPを理解するための実用的なアプローチが得られることを願っています。