类之间的关系

一般来说,类之间的关系有:依赖、关联、聚集、组合、泛化等。

1. 依赖(dependency)

假如有两个元素 X 和 Y,如果修改 Y 元素的定义会导致 X 元素的修改,那么 X 依赖 Y。Java 中依赖关系常常表现为一个类 X 中某个方法的参数类型为 Y,那么称 X 依赖于 Y。如下图所示:
dependency

2. 关联(association)

关联是对具有相同的结构特性、行为特性、关系和语义的链 (link) 的描述。关联表示的是类与类之间的关系,链表示的是对象与对象之间的关系。
Association

如上图所示,常见的关联关系有单向关联和双向关联。单向关联在 Java 中可以理解为类 Earth 中有一个类型为 Satelite 的变量 moon。而双向关联可以理解为两个类中互有对方类型的变量,如上图中 Father 类中有一个 Son 类型的变量,而 Son 类中有一个 Father 类型的变量。

关联本身也可以有特性,可以通过关联类进一步进行描述。关联还可以加上限定和约束。关联按照种类还可以分为:自返关联(递归关联)、二元关联和 N 元关联。

关联也是一种依赖关系,但是在有关联关系的情况下我们只需要画出关联关系而不需要画出依赖关系。

3. 聚集(aggregation)

聚集是一种特殊形式的关联。聚集表示类之间整体与部分的关系,对应于语义“包含”、“组成”表述等。
aggregation

4. 组合(composition)

组合同样表示类之间整体与部分的关系,但是组合关系中整体和部分有着相同生命周期
Composition

5. 泛化(generalization)

泛化定义了一般元素和特殊元素之间的分类关系,在 Java 中表现为继承(父类)或者实现(接口)。
Generalization

设计模式——六大设计原则

六大设计原则

  1. 单一职责原则
  2. 里氏替换原则
  3. 依赖倒置原则
  4. 接口隔离原则
  5. 迪米特法则
  6. 开闭原则

1 单一职责原则(Single Responsibility Principle)

单一职责,简称SRP。单一职责的定义是:应该有且仅有一个原因引起类的改变。单一职责最难划分的就是职责,一个职责一个接口,但是职责的划分因项目而异。对于接口,我们在设计的时候一定要做到单一,但是对于实现类尽量做到只有一个原因引起变化。实现类生搬硬套单一职责会引起类的剧增。

2 里氏替换原则(Liskov Substitution Principle)

里氏替换原则,简称LSP。通俗来讲,只要父类能出现的地方子类就能出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本不需要知道是父类还是子类。但是有子类出现的地方,父类未必能适应。

里氏替换原则包含四重含义:

1) 子类必须完全实现父类的方法。在类中调用其他类时务必使用父类或接口,如果不能使用父类或接口,则说明违背了LSP原则。

2) 子类可以有自己的个性。

3) 覆盖或实现父类的方法时参数可以被放大。里氏替换原则要求制定一个契约,就是父类或者接口,这种设计方法也叫做Design by Contract (契约设计),与里氏替换原则有着异曲同工之妙。子类中方法的前置条件必须与超类中被覆写的方法的前置条件相同或者更宽松。

4) 覆写或实现父类的方法时输出结果可以被缩小。

3 依赖倒置原则(Dependence Inversion Principle)

依赖倒置原则,简称DIP。其含义为:高层模块不应依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。更加精简的定义就是“面向接口编程”。

两个类之间有依赖关系,只要制定出两者之间的接口(或抽象类)就可以独立开发了,而且项目之间的单元测试也可以独立得运行,而TDD (Test-Driven Development,测试驱动开发) 开发模式就是依赖倒置原则的最高级应用。

抽象是对实现的约束,对依赖者而言,也是一种契约。不仅约束自己,还同时约束自己与外部的关系,其目的是保证所有的细节不脱离契约的范畴,确保约束双方按照既定的契约(抽象)共同发展。

依赖传递的三种方法:

1) 构造函数传递依赖对象。在类中通过构造函数声明依赖对象,按照依赖注入的说法这种方式叫做构造函数注入。

2) Setter方法传递依赖对象。在抽象中设置Setter方法声明依赖关系,也可称为Setter依赖注入。

3) 接口声明依赖对象,也称为接口注入。

依赖倒置原则的本质就是通过抽象(接口或者抽象类)使各个类或模块的实现彼此独立,不相互影响,实现模块间的松耦合。我们可以遵循以下规则:每个类尽量有接口或者抽象类,或者两者都具备;变量的声明类型尽量是接口或者抽象类;任何类都不应从具体类派生;尽量不要覆写基类方法;结合里氏替换原则可以得到如下规则:接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确地实现业务逻辑,同时在适当的时候对父类进行细化。

4 接口隔离原则(Interface Segregation Principle)

接口隔离原则,其含义为:客户端应该依赖它需要的接口,仅提供需要的接口,剔除不需要的,细化需要的接口,保证其纯洁性。接口中的方法应该尽量少。

单一职责要求的是类和接口职责单一,注重的是职责,这是逻辑业务上的划分,而接口隔离原则要求接口方法尽量少。

保证接口的纯洁性要求

1) 接口尽量小。根据接口隔离原则拆分接口时,首先必须满足单一职责原则。

2) 接口要高内聚。在接口中尽量少公布public方法,接口是对外的承诺,承诺越少对开发越有利,变更风险越小,同时有利于降低成本。

3) 定制服务。单独为一个个体提供优良的服务,只提供访问者需要的方法。

4) 接口设计是有限度的。接口设计的粒度要适度。

5 迪米特法则(Law of Demeter)

迪米特法则,简称LoD,也被称为最少知识原则(Least Knowledge Principle,LKP)。一个类应该对自己耦合或调用的类知道得最少。

迪米特法则要求尽量不要对外公布太多的public方法和非静态的public方法,多使用private、package-private、protected等访问权限。

如果一个方法放在本类中,既不增加类间关系,也不对本类产生负面影响,那么就放置在本类中。

迪米特法则的核心就是类间解耦,弱耦合。只有弱耦合以后,类的复用率才能提高。其要求的结果就是产生大量的中转或者跳转类,导致系统的复杂度提高,同时也为维护带来难度。

6 开闭原则(Open Closed Principle)

开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块修改必然有高层模块耦合,否则就是一段无意义的代码。

变化可以归纳为三种类型:

1) 逻辑变化。只变化一个逻辑,而不涉及其他模块,可以通过修改原有类中的方法来实现。前提条件是所有依赖或者关联类都按照相同的逻辑处理。

2) 子模块变化。低层次模块的变化必然引起高层次模块的变化。因此在通过扩展完成变化时,高层次的模块修改是必然的。

3) 可见视图的变化。

如何使用开闭原则:

1) 抽象约束。通过接口或抽象类可以约束一组可能变化的行为,并且能够实现开放。其包含三层含义:第一,通过接口和抽象类对扩展进行边界的限定,不允许出现在接口或抽象类中不存在的public方法;第二,参数类型、引用对象尽量使用接口或者抽象类而不是实现类;第三,抽象层保持稳定,一旦确定就不允许修改。

2) 元数据(metadata)控制模块行为。

3) 制定项目章程。

4) 封装变化。