设计模式 学习笔记 之四

第4章 里氏替换原则

里氏替换原则(Liskov Substitution Principle)可以如下表述:子类必须能够顶替它的父类。

里氏替换原则关注的是面向对象设计中的一个重要方面:继承关系。里氏替换原则是判断是否应该使用把两个类建模成继承关系的准则。

在具体应用里氏替换原则,我们最关键的是应用“行为分析”,即判断子类的行为是否与父类的行为完全一样。如果两者在行为上有哪怕一点点偏差,那么就应该怀疑它们之间是否真地应该被建模成继承关系。

一个经典的例子是矩形和正方形。由于受数学观念中“正方形是矩形”这一思想的影响,人们在使用计算机对两者建模时也习惯性地把正方形建模成矩形的子类。但是,正如里氏替换原则告诉我们的,一定要从“行为”上而不是数学概念上来分析这个模型。从行为上讲,矩形有长和宽的概念,且两者没有关联,可自由变化;而正方形只有边长的概念,四条边一同变化。所以,两者在行为上是不同的,不应该用继承关系来给两者建模。

除了行为分析,我们还有一些启发式的方法来判断违背里氏替换原则的常见情形:

  • 当看到派生类覆盖了基类的方法但在方法体却为空(即不做任何动作)时,引起警惕。这种情形说明子类与父类的行为已经出现了偏差。
  • 当看到派生类覆盖了基类的方法,并让该方法抛出基类方法所没有抛出的异常时,引起警惕。这种情况也说明了子类的行为与父类不一致。

第5章 依赖抽象原则

依赖抽象原则(Dependent-on-Abstraction Principle)可被表述为:

  • 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
  • 抽象不应该依赖于细节,细节应该依赖于抽象。

依赖抽象原则其实我们并不陌生,相反,我们实际上已在前面几章中实践过这个原则:当需求发生变化时,对变化的需求进行共性变性分析,把共性用抽象接口表示出来,让客户代码依赖于这个抽象接口而不再依赖于具体实现细节,这就是依赖抽象原则。

下面我们再具体看看依赖抽象原则的两个不同的表述给我们的启示。

依赖抽象原则在层次化结构中的体现

在第一章中我们已经讲到设计优良的系统是具有层次性的,而不会是钢板一块。层次化的系统通常在初始设计时都是让上层依赖于下层,上层的模块使用下层模块提供的服务。这时,上层是客户层,而下层是服务提供者层。如下图示。

clip_image002

但是,当需求变化发生时,往往是处于下层的服务提供者模块发生变化。由于在初始设计中,上层直接依赖于下层,因此下层的变化就会向上传递给上层,使得客户层也要跟着修改。如果任由这种情况发展下去,当再一次下层发生变化时,上层模块又要再次修改。这已经明显违背了优良设计“松散耦合”的设计理念。

怎样才能阻止这种情况频繁发生?答案还是:共性变性分析。将需求变化时上层客户模块对下层模块的那些不会发生变化的要求抽象出来,形成一个抽象接口。上层客户模块现在就依赖于这个抽象接口,而下层模块作为服务提供者,就必须要去实现上层客户模块所规定好的这个抽象接口,这样它才是一个合格的服务提供者。因此,新的设计变成:

clip_image004

这个新设计的好处是显而易见的。首先,上层客户模块与下层服务提供者模块实现了解耦。下层服务者模块的变化现在不会传递给上层模块了,这样系统使用了松散耦合。其次,由于上层客户模块现在无需关心下层模块的实现细节,下层模块也无需关心它是如何被上层所使用,因此双方都各司其职,集中精力做好各自的事情,系统的复杂性得以降低,扩展性增强。

细节应该依赖于抽象

为什么是细节依赖于抽象,而不是抽象依赖于细节?我们可以从上一小节的设计重构中找到一点启示。在上面这个例子中,ServiceInterface是应客户模块的要求而生的,它的解释权是归客户模块所有的。显然,当客户模块的需要发生变化时,这个接口也会随之变化以体现客户模块的新要求。因此,客户模块的需要决定了接口。而下层模块作为服务提供者,理所当然要满足接口所规定的要求,实现接口所规定的功能。当接口发生变化时,下层模块也需要作出相应的改变,才能继续充当一个合格的服务提供者,也就是说:细节是依赖于抽象的。

posted @ 2011-04-09 09:42  李嘉 (Justin)  阅读(239)  评论(0编辑  收藏  举报