你能用一个好的C#示例来解释Liskovreplace原理吗?

你能用一个很好的C#例子来解释Liskovreplace原理(SOLID的'L')吗? 如果真的有可能。

(这个答案已经被改写2013-05-13,阅读评论底部的讨论)

LSP是关于跟随基类的合同。

你可以例如不要在子类中抛出新的exception,因为使用基类的人不会期望这样做。 如果基类抛出ArgumentNullException如果缺less一个参数,并且子类允许参数为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(); } 

正如你所看到的,有两个鸭子的例子。 一个有机鸭和一个电动鸭子。 电动鸭只能打开才能游泳。 这违反了LSP原则,因为它必须被打开才能够游泳,因为IsSwimming (也是合同的一部分)不会被设置为基类。

你当然可以通过做这样的事情来解决它

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

但是,这将违反开放/封闭的原则,必须在任何地方实施(因此仍然会产生不稳定的代码)。

正确的解决办法是自动打开Swim方法中的鸭子,这样做使电动鸭子的行为完全按照IDuck界面

更新

有人添加了评论,并将其删除。 它有一个有效的观点,我想解决:

使用Swim方法打开鸭子的解决scheme可能会在实际执行( ElectricDuck )时产生副作用。 但是这可以通过使用明确的接口实现来解决。 恕我直言,你更有可能遇到问题,不要在Swim打开它,因为预计它会在使用IDuck接口时游泳

更新2

改写一些部分以使其更清楚。

**

LSP实用方法

**

我到处寻找LSP的C#示例,人们使用虚构的类和接口。 这是我在我们的一个系统中实现的LSP的实际实现。

场景:假设我们有3个数据库(抵押贷款客户,当前账户客户和储蓄账户客户)提供客户数据,我们需要给定客户的姓氏的客户详细信息。 现在,我们可能会从这3个数据库中获得超过1个客户的详细信息。

执行:

业务模型层:

 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 retirve the customer detials } } 

这个抽象类对所有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(); } } 

一旦设置了这三个数据访问类,现在我们将注意力引向客户端。 在业务层中,我们有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; } } 

我没有显示dependency injection,以保持简单,因为它现在已经变得复杂了。

现在,如果我们有一个新的客户详细数据库,我们可以添加一个扩展BaseDataAccess并提供其数据库对象的新类。

当然,我们在所有参与数据库中都需要相同的存储过程

最后, CustomerServiceManager类的客户端只会调用GetCustomerDetails方法,传递lastName,不应该关心数据来自何方。

希望这会给你一个理解LSP的实用方法。