zoukankan      html  css  js  c++  java
  • 敏捷开发原则与实践(五)之替换原则

    替换原则(Liskov Substitution Princple)是OCP的重要支撑,也是继承关系设计的基本原则。其关键在于,不能单方面的、孤立的思考设计,应多从使用者角度考虑问题。设计类结构时,应该父类与子类之间的可替换性,而不是只考虑ISA关系。

    在OCP背后是抽象和多态,Java中通过继承来支持抽象和多态。那么有没有设计规则来管理继承呢?最好的继承层次是怎样的?有哪些陷阱会导致我们的继承不符合OCP?这些问题就是LCP需要解决的。

    文中Rectangle与Square的例子,从作者角度考虑,Square类中setWidth和setHeight同时设置width和height,这是没问题的。但由于Square继承于Rectangle,就会有使用者开发下面的代码

    void g (Rectangle r) {

        r.setHeight(4);

        r.setWidth(5);

        assertTrue(r.getArea() == 20;

    }

    当传入一个Square对象时,就出错了。我们不能假设每个使用者都清楚Square类中对set方法的修改,毕竟当传入是Rectangle对象时,上述assert是完全合理的。

    SUBTYPES MUST BE SUBSTITUTABLE FOR THEIR BASE TYPES.

    Barbara Liskov first wrote this principle in 1988. She said,

    What is wanted here is something like the following substitution property: if for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

    这里强调的是用程序中对象用子类替换后,其行为不会发生变化。

    还是看上面Rectangle和Square的例子,Square类中setWidth和setHeight同时设置width和height,行为与Rectangle不一致,因此违反了LSP。

    子类和父类行为的原则必须一致,如果出现不一致,说明它们之间就不应该是继承关系。

    LSP强调从用户的角度进行设计。The validity of a model can only be expressed in terms of its clients. 但谁知道用户会作哪些假设呢?很多情况都不好预测,预测过多,就会陷入Needless Complexity中。因此,与其他原则类似,除了明显的违反LSP外,我们只有在嗅到Fragility smell 时才考虑使用该原则进行设计。

    Like all other principles, it is often best to defer all but the most obvious LSP violations until the related Fragility has been smelled.

    那为什么明明Square属于Rectangle的子集,不能进行继承呢?这是因为从行为上看,Square不是Rectangle。面向对象设计时,更注重行为,而不是属性。这也是面向接口编程

    文中Set例子中对LSP的违反,我们在开发中更容易遇到。

    Set已经有两个子类:Bounded Set, Unbounded Set, 这时要添加第三个子类:PersistentSet。PersistentSet将行为委托给了Third Party Persistent Set,但这个第三方set中的元素都继承于Persistent Object。这样就有了一个隐形限制:任何添加到PersistentSet中元素都必须继承于Persistent Object。而之前的Bounded Set和Unbounded Set则没有这个限制,之前运行没有代码的问题,当使用Persistent Set时可能会抛出异常。

    clip_image002

    违反LSP的Set

    也许我们可以通过约定去让开发人员注意到这一点,作者在最开始甚至增加了一个模块用于屏蔽PersistentSet,该模块执行实际内容与PersistentObject之间的转换,开发人员无需知道PersistentSet的存在。但随着使用范围的扩大,还是有部分不知道这个模块的开发人员会直接去使用PersistentSet,导致异常的产生。

    解决方法:

    Factoring instead of Deriving. 当A继承B违反了LSP时,取消继承,提取A和B的共同行为,放到一个新的抽象类中。

    Rebecca Wirfs-Brock, Brian Wilkerson, and Lauren Wiener say:

    We can state that if a set of classed all support a common responsibility, they should inherit that responsibility from a common supperclass.

    clip_image004

    一种解决方案

    很多时候,我们都会折中一下,容忍一下违反LSP的行为,毕竟没有完美的代码。但一定要慎重考虑违反LSP的情况,因为这样会增加系统的复杂度,一旦你违反LSP,你就要单独考虑每个子类。

    最后,当你发现派生类中的退化函数或派生类中抛出异常时,就要想想是否违反了LSP。

    出现退化函数就意味着父类中部分行为子类中不具备,不能完全替换父类。

    派生类中抛出异常则会导致调用者收到计划外的异常。

  • 相关阅读:
    【ybtoj高效进阶 21173】简单区间(分治)
    【ybtoj高效进阶 21170】投篮训练(贪心)(线段树)(构造)
    【ybtoj高效进阶 21172】筹备计划(线段树)(树状数组)
    【ybtoj高效进阶 21168】打字机器(Trie树)(LCA)(值域线段树)
    【ybtoj高效进阶 21169】毁灭计划(分类讨论)(树形DP)
    【ybtoj高效进阶 21167】旅游计划(基环树)(DP)(单调队列)
    Day-15 面向对象02 成员
    Day-14 初识面向对象
    Day 13 内置函数(点击网址进入思维导图)、递归、二分法
    Day12 生成器函数-推导式
  • 原文地址:https://www.cnblogs.com/ustbdavid/p/3436688.html
Copyright © 2011-2022 走看看