您能用一个很好的 C# 示例来解释里氏替换原理吗?

发布于 2024-10-07 11:55:48 字数 1435 浏览 2 评论 0原文

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

长亭外,古道边 2024-10-14 11:55:48

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

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

例如,您可以不在子类中抛出新的异常,因为使用基类的异常不会发生这种情况。如果缺少参数并且子类允许参数为 null,则基类抛出 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 接口

Update< 定义。 /strong>

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

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

更新 2

时它会游泳重新表述了一些部分以使其更加清晰。

(This answer has been rewritten 2013-05-13, read the discussion in the bottom of the comments)

LSP is about following the contract of the base class.

You can for instance not throw new exceptions in the sub classes as the one using the base class would not expect that. Same goes for if the base class throws ArgumentNullException if an argument is missing and the sub class allows the argument to be null, also a LSP violation.

Here is an example of a class structure which violates 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; } }
}

And the calling code

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

As you can see, there are two examples of ducks. One organic duck and one electric duck. The electric duck can only swim if it's turned on. This breaks the LSP principle since it must be turned on to be able to swim as the IsSwimming (which also is part of the contract) won't be set as in the base class.

You can of course solve it by doing something like this

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

But that would break Open/Closed principle and has to be implemented everywhere (and thefore still generates unstable code).

The proper solution would be to automatically turn on the duck in the Swim method and by doing so make the electric duck behave exactly as defined by the IDuck interface

Update

Someone added a comment and removed it. It had a valid point that I'd like to address:

The solution with turning on the duck inside the Swim method can have side effects when working with the actual implementation (ElectricDuck). But that can be solved by using a explicit interface implementation. imho it's more likely that you get problems by NOT turning it on in Swim since it's expected that it will swim when using the IDuck interface

Update 2

Rephrased some parts to make it more clear.

只涨不跌 2024-10-14 11:55:48

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 的实用方法。

LSP a Practical Approach

Everywhere I look for LSP's C# examples, people have used imaginary classes and interfaces. Here is the practical implementation of LSP that I implemented in one of our systems.

Scenario: Suppose we have 3 databases (Mortgage Customers, Current Accounts Customers and Savings Account Customers) that provide customer data and we need customer details for given customer's last name. Now we may get more than 1 customer detail from those 3 databases against given last name.

Implementation:

BUSINESS MODEL LAYER:

public class Customer
{
    // customer detail properties...
}

DATA ACCESS LAYER:

public interface IDataAccess
{
    Customer GetDetails(string lastName);
}

Above interface is implemented by the abstract class

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
    }
}

This abstract class has a common method "GetDetails" for all 3 databases which is extended by each of the database classes as shown below

MORTGAGE CUSTOMER DATA ACCESS:

public class MortgageCustomerDataAccess : BaseDataAccess
{
    public MortgageCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetMortgageCustomerDatabase();
    }
}

CURRENT ACCOUNT CUSTOMER DATA ACCESS:

public class CurrentAccountCustomerDataAccess : BaseDataAccess
{
    public CurrentAccountCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetCurrentAccountCustomerDatabase();
    }
}

SAVINGS ACCOUNT CUSTOMER DATA ACCESS:

public class SavingsAccountCustomerDataAccess : BaseDataAccess
{
    public SavingsAccountCustomerDataAccess(IDatabaseFactory factory)
    {
        this.Database = factory.GetSavingsAccountCustomerDatabase();
    }
}

Once these 3 data access classes are set, now we draw our attention to the client. In the Business layer we have CustomerServiceManager class that returns the customer details to its clients.

BUSINESS LAYER:

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

I haven't shown the dependency injection to keep it simple as its already getting complicated now.

Now if we have a new customer detail database we can just add a new class that extends BaseDataAccess and provides its database object.

Of course we need identical stored procedures in all participating databases.

Lastly, the client for CustomerServiceManagerclass will only call GetCustomerDetails method, pass the lastName and should not care about how and where the data is coming from.

Hope this will give you a practical approach to understand LSP.

看透却不说透 2024-10-14 11:55:48

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

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

Here's the code for applying Liskov Substitute Principle.

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 states:
"Derived classes should be substitutable for their base classes (or interfaces)"
&
"Methods that use references to base classes (or interfaces) have to be able to use methods of the derived classes without knowing about it or knowing the details."

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文