前置知识:UML 关系
- 车的类图结构为
<<abstract>>
,表示车是一个 抽象类。
在 UML 类图中,抽象类 通常用 斜体字 表示,或在 类名前加上
<<abstract>>
标识。
- 小汽车和自行车与车是 继承关系;更具体为 实现关系,使用 带空心箭头的虚线 表示。
- 小汽车与 SUV 之间也是 继承关系,它们之间的关系为 泛化关系,使用 带空心箭头的实线 表示。
继承关系 为 is-a 的关系,两个对象之间如果可以用 is-a 来表示,就是继承关系:(…是…)
eg:自行车是车、猫是动物
实现关系(Realization) 与 泛化关系(Generalization):
- 泛化关系(Generalization):表示 类与类之间的继承关系,使用带空心三角箭头的实线表示。
- 实现关系(Realization):表示 类实现接口 的关系,使用带空心三角箭头的虚线表示。
- 小汽车与发动机之间是 组合关系,使用 带实心菱形箭头的实线 表示。
- 学生与班级之间是 聚合关系,使用 带空心菱形箭头的实线 表示。
组合关系(Composition) 与 聚合关系(Aggregation)
- 组合关系表示 整体与部分的强依赖关系,使用 带实心菱形箭头的实线 表示。
其中部分对象的生命周期完全依赖于整体对象,部分对象不能独立存在,部分对象不能被多个整体对象共享。小汽车由发动机等部件组成,发动机不能脱离小汽车独立存在。
- 聚合关系表示 整体与部分的弱依赖关系,使用 带空心菱形箭头的实线 表示。
部分对象可以独立于整体对象存在,部分对象可以同时属于多个整体对象。一个班级由多个学生组成,但学生可以在没有班级的情况下存在,或者转到其他班级。
- 学生与身份证之间为 关联关系,使用 一根实线 表示。根据需要,可以添加箭头表示方向。
- 学生上学需要用到自行车,与自行车是一种 依赖关系,使用 带箭头的虚线 表示。
关联关系(Association) 与 依赖关系(Dependency)
- 关联关系表示两个类之间存在某种 结构性的联系(持久存在于类的整个生命周期中),一个类知道另一个类的属性和方法。使用 实线 连接两个类,可以根据需要添加箭头表示方向。
一个 Student 类有一个 IDCard 类型的 成员变量,表示 Student 与 IDCard 之间的关联关系。
- 依赖关系表示 一个类在其实现中 临时使用 了另一个类,即一个类的变化可能会影响到另一个类的实现。使用 带箭头的虚线,箭头从依赖方指向被依赖方。
一个 Student 类的
goToSchool(Bicycle bike)
方法中 使用了 Bicycle 类的实例,表示 Student 依赖于 Bicycle。
什么是设计模式
“每一个模式描述了一个在我们周围 不断重复发生的问题,以及 该问题的解决方案的核心。这样,你就能 一次又一次地使用该方案而不必做重复劳动”。
—— Christopher Alexander
历史性著作《设计模式:可复用面向对象软件的基础》一书书系统地总结了 面向对象软件设计中的 23 种经典设计模式,旨在提供 可复用的解决方案。
这些模式被分为三大类:
- 创建型模式:关注对象的创建过程,封装了对象的实例化方式。
- 结构型模式:关注类和对象的组合,旨在实现更大的结构。
- 行为型模式:关注对象之间的通信和职责分配。
由埃里希·伽玛(Erich Gamma)、理查德·赫尔姆(Richard Helm)、拉尔夫·约翰逊(Ralph Johnson)和约翰·弗利西德斯(John Vlissides)四位作者合著,因而被称为 “四人帮”(Gang of Four,简称 GoF) 。
书名里有两个关键字,
- 可复用:这是设计模式的目标,不能忘记这个目标呀!
- 面向对象:是具体的方法。
软件设计复杂性的根本原因,就是 变化。如,客户需求的变化、技术平台的变化、市场环境的变化…
人们面对 复杂性 有两个常见的做法,
- 分解:分而治之,将大问题分解为多个小问题,将复杂问题分解为多个简单问题;
- 抽象:光分解是不够的。由于不能掌握全部的复杂对象,我们选择 忽视它的非本质细节,而去处理泛化和理想化了的对象模型。
分而治之的方法是不容易复用的,将来再来一种新的情况,就要再对它处理。比如,有 Line 类、Rect 类,再来一个 Circle 类,它们各自实现自己的 draw()
方法,但是如果是抽象成一个 Shape 类,其他 Line 类、Rect 类、Circle 类 符合这个 Shape 的抽象,那么就继承自它,这样就能对形状 统一处理。
设计模式就像能 根据需求进行调整 的预制蓝图, 可用于解决代码中 反复出现的设计问题。模式并不是一段特定的代码, 而是解决特定问题的一般性概念。
人们常常会混淆 模式和算法, 因为两者在概念上都是 已知特定问题的典型解决方案。 但算法总是明确定义达成特定目标所需的一系列步骤, 而模式则是对解决方案的更高层次描述。 同一模式在两个不同程序中的实现代码可能会不一样。
算法更像是菜谱: 提供达成目标的明确步骤。而模式更像是蓝图: 你可以看到最终的结果和模式的功能, 但需要自己确定实现步骤。
从面向对象谈起
我们程序猿,作为人类需求和计算机的桥梁,和计算机的沟通叫底层思维,和人类需求的沟通叫抽象思维。
- 底层思维:向下,如何把握 计算机底层,比如语言构造、编译转换、内存模型、运行时机制;
- 抽象思维:向上,如何将我们 周围的世界抽象为程序代码,比如 面向对象、组件封装、设计模式、架构模式。
-
向下:深入理解 三大面向对象机制
- 封装,隐藏内部实现;
- 继承,复用现有代码;
- 多态,改写对象行为。
-
向上:深刻把握面向对象机制所带来的抽象意义,理解 如何使用这些机制来表达现实世界,掌握什么是 好的面向对象设计。
面向对象设计原则
面向对象要能
- 隔离变化。面向对象能把 相对稳定的部分 和 容易变化的部分 隔离,能够将变化带来的影响减到最小。比如上面的 Shape 是相对稳定的,而 Line、Rect 容易变化的被隔离起来。
- 多态调用,各负其责。比如上面的 Shape 类,你是 Line 类呢,你就去画直线,自己画自己,就叫各负其责。
原则 1:依赖倒置原则(DIP)—— 面向抽象(接口)编程,而不是面向实现编程
- 高层模块(相对稳定)不应该依赖于底层模块(容易变化),二者都 应该依赖于抽象(相对稳定)。
- 抽象(相对稳定)不应该依赖于实现细节(容易变化),实现细节应该依赖于抽象(相对稳定)。
看下面这个代码,要用鼠标绘图,关注这个 MainForm 类,它依赖 Line 和 Rect 类,这违反了依赖倒置原则,
再来看看遵守依赖倒置原则的长什么样,MainForm 去依赖这个抽象类 Shape,而 Line 和 Rect 类继承 Shape,
- 抽象 就是指 接口或抽象类,两者都是不能直接被实例化的;
- 细节 就是 实现类,实现接口或继承抽象类而产生的类就是细节。
再举个例子,人给动物喂食的场景,
public class CatAnimal {
void eat(){
System.out.println("猫吃鱼");
}
}
public class DogAnimal {
void eat(){
System.out.println("狗啃骨头");
}
}
public class Person {
//人喂动物
void feed(DogAnimal dog){
dog.eat();
}
void feed(CatAnimal cat){
cat.eat();
}
}
public class negtive.AppTest {
public static void main(String[] args) {
Person person = new Person();
person.feed(new DogAnimal());
person.feed(new CatAnimal());
}
}
看下类图,
Person 是直接依赖 Dog 和 Cat 的,若还要喂食其他动物,还需要在 Person 类中去重载 feed 方法。这是妥妥的反例。
// 定义一个动物接口: Animal
public interface Animal {
public void eat();
}
public class CatAnimal implements Animal{
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}
public class DogAnimal implements Animal{
@Override
public void eat() {
System.out.println("狗啃骨头");
}
}
public class Person {
void fead(Animal animal){
animal.eat();
}
}
Person 类不再直接依赖 Dog 和 Cat 了,而是依赖于 Animal 这个接口,当我们还需要去喂食其他动物时,只需要创建个具体动物类去实现动物接口并重写方法就 OK,不用再去重载 Person 的 fead 方法了,实现了解耦(稳定和变化的隔离)。
原则 2:开放封闭原则(OCP)—— 对扩展开放,对更改封闭
对扩展开放,对更改封闭: 类模块应该是可扩展的,但是不可修改。
比如说当需求从画直线变更到画圆圈,那么不应该想着修改 Line,而是应该增加一个 Circle 类,让它继承自 Shape 类。
原则 3:单一职责原则(SRP)—— 一个类只做一件事
一个类应该只有一个引起它变化的原因,变化的方向隐含着类的责任。
原则 4:Liskov 替换原则(LSP)—— 子类必须能够替换它们的基类
子类必须能够替换它们的基类,即 所有使用父类对象的地方,都应该可以透明的替换为子类的对象。
当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有是一个(is-a)关系。
原则 5:接口隔离原则(ISP)—— 接口应该小而完备
不应该强迫客户程序依赖它们不用的方法。
使用 多个专门的接口,而不是总接口。
原则 6:组合优于继承原则 —— 多使用关联,少使用继承
类继承通常为 “白箱复用”,对象组合通常为 “黑箱复用”。
- 继承在某种程度上破坏了封装性,子类父类耦合度高。
- 而对象组合只要求被组合的对象具有良好定义的接口,耦合度低。
多使用关联,少使用甚至不使用继承来达到复用已有对象的目的。
原则 7:封装变化点
使用封装来创建对象之间的分界层,让设计者可以在分界层一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的耦合。