你能用一个好的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的实用方法。