设计原则(Java 与模式-笔记 一)

 

第二部分 面向对象的设计原则

 

如何同时提高一个软件系统的可维护性Maintainability)和可复用性Reuseability)是面向对象的设计要解决的核心问题。

一个好的系统设计应该有如下的性质:可扩展性(Extensibility)、灵活性(Flexibility)、可插入性(Pluggability

系统的可扩展性是由“开-闭”原则、里氏代换原则、依赖倒转原则和组合/聚合复用原则所保证的。

恰当地提高系统的可复用性,可以提高系统的灵活性。在一个设计得当的系统中,每一个模块都相对于其他模块独立存在,并只保持与其他模块的尽可能少的通信。这样一来,在其中某一个模块发生代码修改的时候,这个修改的压力不会传递到其他的模块。

系统的灵活性是由“开-闭”原则、迪米特法则、接口隔离原则所保证的。

恰当地提高系统的可复用性,可以提高系统的可插入性。在一个符合“开-闭”原则的系统中,抽象层封装了与商业逻辑有关的重要行为,这些行为的具体实现由实现层给出。当一个实现类不再满足需要,需要以另一个实现类取代的时候,系统的设计可以保证旧的类可以被“拔出(Unplug)”,新的类可以被“插入(Plug)”。

系统的可插入性是由“开-闭”原则、里氏代换原则、依赖倒转原则和组合/聚合复用原则所保证的。

 

1、“开-闭原则”(Open-Closed Principle,缩写为OCP

“开-闭”原则:一个软件实体应当对扩展开放,对修改关闭

如何实现?

1)抽象化是关键

Java 语言里,可以给出一个或多个抽象 Java 类或Java 接口,规定出所有的具体类必须提供的方法的特征作为系统设计的抽象层。这个抽象层预见了所有的可能扩展,因此,在任何扩展情况下都不会改变。这就使得系统的抽象层不需要修改,从而满足了“开-闭”原则的第二条:对修改关闭。

同时,由于从抽象层导出一个或多个新的具体类可以改变系统的行为,因此系统的设计对扩展时开发的,这就满足了“开-闭”原则的第一条:对扩展开放。

2)对可变性的封装原则

找到一个系统的可变因素,将它封装起来。一种可变性不应当散落在代码的很多角落里,而应当被封装到一个对象里面。一种可变性不应当与另一种可变性呼喝在一起。所有的类图的继承接口一般不会超过两层,不然就意味着将两种不同的可变性混合在了一起。

相关的设计模式:策略模式、简单工厂模式、工厂方法模式、抽象工厂模式、建造模式、桥梁模式、门面模式、调停者模式、访问者模式迭代子模式。

当读者学习设计模式的时候,要学会问一个问题:这个设计模式可以对什么样的变换开放,以及它做到这一点所付出的代价是什么。通过这样的思考,可以更加透彻地了解这种模式对“开-闭”原则的支持程度,以及这种设计模式本身。

当代码包含大段大段的代码转移语句块往往意味着某种可变性。可以将这种可变性用多态代替,就意味着将这种可变性封装起来。但是如果一个条件转移语句没有涉及重要的商务逻辑,或者不糊随着时间的变化而变化,也不意味着任何的可扩展性,那么它就没有涉及任何有意义的可变性,这时候运用多态性就是毫无意义的。

 

附加专题内容:

Java 接口的常见的用法:单方法接口、标识接口、常量接口

在设计中,只要有可能,不要从具体类继承。从继承的等级结构里面,树叶节点均应当是具体类,而树枝节点均应当是抽象类(或接口)。这样的设计是所有的Java 设计师都应当努力做到的。

继承使用的条件,当以下的条件全部满足时:

1)子类是超类的一个特殊种类,而不是超类的一个角色,也就是要区分“Has-A”与“Is-A”两种关系的不同。Has-A关系应当使用聚合关系描述,而只有Is-A关系才符合继承关系。

2)永远不会出现需要将子类换成另一个类的子类的情况。如果设计师不是很肯定一个类会不会在将来变成另一个类的子类的话,就不应当将这个类设计成当前这个超类的子类。

3)子类具有扩展超类的责任,而不是具有置换掉(Override)或者注销掉(Nullify)超类的责任。如果子类需要大量地置换掉超类的行为,那么这个子类不应当成为这个超类的子类。

4)只有在分类学角度上有意义时,才可以使用继承,不要从工具类继承。

 

