基本上,我们希望事情表现得明智。
考虑以下问题:
我得到了一组矩形,我想将其面积增加10%。所以我要做的是将矩形的长度设置为以前的1.1倍。
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
foreach(var rectangle in rectangles)
{
rectangle.Length = rectangle.Length * 1.1;
}
}
现在,在这种情况下,我所有矩形的长度都增加了10%,这将使它们的面积增加了10%。不幸的是,实际上有人通过了正方形和矩形的混合体,并且当矩形的长度改变时,宽度也改变了。
我的单元测试通过了,因为我编写了所有单元测试以使用矩形的集合。现在,我在我的应用程序中引入了一个细微的错误,这个错误可能在几个月后才被忽略。
更糟糕的是,来自会计的Jim看到了我的方法,并编写了一些其他代码,该代码利用以下事实:如果他将平方数传递到我的方法中,那么他的大小就会增加21%。吉姆很高兴,没有人比他更明智。
吉姆因出色的工作而晋升到另一个部门。阿尔弗雷德(Alfred)以初级职位加入公司。Advertising的Jill在他的第一份bug报告中报告说,将方格传递给该方法会导致21%的增长,并且希望修复该bug。Alfred看到Square和Rectangles在代码中随处可见,并且意识到打破继承链是不可能的。他也无权访问Accounting的源代码。因此,Alfred修复了如下错误:
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
foreach(var rectangle in rectangles)
{
if (typeof(rectangle) == Rectangle)
{
rectangle.Length = rectangle.Length * 1.1;
}
if (typeof(rectangle) == Square)
{
rectangle.Length = rectangle.Length * 1.04880884817;
}
}
}
阿尔弗雷德(Alfred)对他的超级黑客技术感到满意,而吉尔(Jill)则表示该错误已修复。
下个月没有人得到报酬,因为会计依赖于能够对IncreaseRectangleSizeByTenPercent
方法进行平方并增加21%的面积。整个公司都进入“优先级1错误修正”模式,以跟踪问题的根源。他们将问题追溯到阿尔弗雷德(Alfred)的解决方法。他们知道必须使会计和广告两全其美。因此,他们通过使用方法调用识别用户来解决问题,如下所示:
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
IncreaseRectangleSizeByTenPercent(
rectangles,
new User() { Department = Department.Accounting });
}
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
foreach(var rectangle in rectangles)
{
if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
{
rectangle.Length = rectangle.Length * 1.1;
}
else if (typeof(rectangle) == Square)
{
rectangle.Length = rectangle.Length * 1.04880884817;
}
}
}
等等等等。
这个轶事是基于每天面对程序员的现实情况。违反Liskov Substitution原则可能会引入非常细微的错误,这些错误只有在编写多年后才会被发现,到那时修复此违规将破坏一堆事情,而未修复它会激怒您的最大客户。
有两种解决此问题的现实方法。
第一种方法是使Rectangle不可变。如果Rectangle的用户无法更改Length和Width属性,则此问题将消失。如果要使用不同长度和宽度的矩形,可以创建一个新的矩形。正方形可以愉快地从矩形继承。
第二种方法是打破正方形和矩形之间的继承链。如果将正方形定义为具有单个SideLength
属性,而将矩形定义为具有Length
和Width
属性并且没有继承,则不可能通过期望矩形并获得正方形来意外破坏事物。用C#术语来说,您可以使用seal
矩形类,以确保您获得的所有矩形实际上都是矩形。
在这种情况下,我喜欢解决问题的“不可变对象”方法。矩形的标识是矩形的长度和宽度。有意义的是,当您要更改对象的标识时,您真正想要的是一个新对象。如果您失去了一个老客户并获得了一个新客户,则无需将Customer.Id
字段从老客户更改为新客户,而是创建一个新客户Customer
。
违反Liskov替换原理在现实世界中很常见,这主要是因为那里的许多代码是由不称职/在时间压力下/不在乎/犯错误的人编写的。它会并且确实会导致一些非常令人讨厌的问题。在大多数情况下,您宁愿使用组合而不是继承。