在面向对象过程中,我们通常提到这样的关键词:"封装"、"继承"、"多态"。没错这是面向对象的核心思想,但是细想为何要这样做?首先封装是为了要达到数据保护,即将类的内部结构进行隐藏,对于使用者而言不需要清楚地知道在类的内部实现,仅提供可供访问的方法,对类进行操作。继承是通过对`IS-A`的具体过程,在面向对象过程中,同面相对象最大的区别在于,其需要尽量避免重复工作,以实现复用。继承则是达到复用的手段之一,通过继承子类可以继承父类的方法或某些属性,使得子类可以复用父类的方法或属性而不用重复实现,并额外地添加新的功能。多态基于继承之上,它能够允许通过使用父类的引用而在运行时动态地调用子类的方法,这样的好处在于对于父类而言其无需清楚具体的子类,当使用父类调用方法时,会动态地执行该执行的行为。
以上是面向对象的大致目标,而"设计模式"则是基于面向对象设计的更高一层的设计思路。但是需要注意的是,设计模式并不是一种语法或语言特性,而是一种基于经验的总结积累。以下引用Head First中对设计模式的定义:
A Pattern is a solution to a problem in a context.
定义给出,设计模式是在特定场景下解决某种问题的解决方案,该解决方案是通过积累而给出的建议,而不是强烈要求必须要这样做。在Head First原著中对定义的解释如下:
The context is the situation in which the pattern applies. This should be a recurring situation.
The problem refers to the goal you are trying to archieve in this context, but it refers to any constraints that occur in the context.
The solution is what you're after: a general design that anyone can apply which resolves the goal and set of contraints.
首先"场景"是指模式的所适用的情景,这种情景在软件设计中会重复多次出现。"问题"是指在上述情景下,你需要努力达到的设计目标,目标是指解决实现过程中出现的各种约束性条件。"解决方案"则正是当你面临需要解决一个特定目标和一系列约束时任何人都可以采用的一种通用的模式。因此设计模式是为在软件设计中的某一类特定问题而提出的特定的解决方案,该解决方案是一种通用的模式,类似于在建筑中所广泛采用的某种不成文的规约。
Refactoring time is is Patterns time.
Head First中提出在重构时正是设计模式使用的时候,事实也正如此,设计模式能够尽可能地在不修改或尽少的修改原有系统地情况下,对软件系统进行重构和扩展。但是在进行设计初期,便使用设计模式,对项目的扩展和维护也有巨大帮助。
软件设计原则(Design Principle)
在软件设计过程中是为了应对变化,并且需要识别变化,分离在软件设计中的变化,变化是可以扩展的部分,而使用设计模式适应该变化则是为了能够实现可扩展。
HAS-A can be better than IS-A
Head First中对该原则进行了这样的定义:
Favor composition over inheritance.
在很多中文书籍中,我们称该原则为"合成复用原则",根据Head First的描述,"有一个"的关系比"是一个"的关系更好,并且要善于利用组合去代替继承。合成复用原则对类的依赖关系做了一个要求,即要尽可能地使用弱依赖而不是强依赖,继承是一种静态的并且依赖性极强的关系,而组合较继承具有更低的依赖性,更具有扩展性和灵活性,使用组合模式构建系统,不仅让你可以在类中封装一系列你所期望的算法,而且能够在运行时动态地改变组合的行为,以下是原著的描述:
Creating systems using composition gives you a lot more flexibility. Not only does it let you encapsulate a family of algorithms into their own set of classes, but it also lets you change behavior at runtime as long as the object you're composing with implements the correct behavior interface.
开闭原则(Open-Closed Principle)
原著中对"开闭原则"的优先级是这样描述的:
Grasshopper is on to one of the most important design principles.
说明开闭原则的重要性。
开闭原则的定义如下:
Classes should be open for extension, but closed for modification.
对于类而言需要对扩展开放对修改关闭。"开闭原则"能够保证在我们对系统进行设计时,尽可能少的修改已有的代码,保证原有系统不被破坏,在保证稳定的情况下,对功能进行扩展。
好莱坞原则(The Hollywood Principle)
Don't call me, we'll call you.
不要调用我,我们会调用你。在原著中这句话较为隐晦,其表达的对象是基于父类和子类而言,具体的是:子类不要调用父类,父类会去调用子类。
在原著中有个概念叫"Dependency rot",依赖腐烂,其是这样描述的:
Denpency rot happens when you have high-level components depending on low-level components depending on high-level components depending on sideways components depending on low-level components, and so on.
其核心在于说明,依赖的不合理性,将造成复杂的系统,并且难以理解难以维护。因此好莱坞原则指的是,在软件设计过程中,要保证低层组件依赖于高层组件,而高层组件要于高层组件依赖。这是在告诉我们,在进行设计时,要实现抽象、多态的特性,"we'll call you"指的便是对于父类,其会选择性地去动态地调用子类。
依赖倒置原则(Dependency Inversion Principle)
Depend upon abstractions. Do not depend upon concrete classes.
依赖于抽象,而不是依赖于具体实现。依赖倒置原则,其核心概念便是:面向抽象编程。同好莱坞原则相似的是,其均对依赖于抽象这样一点提出了要求,而该原则基于好莱坞原则的基础上,提出了更加严格的要求。
迪米特法则(Law of Demeter)、最少知道原则(Least Knowledge Principle)
Principle of latest knowledge talk only to your immediate friends.
在最少知道原则下,类仅与其相关的类通信。换句话说便是,一个类应该尽可能少地了解其他类,这是希望能够简化在系统中类与类的之间的依赖,从而将各个类独立性更强,耦合性更低。在这种背景下,对一个类的改造,将不至于导致整个系统所有类的失效或重构。否则系统将难以维护。
UML类图
在面向对象设计中,类图能够清楚的表示类与类之间的关系,而设计模式也是借助类图来描述整个模式中,抽象、具体等的结构关系,因此读懂类图是学习模式前重要的一环。
在UML类图中,实体有以下几种`class`、`interface`、`abstract`,而实体中又有属性、方法等元素。类与类之间的关系有:继承、实现、依赖、关联、组合、聚合,下面我们以具体的类图进行说明。
类、抽象类、接口
如下图中为一个类图的基本结构,在第一层表示为类名称(Class Name),而第二层表示该类的各属性,其中`attr1`为**私有属性,其标识为红色框**,访问权限为`private`的属性将不能由子类继承访问。`attr2`属性为包内可见,或可称为在命名空间内可见,但是该权限使用较少。`attr3`表示为静态公有属性,`public`方法或属性均可被子类继承,并且可被外界直接访问,在类图中通常**以圆形表示公有属性或方法**而`static`属性或方法,则会在**方法名或属性名上添加下划线**以示区分。`attr4`访问权限为`protected`,保护属性可被子类继承并访问,但是不可被外界直接访问,在UML类图中通常使用**棱形表示**。在方法中,同样有以上相同的访问权限设置,其标识在形状上大体相同,可相互对照。通常在类图中,我们可能出现`attr:int`及`method(name:String):void`的写法,其表示的意义相同,均为了表示参数、返回值及其类型。
下图为抽象类的表示方法,在抽象类中允许抽象方法的定义,因此在UML类图中,通常以**斜体**表示一个抽象方法,并且**抽象类类名也以斜体表示**,其他表示与类图一至。
下图为接口的类图表示,接口仅可定义`public`方法,**除静态公有常量属性外不允许有其他属性**,在类图中**接口类名也以斜体表示**,在某些类图工具中,通常会加上`<<interface>>`的标识。
### 继承、实现、依赖、关联、组合、聚合
继承与实现表示方法类似,区别在于其线条,**继承使用实线而实现使用虚线**,通常而言,在表示类关系时,**虚线的耦合比实线耦合更低**。
如图所示,类`Class`实现了借口`Interface`,以**虚线加三角**表示,而`SubClass`继承了`Class`,则以**实线加三角**表示,其中箭头指向父类。
依赖于关联属于相似的关系,其区别也在于线条,**依赖关系使用虚线**,而**关联关系使用实线**,因此依赖的耦合度比关联更低。从图中可以看出,左侧实线表示了关联关系,而右侧虚线表示了依赖关系。
在具体实现中,关联更多表示为在一个类中使用了另一个类的对象作为属性,如在`ClassA`中有属性`attr`类型为`ClassB`,因此为关联关系。而依赖关系其耦合更低,通常表示在一个类中,**另一个类作为其方法参数、返回值或局部变量**,如类图中所示的`method()`方法。
或在如下代码中体现:
```java
public class ClassA {
public ClassB method1() {
return new ClassB();
}
public void method2(ClassB c) {
...
}
public void method3() {
ClassB b = new ClassB();
b.xxx;
...
}
}
```
上述三个方法均是依赖关系的体现,表现为三种形式:**返回值、参数、局部对象**。在关联、依赖关系中,箭头方向指向被关联或被依赖的类,如`ClassA`关联`ClassB`则方向为`A->B`,并且在`A`中存储了`B`的对象,依赖等同。并且关联关系中,通常有`1..n`,`0..n`,`1`,`n`等的表示方法,其表示的为数字端的该类对象在关联类中的数量,如下图中,`1`表示在`ClassA`中维持了一个数量为`1`的`ClassB`对象。
*注:关联还能双向关联,多维关联等,该两种可以不加方向箭头。*
而组合及聚合则是关联的更高一层次的体现,其比关联拥有更高的耦合度,在代码层面上,组合及聚合同关联一样,均会导致类属性的增加。在UML类图中,**聚合关系使用空心棱形加箭头形式**表示,而**组合关系使用实心棱形**,从图形上看,实心比组合具有更高的耦合度。
在图中可以看出,在左侧`HAS-A`关系使用了聚合,而`CONTAINS-A`使用了组合。我们可以这样理解这两者的区别:对于聚合关系,其表示了更低的耦合性,其表示的关系是**整体与个体之间的关系**,如部门与职员的关系;而组合关系,其耦合性更强,其**表示的关系是个体与部分的关系**,如人是个体,手是一个个体中的一个必不可少的组成部分。在UML图中,箭头及箭头方向与关联表示的一致。
从代码层面上分析,在`Java`语言中上述两种关系区分并不明确,只能从意义上分析。从生命周期分析,组合关系中"部分"与"个体"生命周期一致,当"个体"消失时,"部分"也无法单独完成服务。而聚合关系,"个体"与"整体"生命周期相互独立,"个体"即使脱离了"整体"仍可单独地完成服务和行为。在`C++`语言中,两种关系可以使用指针、对象引用区分。
**组合、聚合同关联关系的区别在于类层次不同**,关联关系是无法使用组合、聚合表示时的一种类关系,关联关系的两个类只表明两个类有关系,但是**属于同一个类层次**,如头和手,头可以指挥手进行工作,相当于头与手与关联,但是他们对于人而言,都是属于器官层次,层次相同。而**组合聚合,则类之间属于不同层次**,如手和人,部门与职员。
**综上,上述类关系的类关系强弱为:依赖<关联<聚合<组合<实现<继承。