回答:
(この回答は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つの電気アヒル。電気アヒルは、電源が入っている場合にのみ泳ぐことができます。これは、LSPの原則に違反します。これは、IsSwimming
(これもコントラクトの一部です)が基本クラスのように設定されないため、泳げるようにするためにオンにする必要があるためです。
もちろん、このようなことをすることで解決できます
void MakeDuckSwim(IDuck duck)
{
if (duck is ElectricDuck)
((ElectricDuck)duck).TurnOn();
duck.Swim();
}
しかし、それはOpen / Closedの原則に違反し、どこにでも実装する必要があります(したがって、まだ不安定なコードが生成されます)。
適切な解決策は、Swim
メソッドでアヒルを自動的にオンにすることです。そうすることで、電気アヒルがIDuck
インターフェイスで定義されたとおりに動作するようになります。
更新
誰かがコメントを追加して削除しました。それは私が対処したい有効なポイントを持っていました:
Swim
メソッド内でアヒルをオンにするソリューションは、実際の実装で作業するときに副作用が発生する可能性があります(ElectricDuck
)。しかし、それは明示的なインターフェース実装を使用することで解決できます。インターフェースSwim
を使用するときに泳ぐことが期待されているので、オンにしないと問題が発生する可能性が高くなりIDuck
ます
アップデート2
わかりやすくするために一部を言い換えました。
if duck is ElectricDuck
部分について言及している場合は、もう一度私の答えを読んでください。私は先週の木曜日にSOLIDについてのセミナーをしました:)
as
キーワードに気づいていないため、実際には多くの型チェックからそれらを節約できます。私は、次のようなものを考えている:if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).
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
クラス GetCustomerDetailsメソッドのみを呼び出し、lastNameを渡します。データがどこからどのように取得されるかを気にする必要はありません。
これがLSPを理解するための実用的なアプローチになることを願っています。
Liskov Substitute Principleを適用するためのコードは次のとおりです。
public abstract class Fruit
{
public abstract string GetColor();
}
public class Orange : Fruit
{
public override string GetColor()
{
return "Orange Color";
}
}
public class Apple : Fruit
{
public override string GetColor()
{
return "Red color";
}
}
class Program
{
static void Main(string[] args)
{
Fruit fruit = new Orange();
Console.WriteLine(fruit.GetColor());
fruit = new Apple();
Console.WriteLine(fruit.GetColor());
}
}
LSVの状態:「派生クラスは、その基本クラス(またはインターフェース)の代わりに使用できる」&「基本クラス(またはインターフェース)への参照を使用するメソッドは、それについて知らないか、詳細を知らなくても、派生クラスのメソッドを使用できる必要があります」