本文是学习设计模式时做的笔记,原视频链接:
https://www.bilibili.com/video/BV1G4411c7N4
目录
聚合(Aggregation)和组合(Composition)
设计模式
1.什么是设计模式?
设计模式是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。
2.为什么要使用设计模式?
使用设计模式,程序可以具有更好的:
- 代码重用性:实现相同功能的代码,不需要重复编写;
- 可读性:增强编程的规范性,便于其他程序员阅读;
- 可扩展性:在原程序上添加新的功能非常方便;
- 可靠性:添加新的功能后,对原来的功能没有影响;
- 内聚性、耦合性:即“高内聚、低耦合”。
设计模式包含了面向对象的精髓。
视频采用图解方式讲解设计模式,在学习之前应对UML图有一定了解。
如果已经掌握了UML图,可以跳过此部分。
UML
UML(Unified Modeling Language,统一建模语言),是一种用于软件分析和设计的语言工具。
UML本质上是一套符号规定,这些符号用于描述 软件模型中的各个元素(如类、接口)和它们之间的关系(如泛化、实现)。
UML图分类:
静态结构图:类图、对象图、包图、组件图、部署图。
动态行为图:交互图、状态图、活动图。
其中,类图用来描述类与类之间的关系,在讲解设计模式时,主要是用到类图。
UML类图
UML类图用于描述 系统中的类 本身的组成 和 类之间的各种静态关系。
类的组成
我们将下面这个类画成UML图:
public class Person{
private Integer id;
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
UML类图:
可以看到,UML类图中的类包含的信息有:类名(Person),成员变量(id、name),成员方法(setName、getName)。
其中,成员变量信息包括成员变量名和成员变量类型,两者之间用":"隔开;成员方法信息包括成员方法名、参数列表、返回值类型。
类之间的关系
类之间的关系包括:依赖、泛化、实现、关联、聚合、组合。
依赖:
泛化:
实现:
关联:
聚合:
组合:
依赖(Dependence)
只要在一个类中用到了另一个类,两个类之间就存在依赖关系。
在一个类A中使用另一个类B的方式有:
- B作为A的成员属性
- B作为A中方法接收的参数
- B作为A中方法的返回类型
- A在方法中用到了B
以下面的PersonServiceBean类为例:
public class PersonServiceBean {
private PersonDao personDao;// PersonDao类作为成员属性
public void save(Person person) {// Person类作为方法的接收参数
}
public IDCard getIDCard(Integer personid) {// IDCard类作为方法的返回类型
return null;
}
public void modify() {
Department department = new Department();// Department类在方法中被用到
}
}
class PersonDao {
}
class IDCard {
}
class Person {
}
class Department {
}
以上四种形式都构成依赖关系。
用UML图来表示就是:
依赖关系传递的三种方式:接口、构造器、setter
通过接口:
interface IOpenAndClose { public void open(ITV tv); } interface ITV { public void play(); } class OpenAndClose implements IOpenAndClose { public void open(ITV tv) { tv.play(); } } class ChangHong implements ITV { @Override public void play() { System.out.println("打开长虹电视机"); } } public class Test { public static void main(String[] args) { ChangHong changHong = new ChangHong(); OpenAndClose openAndClose = new OpenAndClose(); openAndClose.open(changHong); } }
通过构造方法:
interface IOpenAndClose { public void open(); } interface ITV { public void play(); } class OpenAndClose implements IOpenAndClose { public ITV tv; public OpenAndClose(ITV tv) { this.tv = tv; } public void open() { this.tv.play(); } } class ChangHong implements ITV { @Override public void play() { System.out.println("打开长虹电视机"); } } public class Test { public static void main(String[] args) { ChangHong changHong = new ChangHong(); OpenAndClose openAndClose = new OpenAndClose(changHong); openAndClose.open(); } }
通过setter方法:
interface IOpenAndClose { public void open(); public void setTv(ITV tv); } interface ITV { public void play(); } class OpenAndClose implements IOpenAndClose { public ITV tv; public void setTv(ITV tv) { this.tv = tv; } public void open() { this.tv.play(); } } class ChangHong implements ITV { @Override public void play() { System.out.println("打开长虹电视机"); } } public class Test { public static void main(String[] args) { ChangHong changHong = new ChangHong(); OpenAndClose openAndClose = new OpenAndClose(); openAndClose.setTv(changHong); openAndClose.open(); } }
输出:
打开长虹电视机
泛化(Generalization)
泛化关系实际上就是面向对象中的继承,它本质上是依赖关系的特例。
如果A类继承了B类,我们就说A类和B类之间存在泛化关系。
以下面的DaoSupport类和PersonServiceBean类为例:
public abstract class DaoSupport {
public void save(Object entity) {
}
public void delete(Object id) {
}
}
class PersonServiceBean extends DaoSupport {
}
用UML图来表示就是:(子类指向父类)
实现(Implementation)
实现关系实际上就是面向对象中的实现,它也是依赖关系的特例。
如果A类实现了B接口,我们就说A类和B接口之间存在实现关系。
以下面的PersonService接口和PersonServiceBean类为例:
public interface PersonService {
public void delete(Integer id);
}
class PersonServiceBean implements PersonService {
public void delete(Integer id) {
System.out.println("delete");
}
}
用UML图来表示就是:(类指向接口)
关联(Association)
关联关系是类与类、或者类与接口之间的一种强依赖关系。
关联关系具有导航性,可以是双向关系,也可以是单向关系。
关系可以是一对一、一对多、多对多。
以单向一对一关系和双向一对一关系为例:
单向一对一关系:
public class Person {
private IDCard card;
}
class IDCard {
}
UML图:
双向一对一关系:
public class Person {
private IDCard card;
}
class IDCard {
private Person person;
}
UML图:
聚合(Aggregation)和组合(Composition)
聚合关系:
聚合关系表示的是整体和部分之间的关系,聚合关系认为部分可以从整体中分离出来。
如果部分不可以从整体中分离出来,则是组合关系。
聚合关系是关联关系的特例,所以它具有关联关系的导航性与多重性。
以一台电脑(Computer)为例,电脑由键盘(keyboard)、显示器(monitor)、鼠标(mouse)等部分组成,如果我们认为组成电脑的各个部分都是可以从电脑中分离出来的,用代码表示就是:
public class Computer {
private Monitor monitor;
private Mouse mouse;
}
class Monitor {
}
class Mouse {
}
聚合关系用UML图来表示就是:
组合关系:
组合关系也表示整体和部分之间的关系,组合关系认为部分不可以从整体中分离出来。
如果我们认为Monitor、Monse和Computer是不可分离的,则上面的聚合关系可以升级为组合关系。
public class Computer {
private Monitor monitor = new Monitor();
private Mouse mouse = new Mouse();
}
class Monitor {
}
class Mouse {
}
这样,只要创建了Computer对象,就会创建monitor和mouse;同样的,只要销毁了Computer对象,与之对应的monitor和mouse会一并销毁。
组合关系用UML图来表示就是:
鉴于“显示器、鼠标等能不能从电脑中分离出来”是一个哲学问题,下面用一个 人(Person)-脑袋(Head)-身份证(IDCard) 的例子来比较聚合关系和组合关系。
public class Person { private IDCard card;// 聚合关系,可以分离 private Head head = new Head();// 组合关系,不可分离 } class IDCard { } class Head { }
用UML图来表示就是:
设计模式七大原则
设计模式原则是程序员在使用设计模式编程时应当遵守的规则。
提问:xx模式为什么要这么设计?
答:因为xx设计原则。
设计模式原则包括:
- 单一职责原则
- 接口隔离原则
- 依赖倒转原则
- 里氏替换原则
- 开闭原则
- 迪米特法则
- 合成复用原则
单一职责原则
单一职责原则:一个类应该只负责一项职责。
如果类A负责两个不同的职责:职责1、职责2,当职责1需求变更并要求改变A时,可能会造成职责2执行错误。可将类A分解为A1、A2,分别负责职责1、职责2。
遵循单一职责原则主要的目的是降低类的复杂度,提高类的可读性、可维护性,降低业务变更带来的风险。
接口隔离原则
接口隔离原则:客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。
假设有接口Interface1定义了5个抽象方法,类B、类D实现了Interface1,类A通过接口Interface1依赖于类B,但只会用到接口中的1、2、3三个方法,类C通过接口Interface1依赖于类D,但只会用到接口中的1、4、5三个方法。
如果按照下图来设计:
这样就违反了接口隔离原则,因为Interface1对于类A和类C来说不是最小接口,类B和类D必须实现它们不需要的方法。
为遵循接口隔离原则,可以将接口Interface1拆分为几个独立的接口,类A和类C分别与它们需要的接口建立依赖关系。如下图所示:
依赖倒转原则
依赖倒转原则:
- 高层模块不应该依赖底层模块,二者都应该依赖其抽象。
- 抽象不应该依赖细节,细节应该依赖抽象。
- 依赖倒转的中心思想是面向接口编程。
- 依赖倒转原则的设计理念:相对于细节的多变性,抽象的东西相对稳定。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在Java中,抽象就是指接口或抽象类,细节就是具体的实现类。
- 使用接口或抽象类的目的是制定规范,而不涉及任何具体的实现,展现细节的任务交给它们的实现类去完成。
依赖倒转原则的注意事项有:
- 底层模块尽量都要有抽象类或接口。
- 变量的声明类型尽量是抽象类或接口,这样变量引用和实际对象间就存在一个缓冲层,利于程序扩展和优化。
- 继承时遵循里氏替换原则。
我们先不遵循依赖倒转原则写一个接收email信息的Person类:
public class Test {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
}
}
class Email {
public String getInfo() {
return "Email: Hello World!";
}
}
class Person {
public void receive(Email email) {
System.out.println(email.getInfo());
}
}
输出结果:
Email: Hello World!
这样做比较容易理解。
现在我们要新增一个接收微信消息的功能,需要新增一个WeChat类,并在Person类中添加接收WeChat消息的方法:
public class Test {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
person.receive(new WeChat());
}
}
class Email {
public String getInfo() {
return "Email: Hello World!";
}
}
class WeChat {//新增WeChat类
public String getInfo() {
return "WeChat: Hello World!";
}
}
class Person {
public void receive(Email email) {
System.out.println(email.getInfo());
}
public void receive(WeChat weChat) {//新增接收WeChat消息的方法
System.out.println(weChat.getInfo());
}
}
输出结果:
Email: Hello World!
WeChat: Hello World!
这么做当然也可以完成需求,但每次增加接收新的消息类型的功能时,都需要改动Person类(违反了开闭原则)。遵循依赖倒转原则,我们就可以在不改动Person类的情况下增加新的消息类型。
创建接口IReceiver,并使每种消息类型都实现这个接口:
public class Test {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
person.receive(new WeChat());
}
}
interface IReceiver {//创建接口IReceiver
String getInfo();
}
class Email implements IReceiver {//Email类实现IReceiver接口
public String getInfo() {
return "Email: Hello World!";
}
}
class WeChat implements IReceiver {//新增的WeChat类实现IReceiver接口
public String getInfo() {
return "WeChat: Hello World!";
}
}
class Person {
public void receive(IReceiver receiver) {//参数改为IReceiver接口对象
System.out.println(receiver.getInfo());
}
}
输出结果:
Email: Hello World!
WeChat: Hello World!
之后每次增加接收新的消息类型的功能时,都创建新增消息类型的类,并实现IReceive接口,不需要再对Person类进行改动。
里氏替换原则
里氏替换原则:如果对每个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。换言之,所有引用基类的地方必须能透明地使用其子类的对象。
更直白地来讲,在子类中尽量不要重写父类的方法。
继承实际上让两个类的耦合性增强了,在适当的情况下,可以使用依赖、聚合、组合的方式来代替继承解决问题(合成复用原则)。
通常的做法是:把原有的继承关系去掉,原来的父类和子类都继承一个更通俗的基类,采用依赖、聚合、组合等关系代替。
例如,类B继承类A:
class A { public int func1(int a, int b) { return a + b; } } class B extends A { public int func1(int a, int b) { return a - b; } }
类A中的func1方法被重写。
可以改为:
class Base { } class A extends Base { public int func1(int a, int b) { return a + b; } } class B extends Base { private A a = new A(); public int func1(int a, int b) { return a - b; } public int func2(int a, int b) { return this.a.func1(a, b); } }
调用类B中的func2,相当于调用类A中的func1。
开闭原则
开闭原则:一个软件实体(如类、模块、函数)应该对扩展开放,对修改关闭。用抽象构建框架,用实现扩展细节。
对提供方开放,对使用方关闭。
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。
我们回到讲依赖倒转原则时举的例子,其中People类是使用方,应该避免在增加新功能时对People类进行修改,而应该通过扩展现有的软件实体完成需求。
public class Test {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
person.receive(new WeChat());
}
}
interface IReceiver {
String getInfo();
}
class Email implements IReceiver {
public String getInfo() {
return "Email: Hello World!";
}
}
class WeChat implements IReceiver {
public String getInfo() {
return "WeChat: Hello World!";
}
}
class Person {
public void receive(IReceiver receiver) {
System.out.println(receiver.getInfo());
}
}
迪米特法则
迪米特法则:一个对象应该对其它对象保持最少的了解。
迪米特法则又叫最少知道原则,即一个类对自己依赖的类了解的越少越好,被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部,除了提供的public方法,不对外泄漏任何信息。
遵循迪米特法则是为了降低类之间的耦合。
关于迪米特法则我们只需要记住一点:陌生的类不要以局部变量的形式出现在类的内部。
所谓陌生的类,即没有在成员变量、方法参数、方法返回值中出现过的类。
合成复用原则
合成复用原则:尽量使用合成/聚合的方式,而不是使用继承。
如果我们想让类B使用类A中的方法,使用继承会使A和B的耦合性增强:
可以将A和B改进成依赖关系:
或者改成聚合关系:
或者改成组合关系:
加油!