Can you explain Liskov Substitution Principle with a good C# example?
你能用一个很好的C例子来解释Liskov替换原理(固体的"L")吗?这个例子以一种简单的方式涵盖了原理的所有方面。如果真的有可能的话。
(此答案已改写2013-05-13,请阅读评论底部的讨论)
LSP是关于遵循基类的约定。
例如,您可以不在子类中抛出新的异常,因为使用基类的类不会期望这样做。如果基类抛出了
以下是违反LSP的类结构示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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; } } } |
以及呼叫码
1 2 3 4 | void MakeDuckSwim(IDuck duck) { duck.Swim(); } |
如你所见,有两个鸭子的例子。一只有机鸭和一只电子鸭。电鸭只有打开电源才能游泳。这打破了LSP原则,因为必须打开它才能游泳,因为
你当然可以通过这样的方法来解决
1 2 3 4 5 6 | void MakeDuckSwim(IDuck duck) { if (duck is ElectricDuck) ((ElectricDuck)duck).TurnOn(); duck.Swim(); } |
但这将打破开放/封闭的原则,必须在任何地方实现(而这仍然会生成不稳定的代码)。
正确的解决方案是在
更新
有人添加了评论并删除了它。我想强调的是,这一点是正确的:
在使用实际实现时,在
更新2
对某些部分进行了重新措辞以使其更清晰。
一种实用的方法
无论我在哪里寻找LSP的C示例,人们都使用了虚构的类和接口。下面是我在一个系统中实现的LSP的实际实现。
场景:假设我们有3个数据库(抵押贷款客户、活期账户客户和储蓄账户客户)提供客户数据,并且我们需要给定客户姓氏的客户详细信息。现在,我们可以根据给定的姓氏从这3个数据库中获取1个以上的客户详细信息。
实施:
业务模型层:
1 2 3 4 | public class Customer { // customer detail properties... } |
数据访问层:
1 2 3 4 | public interface IDataAccess { Customer GetDetails(string lastName); } |
以上接口由抽象类实现
1 2 3 4 5 6 7 8 9 10 11 | 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",该方法由每个数据库类扩展,如下所示
抵押客户数据访问:
1 2 3 4 5 6 7 | public class MortgageCustomerDataAccess : BaseDataAccess { public MortgageCustomerDataAccess(IDatabaseFactory factory) { this.Database = factory.GetMortgageCustomerDatabase(); } } |
活期账户客户数据访问:
1 2 3 4 5 6 7 | public class CurrentAccountCustomerDataAccess : BaseDataAccess { public CurrentAccountCustomerDataAccess(IDatabaseFactory factory) { this.Database = factory.GetCurrentAccountCustomerDatabase(); } } |
储蓄账户客户数据访问:
1 2 3 4 5 6 7 | public class SavingsAccountCustomerDataAccess : BaseDataAccess { public SavingsAccountCustomerDataAccess(IDatabaseFactory factory) { this.Database = factory.GetSavingsAccountCustomerDatabase(); } } |
一旦设置了这3个数据访问类,现在我们将注意力集中到客户机上。在业务层中,我们有CustomerServiceManager类,它将客户详细信息返回给客户机。
业务层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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并提供其数据库对象的新类。
当然,我们需要在所有参与的数据库中使用相同的存储过程。
最后,
希望这能给你一个实际的方法来理解LSP。
这是应用里斯科夫替代原理的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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状态:"派生类应可替换为其基类(或接口)"和;"使用对基类(或接口)的引用的方法必须能够使用派生类的方法,而不必了解它或了解详细信息。"