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

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


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

LSP是关于遵循基类的契约。

你可以例如不要在子类中抛出新的异常,因为使用基类的人不会期望这样。 如果基类抛出ArgumentNullException如果缺少参数并且子类允许参数为空(也是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界面所定义的IDuck

更新

有人添加了评论并将其删除。 它有一个我想要解决的有效问题:

Swim方法中打开鸭子的解决方案在实际实施( ElectricDuck )时可能会产生副作用。 但是这可以通过使用明确的接口实现来解决。 imho更有可能是因为没有在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 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替代原则的代码。

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指出:“派生类应该可替代其基类(或接口)”&“使用对基类(或接口)的引用的方法必须能够使用派生类的方法,而不必知道它或知道细节“。

链接地址: http://www.djcxy.com/p/50113.html

上一篇: Can you explain Liskov Substitution Principle with a good C# example?

下一篇: MethodInvoker vs Action for Control.BeginInvoke