你能用一个好的 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 并提供其数据库对象的新类。
当然,我们需要在所有参与的数据库中使用相同的存储过程。
最后,CustomerServiceManager
class 的客户端只会调用 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 Color
和 Red Color
。如果您需要在 VSCode 中测试此代码,请查看 code.visualstudio.com/docs/languages/csharp 和 channel9.msdn.com/Blogs/dotnet/…以上是关于你能用一个好的 C# 例子来解释 Liskov 替换原则吗? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章
我无法理解 Iterable 类然后使用 .map 语法。你能用一种简单的语言来表达吗?