您可以用一个很好的C#示例解释Liskov替代原理吗?[关闭]


91

您能用一个很好的C#示例以简单的方式解释Liskov替代原理(SOLID的“ L”)吗?如果真的有可能。


9
简而言之,这是一种简化的思考方式:如果遵循LSP,则可以用Mock对象替换代码中的任何对象,并且需要对调用代码中的所有内容进行调整或更改以解决替换问题。LSP是对Test by Mock模式的基本支持。
kmote

Answers:


128

(此答案已被改写为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; } }
}

和调用代码

void MakeDuckSwim(IDuck duck)
{
    duck.Swim();
}

如您所见,有两个鸭子的例子。一只有机鸭和一只电鸭。电动鸭子只有在打开的情况下才能游泳。这违反了LSP原理,因为必须将其打开才能游泳,因为IsSwimming(这也是合同的一部分)不会像在基类中那样设置。

您当然可以通过执行以下操作来解决它

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

但这会破坏“打开/关闭”原理,必须在任何地方实现(因此仍然会生成不稳定的代码)。

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

更新资料

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

Swim在实际实现中工作时,在方法内部打开鸭子的解决方案可能会有副作用ElectricDuck。但这可以通过使用显式接口实现来解决。恕我直言,您很有可能通过不打开电源来解决问题,Swim因为使用该IDuck界面时,它会游动

更新2

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


1
@jgauffin:示例简单明了。但是,您提出的解决方案首先是:打破了开放原则,它不符合Bob叔叔的定义(请参阅其文章的结论部分),其中写道:“ Liskov替代原则(AKA按合同设计)是一个重要特征。符合开放式封闭原则的所有程序。” 参见:objectmentor.com/resources/articles/lsp.pdf
PencilCake 2010年

1
我看不到解决方案如何中断“打开/关闭”。如果您指的是if duck is ElectricDuck零件,请再次阅读我的答案。上周四,我参加了有关SOLID的研讨会:)
jgauffin 2010年

并不是真正的话题,但是可以请您更改示例,以免再次进行类型检查吗?许多开发人员都不知道该as关键字,这实际上使他们免于进行大量类型检查。我在想以下问题:if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn();
Siewers

3
@jgauffin-这个示例让我有些困惑。我认为Liskov替换原理在这种情况下仍然有效,因为Duck和ElectricDuck都源自IDuck,您可以将ElectricDuck或Duck放在使用IDuck的任何位置。如果必须先打开ElectricDuck,然后鸭子才能游泳,这不是ElectricDuck的职责或实例化ElectricDuck然后将IsTurnedOn属性设置为true的某些代码的责任。如果这违反了LSP,那么LSV似乎很难遵守,因为所有接口的方法都包含不同的逻辑。
Xaisoft 2011年

1
@MystereMan:恕我直言LSP都是关于行为正确性的。在矩形/正方形示例中,您将获得其他属性设置的副作用。有了鸭子,你会得到它不游泳的副作用。LSP:if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).
jgauffin

8

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();
    }
}

一旦设置了这三个数据访问类,现在我们将注意力吸引到客户端。在业务层中,我们具有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的实用方法。


3
这怎么可能是LSP的示例?
somegeek

1
我也没有看到LSP示例...为什么会有这么多的支持?
StaNov

1
@RoshanGhangare IDataAccess具有3个具体的实现,可以在业务层中替代。
Yawar Murtaza

1
@YawarMurtaza不管您引用的示例是战略模式的典型实现。您能清除中断LSP的地方以及您如何解决违反LSP的问题吗
-Yogesh

0

这是应用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指出:“派生类应该可以替换其基类(或接口)”和“使用对基类(或接口)的引用的方法必须能够使用派生类的方法,而无需对其进行了解或不了解细节。”

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.