你能用一个好的 C# 例子来解释 Liskov 替换原则吗? [关闭]

Posted

技术标签:

【中文标题】你能用一个好的 C# 例子来解释 Liskov 替换原则吗? [关闭]【英文标题】:Can you explain Liskov Substitution Principle with a good C# example? [closed] 【发布时间】:2011-05-24 15:28:25 【问题描述】:

您能否通过一个很好的 C# 示例来解释 Liskov 替换原则(SOLID 的“L”),以简化的方式涵盖该原则的所有方面?如果真的可以的话。

【问题讨论】:

这里有一个简单的思考方式:如果我遵循 LSP,我可以将代码中的任何对象替换为 Mock 对象,调用代码中的任何内容都需要调整或更改以考虑替换。 LSP 是对 Test by Mock 模式的基本支持。 this answer中还有更多符合和违规的示例 【参考方案1】:

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

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

但这会破坏 Open/Closed 原则并且必须在任何地方实现(因此仍然会产生不稳定的代码)。

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

更新

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

在使用实际实现 (ElectricDuck) 时,在 Swim 方法中打开鸭子的解决方案可能会产生副作用。但这可以通过使用explicit interface implementation 来解决。恕我直言,在Swim 中不打开它更有可能遇到问题,因为预计它会在使用IDuck 界面时游泳

更新 2

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

【讨论】:

@jgauffin:示例简单明了。但是你提出的解决方案,首先:打破了开闭原则,它不符合鲍勃叔叔的定义(见他文章的结论部分),其中写道:“Liskov 替换原则(AKA 合同设计)是一个重要特征所有符合开闭原则的程序。”见:objectmentor.com/resources/articles/lsp.pdf 我看不出解决方案如何破坏打开/关闭。如果您指的是if duck is ElectricDuck 部分,请再次阅读我的答案。上周四我参加了一个关于 SOLID 的研讨会 :) 不是真正的主题,但你能改变你的例子,这样你就不会做两次类型检查了吗?许多开发人员不知道as 关键字,这实际上使他们免于进行大量类型检查。我在想类似以下的事情:if var electricDuck = duck as ElectricDuck; if(electricDuck != null) electricDuck.TurnOn(); @jgauffin - 我对这个例子有点困惑。我认为 Liskov 替换原则在这种情况下仍然有效,因为 Duck 和 ElectricDuck 都派生自 IDuck,您可以在任何使用 IDuck 的地方放置 ElectricDuck 或 Duck。如果 ElectricDuck 必须在鸭子游泳之前打开,这不是 ElectricDuck 的责任还是某些代码实例化 ElectricDuck 然后将 IsTurnedOn 属性设置为 true 的责任。如果这违反了 LSP,那么 LSV 似乎很难遵守,因为所有接口都包含不同的方法逻辑。 @MystereMan:恕我直言 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).【参考方案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 并提供其数据库对象的新类。

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

最后,CustomerServiceManagerclass 的客户端只会调用 GetCustomerDetails 方法,传递 lastName 并且不应该关心数据的来源和方式。

希望这将为您提供了解 LSP 的实用方法。

【讨论】:

这怎么可能是 LSP 的例子? 我也没有看到 LSP 的例子......为什么它有这么多的赞成票? @RoshanGhangare IDataAccess 有 3 个具体的实现,可以在业务层中替换。 @YawarMurtaza 无论你引用什么例子都是典型的策略模式实现,就是这样。你能清楚它在哪里破坏 LSP 以及你如何解决违反 LSP 的问题 @Yogesh - 您可以将 IDataAccess 的实现与其任何具体类交换,这不会影响客户端代码 - 简而言之,这就是 LSP。是的,某些设计模式存在重叠。其次,上面的答案只是为了说明如何在银行应用程序的生产系统中实现 LSP。我的目的不是展示如何破坏 LSP 以及如何修复它 - 那将是一个培训教程,你可以在网络上找到 100 个。【参考方案3】:

这是应用里氏替换原则的代码。

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 状态: “派生类应该可以替代它们的基类(或接口)” & “使用对基类(或接口)的引用的方法必须能够使用派生类的方法,而无需了解它或了解细节。”

【讨论】:

此代码打印出来:Orange ColorRed Color。如果您需要在 VSCode 中测试此代码,请查看 code.visualstudio.com/docs/languages/csharp 和 channel9.msdn.com/Blogs/dotnet/…

以上是关于你能用一个好的 C# 例子来解释 Liskov 替换原则吗? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

你能解释一下提供的例子中的分类报告(召回率和精度)吗?

我无法理解 Iterable 类然后使用 .map 语法。你能用一种简单的语言来表达吗?

如何在 C# 中使用 Discord Bot 发送消息?

你能用 BatchNormalization 解释神经网络中的 Keras get_weights() 函数吗?

下面的例子是不是违反了 Liskov 替换原则?

谁能解释这个除法算法是如何工作的?