关于LSP的思考(boke5)

本文详细介绍了里氏替换原则(LSP),它是面向对象设计的基本原则之一,确保子类可以替换基类而不影响程序功能。内容涵盖了LSP的定义、参数逆变与返回值协变、异常处理以及方法的预后条件和不变量。通过实例分析了如何在实践中遵循LSP,强调了子类方法必须保持与基类相同的行为约束,以实现完全的可替换性。
摘要由CSDN通过智能技术生成

LSP(Liskov substitution principle)又名里氏替换原则。

        里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。(摘自百度百科)

直观来讲,LSP就是指我们在写子类时需要遵守的规则,它要求子类在使用时可以完全替换父类。然后提出了具体的几点行为规定:

        

        其实这几条原则很容易理解,只需要把握“替换”这个关键词就好了。

        我的理解是,如果满足了LSP的话,那么可以进行完全的替换。假想这样一个场景:你已经围绕这个父类型写了许多代码,将其所有的方法全部调用过了,而且它也是你代码的核心部分。如果你设计的子类型满足LSP的话,我们就可以在声明示例时,只将构造方法改为子类型的构造方法,而其他的代码完全不变,这样仍然能保证我们的程序按照与原来一样的结果正确的跑下去。

        接下来,我将围绕我对LSP的理解,来分析一下LSP具体实践时需要遵守的原则。

        先说关于方法的参数(摘自维基百科):

  • Contravariance of method parameter types in the subtype.
  • Covariance of method return types in the subtype.
  • New exceptions cannot be thrown by the methods in the subtype, except if they are subtypes of exceptions thrown by the methods of the supertype.

        传入的参数要求“逆变”,也就是可以是原来的参数的父类型或者不变;而返回值则是协变,即原来参数类型的子类型或者协变;抛出的异常可以是原来的子类型,也可以不抛出异常。

        关于逆变和协变,其实就是一个is-a的关系:我们可以用一个父类型的引用来指向一个子类型的对象。参数逆变,这样原先传入的子类型参数依然可以被我们重写的方法中对应的引用接受;返回值协变,那么我们返回的子类型可以被原先代码中的父类型引用接收,逻辑上是自洽的。而对于异常,协变同理;如果不抛出的话,相当于原来代码中处理异常的代码不会被执行到,但是可以正常运行。当然,这里的逆变和协变,父子类型同样需要满足LSP。

        但是仅仅是参数满足要求还不够,还需要满足一些行为条件:

  • Preconditions cannot be strengthened in the subtype.
  • Postconditions cannot be weakened in the subtype.
  • Invariants must be preserved in the subtype.
  • History constraint (the "history rule"). Objects are regarded as being modifiable only through their methods (encapsulation). Because subtypes may introduce methods that are not present in the supertype, the introduction of these methods may allow state changes in the subtype that are not permissible in the supertype. The history constraint prohibits this. It was the novel element introduced by Liskov and Wing. (摘自维基百科)

1.关于pre/post-conditions

        我自己一开始理解有些偏差,单纯的认为preconditions就是我们输入的参数具有的性质,而postconditions是我们返回值或抛出异常需要满足的要求。但是再看了那个“正方形不是长方形”的经典例子后,发现这样的认识是不完善的。

        用哲学的角度去理解。实践的要素有主体、客体和中介,执行一个方法也可以看作一次实践。主体就是我们的ADT实例,客体则是传入的参数,而加工后变为返回值。那么我们的conditions就不仅包含客体的conditions,也包含主体的conditions了。

        回想“正方形不是长方形“的例子,或者看一下这篇。对于SetWidth方法,长方形中执行该方法后,其高度并不会改变,因此后置条件应该是:宽度改变为对应宽度,高度不变。而在正方形中,后置条件时:宽度和高度都设置为对应值。显然,这俩个后置条件是不一样的,在执行完该方法后,实例的状态不能等价,故违反了LSP原则。

        那么怎样才可以满足呢?我的理解是,对于父类型所包含的Rep空间,子类型在执行任何父类型中方法后,所得到的Rep空间应该同父类型执行对应方法所得到的结果是一样的,这样子类型就可以完全当作一个父类型来使用。而正方形的例子显然不满足。

2.关于RI

        关于RI。我们知道RI是对Rep有效值域的约束,在这里我的理解是:父类型RI约束的值域中合法的对象,在子类型中必须有完全的对应,也就是满足从子类型到父类型的一个满射。这样在我们原先使用父类型的情景下,都可以找到一个对应的子类型实例来进行替换了。那么子类型中可以存在父类型RI约束之外的Rep吗?关于这一点我认为是可以的,但是要满足一个前提:postcondition和procondition必须满足要求。这样的话,如果我们单纯使用父类型中的方法的话,得到的Rep值域仍然在父类型的RI约束范围之内,是可以完全替换的;而我们的子类型还可以在更多的场景下使用。

        再回头看正方形、长方形的例子。正方形的Rep只是长方形的一个子集,如果有些场景下必须用到长宽不一致的长方形,那么正方形的实例是不可以替代的,因此不满足LSP。那么长方形可以替代正方形吗?我认为是可以的,只需要在重写正方形中方法时,同步地设置长和宽俩个属性就好了。

3.关于Invariant

        当我们的Rep和各重写的方法满足上面的俩个约束条件后,那么父类型的Invariant也自然能够满足了。

4.关于历史约束

        这个条件是一个更强的约束条件。它的含义是:如果一些Rep的RI由父类的方法来维护,那么子类中不应该增加修改这些Rep的方法。这个约束比我上面写的第二点中的约束条件更强。

A violation of this constraint can be exemplified by defining a mutable point as a subtype of an immutable point. This is a violation of the history constraint, because in the history of the immutable point, the state is always the same after creation, so it cannot include the history of a mutable point in general. Fields added to the subtype may however be safely modified because they are not observable through the supertype methods. Thus, one can define a circle with immutable center and mutable radius as a subtype of an immutable point without violating the history constraint.

        维基百科给出了这样的例子,很好理解就不翻译了。

        也就是说,父类型中声明的field,子类型中新增的方法不能对其进行更改,只有对父类型中重写的方法才可以更改这些field,而依据之前条件,这些重写的方法还必须满足之前的pre/post-conditions。这样可以严格地保证子类型完全替换掉父类型,而不用考虑是否使用的子类型中特有的方法来破坏了这种可替换性。

        我的理解是,可以,但没必要。在JAVA中,如果我们使用父类型的引用来指向子类型的对象,那么只能使用父类型的方法,自然不会出现上面考虑的情况。当然,如果能满足历史约束,那自然是更好了。

        而我们增加field的行为,相当于在Rep中增加维度,而我们子类型中可以任意增加对这个新的维度Rep的操作,只要增加后的几何空间,在投射到父类型Rep空间中,依然满足第二点中的约束,那就是满足LSP的。


        最后,在这篇里没有放任何代码,只是结合这几天的做题和思考,结合一些查询的资料和老师的讲解,做了一下自己理解的总结。希望大家不吝赐教,指出我的错误。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值