你能用一个好的C#例子来解释Liskov Substitution Principle吗?

pen*_*ake 90 .net c# oop liskov-substitution-principle solid-principles

你能用一个很好的C#例子解释Liskov替换原理(SOLID的'L'),以简化的方式涵盖原理的所有方面吗?如果真的有可能.

jga*_*fin 125

(这个答案已经改写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; } }
}
Run Code Online (Sandbox Code Playgroud)

和调用代码

void MakeDuckSwim(IDuck duck)
{
    duck.Swim();
}
Run Code Online (Sandbox Code Playgroud)

如您所见,有两个鸭子的例子.一只有机鸭和一只电鸭.电鸭只有在打开时才会游泳.这打破了LSP原则,因为它必须打开才能游泳,因为IsSwimming(也是合同的一部分)不会像在基类中那样设置.

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

void MakeDuckSwim(IDuck duck)
{
    if (duck is ElectricDuck)
        ((ElectricDuck)duck).TurnOn();
    duck.Swim();
}
Run Code Online (Sandbox Code Playgroud)

但这会打破开放/封闭原则,必须在任何地方实施(因此仍会产生不稳定的代码).

正确的解决方案是在Swim方法中自动打开鸭子并通过这样做使电子鸭的行为完全按照IDuck界面的 定义

更新

有人添加了评论并将其删除.它有一个我想提出的有效观点:

Swim在使用实际的实现(ElectricDuck)时,在方法中打开鸭子的解决方案可能会产生副作用.但这可以通过使用显式接口实现来解决.imho你更有可能因为不打开它而遇到问题,Swim因为预计它会在使用IDuck界面时游泳

更新2

重新描述一些部分以使其更清晰.

  • @jgauffin - 我对这个例子感到有些困惑.我认为Liskov替换原则在这种情况下仍然有效,因为Duck和ElectricDuck都来自IDuck,你可以在任何地方放置一个ElectrDuck或Duck IDuck.如果ElectricDuck必须在鸭子可以游泳之前打开,那么PowerDuck或某些代码的责任不是实例化ElectricDuck然后将IsTurnedOn属性设置为true.如果这违反了LSP,那么LSV似乎很难坚持,因为所有接口都会包含不同的逻辑方法. (2认同)

Yaw*_*aza 8

LSP一种实用方法

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

场景:假设我们有3个数据库(抵押客户,经常账户客户和储蓄账户客户)提供客户数据,我们需要客户详细信息给出客户的姓氏.现在,根据给定的姓氏,我们可以从这3个数据库中获得超过1个客户详细信息.

执行:

商业模式层:

public class Customer
{
    // customer detail properties...
}
Run Code Online (Sandbox Code Playgroud)

数据访问层:

public interface IDataAccess
{
    Customer GetDetails(string lastName);
}
Run Code Online (Sandbox Code Playgroud)

上面的接口由抽象类实现

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
    }
}
Run Code Online (Sandbox Code Playgroud)

这个抽象类为所有3个数据库都有一个通用的方法"GetDetails",它由每个数据库类扩展,如下所示

抵押客户数据访问:

public class MortgageCustomerDataAccess : BaseDataAccess
{
    public MortgageCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetMortgageCustomerDatabase();
    }
}
Run Code Online (Sandbox Code Playgroud)

当前账户客户数据访问:

public class CurrentAccountCustomerDataAccess : BaseDataAccess
{
    public CurrentAccountCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetCurrentAccountCustomerDatabase();
    }
}
Run Code Online (Sandbox Code Playgroud)

节省账户客户数据访问:

public class SavingsAccountCustomerDataAccess : BaseDataAccess
{
    public SavingsAccountCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetSavingsAccountCustomerDatabase();
    }
}
Run Code Online (Sandbox Code Playgroud)

一旦设置了这3个数据访问类,现在我们将注意力吸引到客户端.在Business层中,我们有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;
   }
}
Run Code Online (Sandbox Code Playgroud)

我没有展示依赖注入来保持简单,因为它现在已经变得复杂了.

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

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

最后,CustomerServiceManager类的客户端将只调用GetCustomerDetails方法,传递lastName,而不应关心数据的来源和位置.

希望这能为您提供一种理解LSP的实用方法.

  • 这怎么可以作为LSP的例子? (2认同)