• 敏捷开发原则与实践(五)之替换原则


    替换原则(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。

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

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

  • 相关阅读:
    iOS之由身份证号返回性别
    iOS之获取经纬度并通过反向地理编码获取详细地址
    iOS之获取App的LaunchImage
    iOS之UI组件整理
    iOS之在写一个iOS应用之前必须做的7件事(附相关资源)
    The ADB instructions
    The packaging and installation process of Android programs
    SDK Manager
    JVM vs DVM
    Android architecture
  • 原文地址:https://www.cnblogs.com/ustbdavid/p/3436688.html
Copyright © 2020-2023  润新知