关于.net:你能用一个好的C#例解释Liskov Substitution Principle吗?

Can you explain Liskov Substitution Principle with a good C# example?

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


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

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

例如,您可以不在子类中抛出新的异常,因为使用基类的类不会期望这样做。如果基类抛出了ArgumentNullException,如果缺少参数,并且子类允许参数为空,那么也会出现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原则,因为必须打开它才能游泳,因为IsSwimming(也是合同的一部分)不会设置为基类。

你当然可以通过这样的方法来解决

1
2
3
4
5
6
void MakeDuckSwim(IDuck duck)
{
    if (duck is ElectricDuck)
        ((ElectricDuck)duck).TurnOn();
    duck.Swim();
}

但这将打破开放/封闭的原则,必须在任何地方实现(而这仍然会生成不稳定的代码)。

正确的解决方案是在Swim方法中自动打开duck,这样可以使电子duck的行为完全符合IDuck接口的定义。

更新

有人添加了评论并删除了它。我想强调的是,这一点是正确的:

在使用实际实现时,在Swim方法中打开duck的解决方案可能会产生副作用(ElectricDuck)。但这可以通过使用显式接口实现来解决。imho在Swim中不打开它更有可能会出现问题,因为它在使用IDuck接口时会游泳。

更新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并提供其数据库对象的新类。

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

最后,CustomerServiceManager类的客户机只调用getCustomerDetails方法,传递姓氏,而不关心数据的来源和方式。

希望这能给你一个实际的方法来理解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状态:"派生类应可替换为其基类(或接口)"和;"使用对基类(或接口)的引用的方法必须能够使用派生类的方法,而不必了解它或了解详细信息。"