我一直在关注这个可能会违反《里斯科夫换人原则》的极富争议的问题。我知道Liskov替代原则是什么,但是我仍然不清楚的是,如果我作为开发人员在编写面向对象的代码时没有考虑该原则,那可能会出问题。
我一直在关注这个可能会违反《里斯科夫换人原则》的极富争议的问题。我知道Liskov替代原则是什么,但是我仍然不清楚的是,如果我作为开发人员在编写面向对象的代码时没有考虑该原则,那可能会出问题。
Answers:
我认为在这个问题上讲得很好,这是投票率很高的原因之一。
现在,当在Task上调用Close()时,如果它是具有启动状态的ProjectTask,则调用将失败,而如果它是基本Task则不会失败。
想象一下您是否会:
public void ProcessTaskAndClose(Task taskToProcess)
{
taskToProcess.Execute();
taskToProcess.DateProcessed = DateTime.Now;
taskToProcess.Close();
}
在此方法中,.Close()调用有时会中断,因此,现在基于派生类型的具体实现,必须更改此方法的行为方式,与如果Task没有任何可能的子类型时该方法的编写方式相同移交给这种方法。
由于违反liskov替换,使用您的类型的代码将必须具有派生类型的内部工作的显式知识,以区别对待它们。这紧密耦合了代码,并且通常使实现更难以一致地使用。
如果您不履行基类中定义的合同,那么当您获得不满意的结果时,事情可能会无声地失败。
如果其中任何一个都不成立,则呼叫者可能会得到他未预期的结果。
考虑一下访谈问题记录中的一个经典案例:您从Ellipse派生了Circle。为什么?因为IS-AN椭圆当然是圆的!
除了...椭圆具有两个功能:
Ellipse.set_alpha_radius(d)
Ellipse.set_beta_radius(d)
显然,必须为Circle重新定义这些参数,因为Circle具有统一的半径。您有两种可能性:
大多数OO语言都不支持第二种语言,这是有充分理由的:发现您的Circle不再是Circle将令人惊讶。所以第一个选择是最好的。但是请考虑以下功能:
some_function(Ellipse byref e)
想象一下some_function调用e.set_alpha_radius。但是由于e 确实是一个Circle,因此令人惊讶的是它的beta半径也已设置。
替代原则就在这里:子类必须可以替代超类。否则会发生令人惊讶的事情。
用外行的话来说:
您的代码到处都有很多CASE / switch子句。
这些CASE / switch子句中的每一个子句都需要不时添加新的用例,这意味着代码库没有应有的可伸缩性和可维护性。
LSP允许代码更像硬件一样工作:
您无需修改iPod,因为您购买了一对新的外部扬声器,因为旧的和新的外部扬声器都使用同一接口,因此它们可以互换使用而不会失去iPod所需的功能。
typeof(someObject)
以决定“允许执行的操作”,那么可以肯定,但这完全是另一种反模式。
用Java的UndoManager给出一个真实的例子
它从其AbstractUndoableEdit
合同中继承,该合同指定它具有2个状态(撤消和重做),并且可以通过一次调用undo()
和进入它们之间redo()
但是UndoManager具有更多的状态,其作用类似于撤消缓冲区(每次调用都undo
撤消部分但不是全部编辑操作,从而削弱了后置条件)
这导致了一种假设情况,即您在调用之前将UndoManager添加到CompoundEdit,end()
然后在该CompoundEdit上调用undo,则undo()
一旦部分撤消了编辑,它将导致它在每次编辑时都进行调用
UndoManager
为了避免这种情况,我滚动了自己(我可能应该将其重命名为UndoBuffer
)
我最近继承了一个代码库,其中包含一些主要的Liskov违反者。在重要的班级。这给我带来了极大的痛苦。让我解释一下原因。
我有Class A
,它来自Class B
。 Class A
并Class B
共享一堆Class A
用其自己的实现覆盖的属性。设置或获取Class A
属性与从中设置或获取完全相同的属性有不同的效果Class B
。
public Class A
{
public virtual string Name
{
get; set;
}
}
Class B : A
{
public override string Name
{
get
{
return TranslateName(base.Name);
}
set
{
base.Name = value;
FunctionWithSideEffects();
}
}
}
撇开这是在.NET中进行转换的一种非常糟糕的方式这一事实来看,此代码还有许多其他问题。
在这种情况下,Name
它在许多地方用作索引和流量控制变量。上面的类在它们的原始代码和派生形式中乱七八糟。在这种情况下,违反Liskov替换原则意味着我需要知道对采用基类的每个函数的每次调用的上下文。
该代码使用两个对象 Class A
和Class B
,所以我不能只是让Class A
抽象的,迫使人们使用Class B
。
有一些非常有用的实用工具功能在运行,Class A
还有一些非常有用的实用工具功能在运行Class B
。理想情况下,我希望能够使用可以Class A
在Class B
。如果不是因为违反LSP ,采取a的许多功能Class B
很容易采用a Class A
。
最糟糕的是,这种特殊情况确实很难重构,因为整个应用程序取决于这两个类,始终在这两个类上运行,如果我更改此设置,它将以一百种方式中断(我将要做的事情)无论如何)。
为了解决这个问题,我要做的就是创建一个NameTranslated
属性,该属性将是Class B
该Name
属性的版本,并且非常非常小心地更改对派生对象的每个引用Name
属性的以使用我的新NameTranslated
属性。但是,即使这些引用之一出错,整个应用程序也可能崩溃。
考虑到代码库周围没有单元测试,这几乎是开发人员可能面临的最危险的情况。如果我不更改违规,则我必须花费大量的精力跟踪每种方法正在处理哪种类型的对象,如果我解决了违规,我可能会使整个产品在不适当的时候爆炸。
BaseName
并TranslatedName
同时访问A类样式Name
和B类含义,会发生什么情况?然后,任何尝试访问Name
类型变量的操作B
都会被编译器错误拒绝,因此您可以确保将所有引用转换为其他形式之一。
如果您想解决违反LSP的问题,请考虑一下,如果只有基类的.dll / .jar(无源代码),并且必须构建新的派生类,该怎么办。您永远无法完成此任务。