2、里氏代换原则(Liskov Substitution Principle,缩写为LSP

里氏代码原则的严格表达是:如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。换而言之,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别

里氏代换原则讲的是基类与子类的关系。如果有两个具体类A之间的关系违反了这一原则的设计,根据具体情况可以在下面的两种重构方案中选择一种。

 


设计原则(Java 与模式-笔记 一)
 相关的设计模式
:策略模式、合成模式、代理模式。

 

3、依赖倒转原则(Dependence Inversion Principle,缩写为DIP

依赖倒转原则讲的是:要依赖于抽象,不要依赖于具体。也就是说抽象不应当依赖于细节;细节应当依赖于抽象。另一种表述是,要针对接口编程,不要针对实现编程。

下图是违反依赖倒转原则的设计:

 


设计原则(Java 与模式-笔记 一)
 正确的设计应该是:

 


设计原则(Java 与模式-笔记 一)
 相关设计模式
:工厂方法模式、模板方法模式、迭代子模式。

依赖倒转原则的缺点是因为依赖倒转的缘故,对象的创建很可能要使用对象工厂,以比秒对具体类的直接引用,此原则的使用还会导致大量的类。

 

4、接口隔离原则(Interface Segreation Principle,缩写为ISP

接口隔离原则讲的是:使用多个专门的接口比使用单个的总接口要好。换而言之,从一个客户类的角度来讲,一个类对另外一个类的依赖性应当是建立在最小的接口上的。

接口隔离原则的一般常用的有:角色的合理划分、定制服务

 

5、合成/聚合复用原则(Composite/Aggregate Reuse Principle,缩写为CARP

合成/聚合复用原则就是:在一个新的对象里面使用一些已有的对对象,使之成为新对象的一部分,新的对象通过向这些对象的委派达到复用已有功能的目的。另一种表述是:尽量使用合成/聚合,尽量不要使用继承

合成和聚合的区别:聚合用来表示“拥有”关系或者整体与部分的关系;而合成则用来表示一种强得多的“拥有”关系,在合成关系里,部分和整体的生命周期是一样的。

 

6、迪米特法则(Law of Demeter,缩写为LoD

迪米特法则又叫做最少知识原则,就是说,一个对象应当对其他对象有尽可能少的了解

狭义的迪米特法则:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。

下图是一个不满足迪米特法则的系统:

 


设计原则(Java 与模式-笔记 一)
 满足地卡特法则的系统应该为:

 


设计原则(Java 与模式-笔记 一)
 上图中使用了Friend 类作为中间层,间接调用Strange 类的方法。这样使得调用的具体细节被隐藏在Friend内部,从而使SomeoneStrange 之间的直接联系被省略掉了。这样一来,使得系统内部的耦合度降低。

但这样做有个明显的缺点:会在系统里造出大量的小方法,散落在系统的各个角落。这些方法仅仅是传递间接的调用。

广义的迪米特法则:其实,迪米特法则所谈论的,就是对对象之间的信息流量、流向以及信息的影响的控制。

在软件系统中,一个模块设计得好与不好的最主要、最重要的标志,就是该模块在多大的程度上将自己的内部数据和其他与实现有关的细节隐藏起来。一个设计得好的模块可以将它所有的实现细节隐藏起来,彻底地将提供给外界的API和自己的实现分隔开来,而不理会模块内部的工作细节。这一概念就是“信息的隐藏”,或者叫封装

迪米特法则的主要用意是控制信息的过载。在将迪米特法则运用到系统设计中时,要注意下面的几点:

1)在类的划分上,应当创建有弱耦合的类。类之间的耦合越弱,就越有利于复用。一个处在弱耦合中的类一旦被修改,不会对有关系的类造成波及。

2)在类的结构设计上,每一个类都应当尽量降低成员的访问权限。换言之,一个类包装好各自的private 状态。这样一来,想要了解其中的一个类的意义时,不需要了解更多别的类的细节。一个类不应当public 自己的属性,而应当提供取值和赋值方法让外界间接方法自己的属性。

3)在类的设计上,只要有可能,一个类应当设计成不变类

4)在对其他类的引用上,一个对象对其对象的引用应当降低到最低

相关设计模式:门面模式、调停者模式。

 

   这篇总结是我在一年前从 Java 与模式书上摘录的,自己觉得有用,容易忘记的,有时间便记录总结下。发表在这主要是为了方便以后能更好的回顾,还有几篇也将陆续整理下发表。。by zhxing