设计模式复习
1. 面向对象设计原则
1.1 可维护性较低的软件设计
- 过于僵硬
- 过于脆弱
- 复用率低
- 黏度过高
1.2 一个好的系统设计
- 可扩展性
- 灵活性
- 可插入性
复用:一个软件的组成部分可以在同一个项目的不同地方甚至在不同的项目重复使用。
面向对象设计复用的目标:实现支持可维护性的复用。(抽象、继承、封装、多态)
重构:在不改变软件现有功能的基础上,通过调整程序代码改善软件的质量、性能、使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。
1.3 七大设计原则
- 单一职责原则(Single Responsibility Principle , SRP):一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。
- 单一职责用于控制类的粒度大小。
- 是实现高内聚低耦合的指导方针
- 开闭原则(Open-Closed Principle , OCP):一个软件实体应当对扩展开放,对修改关闭。
- 抽象化是开闭原则的关键
- 里氏代换原则(Liskov Substitution Principle , LSP ):所有引用基类(父类)的地方必须能透明地使用其子类的对象。
- 里氏代换原则是实现开闭原则的重要方式之一
- 子类必须实现父类的所有方法
- 尽量把父类设计为抽象类或者接口
- 依赖倒转原则(Dependence Inversion Principle . DIP ):高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不依赖于细节,细节应该依赖于抽象。
- 要针对接口编程,不要针对实现编程。
- 开闭原则是面向对象设计的目标,依赖倒转原则就是面向对象设计的主要手段。
- 类之间的耦合
- 依赖注入:将一个类的对象传入另一个类,注入时尽量注入父类对象,程序运行时通过子类对象覆盖父类对象。
- 构造注入、Setter注入、接口注入
- 接口隔离原则(Interface Segregation Principle , ISP):客户端不应该依赖那些它不需要的接口(方法)。
- 大接口要分割成一些更细小的接口。
- 使用多个专门的接口,而不使用单一的总接口。
- 接口仅仅提供客户端需要的方法。
- 合成复用原则(Composite Reuse Principle , CRP ):又称为组合/聚合复用原则,尽量使用对象组合,而不是继承来达到复用的目的。
- 在一个新的对象里通过关联关系来使用一些已知对象,使之成为新对象的一部分。
- 迪米特法则(Law of Demeter , LoD ):又称为最少知识原则,不要和“陌生人”说话只与你的直接朋友通信。
2. 初识设计模式
2.1 设计模式定义
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码,让代码更容易被他人理解、保证代码的可靠性。
2.2 设计模式基本要素
- 模式名称、问题、目的、解决方案、效果、实例代码和相关设计模式
- 关键元素包括以下四个方面
- 模式名称:通过一两个词来描述模式的问题、解决方案和效果,多数模式是根据其功能或者模式结构来命名的。
- 问题:描述了应该在何时使用模式,包含了设计中存在的问题以及问题存在的原因。
- 解决方案:描述了设计模式的组成成分,以及这些组成成分之间的相互关系,各自的职责和协作方式。解决方案通过类图和核心代码来进行说明。
- 效果:描述了模式应用的效果以及在使用模式时应该权衡的问题。包含了模式的优缺点分析
2.3 设计模式的分类
- 根据目的(模式是用来做什么的)可以分为创建型(Creational)、结构型(Structural)和行为型(Behavioral)三种
- 创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。GoF 中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。
- 结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,GoF 中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。
- 行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。GoF 中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。
- 根据范围(模式主要是用于类之间的关系还是处理对象之间的关系)可以分为类模式和对象模式两种
- 类模式处理类和子类关系,这些关系通过继承确立,在编译的时候就被确定了,是静态的。
- 对象模式处理对象的关系,时刻变化,有动态性。
3.深入理解设计模式
3.1 建造者模式(Builder)
例子:根据计算机组件组装不同的计算机。
用这个例子来理解一下创建者模式:首先这个模式做的事情是这样的,现在有一堆计算机零件,比如说一块硬盘它可以放在笔记本电脑上也可以放在台式机上,那么建造者模式就是把不同电脑的构建和表示分离,提供一个计算机产品类,里面包含了计算机的零件,之后提供一张“图纸”这张图纸就是一个抽象建造者接口,这个接口提供了创建的方法以及返回复杂对象的方法,具体的建造者会实现这个接口,用这张“图纸”来创造不同类型的计算机。
具体角色:
- 具体产品角色:Computer
- 抽象建造者:ComputerBuilder
- 具体建造者:DeskTopComputer、LapTopComputer
- 指挥者:ComputerWaiter
注意点:
抽象建造者里要去new一个具体产品。
protected Computer computer = new Computer();
抽象建造者里需要定义一个返回复杂产品的方法
public Computer getComputer(){
return computer;
}
具体建造者继承自抽象建造者,实现里面的所有建造方法!
public class DeskTopBuilder extends ComputerBuilder
指挥者类是真正干活的类
public class ComputerWaiter {
private ComputerBuilder cb;
public void setCb(ComputerBuilder cb){
this.cb=cb;
}
public Computer construct(){
cb.buildCPU();
cb.buildHardDisk();
cb.buildMainFrame();
cb.buildMemory();
return cb.getComputer();
}
}
这个类需要拥有一个抽象建造者的对象,利用这个对象调用其建造的方法来完整一个具体的产品并调用其方法把这个产品返回!
总结:
- 具体产品类提供了一个产品需要的零件。
- 抽象建造类相当于是总工程师画的一张图纸,这张图纸总体上实现了这个产品的建造,里面需要一个创建(new)一个具体产品的成员对象,并提供返回这个对象的方法。
- 具体建造类相当于是拿着总工程师的图纸根据实际的需要进行了二次加工,这个类继承自抽象建造类,需要实现总图纸的所有方法,不过具体的建造细节可以自己决定。
- 指挥者类是真正干活的工人,这个类拿着实际的图纸来完成工作做出具体的产品,这个类需要聚合抽象建造类,并提供setter接口方法,让外界传入具体的“图纸”参数,然后进行建造。
核心理解
建造者模式的核心在于抽象建造者类,这个类要做的事情是定义方法:首先这个类是用来建造一个实例对象的,所以一定要new一个新的产品对象作为其属性成员,然后定义建造的接口方法,根据具体需要被建造的实例的setter方法提供不同的多个建造方法接口,最后需要一个方法返回最终建造完成的对象。
这个抽象建造方法就是一张建造的图纸,后面实现这个接口的类是具体的建造图纸,把具体的建造图纸用set注入的方式给指挥者类(相当于工人)让指挥者类干活,最后根据具体图纸完成一个实例产品!
3.2 原型模式(ProtoType)
具体角色:
- ProtoType抽象原型类
- ConcreteProtoType具体原型类
步骤:
- 实现一个接口:Cloneable
- 重写一个方法:clone
pubilc Object clone()
object = super.clone() ;
return object;
- 浅克隆:复制对象的引用,对象的属性仍然指向同一处。
- 深克隆:不止复制对象的引用,而且要把对象的所有属性全部克隆一次,两个对象的属性将不会指向同一块区域,从而实现两个对象彻底分离。
核心理解
原型模式只做了一件事情,就是克隆一份一模一样的自己并返回。
- 实现一个接口Cloneable
- 调用一个方法:object = super.clone() ;
- 返回这个object
3.3 单例模式(Singleton)
注意点:
- 静态私有成员变量。
- 私有构造函数。
- 静态公有工厂方法,返回唯一对象实例,方法中判断对象是否为空,如果为空则new一个新对象返回,俄国不为空,则直接将私有成员变量对象返回。
package com.a007;
public class StuNo
{
//静态私有成员变量
private static StuNo instance=null;
private String no;
//私有构造方法
private StuNo()
{
}
//静态公有工厂方法,返回唯一实例
public static StuNo getInstance()
{
if(instance==null)
{
System.out.println("新学号");
instance=new StuNo();
instance.setStuNo("20194074");
}
else
{
System.out.println("学号重复,获得旧学号");
}
return instance;
}
private void setStuNo(String no)
{
this.no=no;
}
public String getStuNo()
{
return this.no;
}
}
核心理解
单例模式做的事情是保证一个类有且只有一个实例对象!
- 首先要保证这个类的构造方法是私有的
- 其次要保证这个对象作为成员属性是静态私有的
- 最后提供一个公有的对外接口返回这个实例化的对象
3.4 适配器模式(Adapter)
用途:将一个类的接口转换成客户希望的另一个类的接口。
例子:电脑网线USB转接器
角色:
电脑(客户端)、网线、转接器、目标接口NetToUsb
- 目标接口或抽象类(目标抽象类或目标抽象接口):这里例子中就是目标接口USB。
- 适配者类(需要适配的类 Adaptee):它定义了一个已经存在的接口,这个接口需要被适配。在这个例子中网线类就是那个已经存在的接口,但是网线不可以直接插到电脑的USB上。
- 适配器类(Adapter):包装网线,让网线支持USB接口,把网线插到USB上并处理请求。
- 适配器类需要同时和两个类打交道,它要把网线和电脑的USB接口连接在一起。有两种方式,
- 一种是继承要被适配的类(网线类)同时实现目标接口。
- 另一种是使用组合模式,不去继承适配者类,而是使用聚合的方式,让网线类作为适配器类的一个成员变量,然后再去实现目标抽象接口。
分类:
- 类适配器:继承模式,继承需要被适配的类,实现目标抽象接口。
- 对象适配器:组合模式,把需要适配的类作为成员属性变量,同时实现目标抽象接口。
类图:(双向适配)
核心理解
适配器模式做的事情是这样的:
有两个不相干的类,但是它们想组合到一起使用,那么就通过一个适配器把二者适配在一起使用。
比如说:电脑有一个USB接口,而网线的接头不是USB的,可是电脑想上网,那么就需要一个接口转接的适配器来完成这个工作,这时候会出现三个类。
- 网线类:这个类提供了具体要实现的业务方法,也就是它可以完成上网这件事,比如说有一个方法是net()
- USB接口类:这个接口是用户想要的接口,用户希望通过USB接口完成上网这件事,比如说有一个方法是execute()
- 转接器类:这个类来完成二者的适配:首先实现USB接口,然后或者通过继承网线类或者通过组合网线类,选择二者的任意一个方式,重写USB接口里的方法execute(),在这个方法里去调用网线类的真实业务方法net()来完成上网这件事
- 客户端在调用时,只需要把实例化的网线类通过set注入交给适配器,然后通过调用适配器类的execute()方法就可以完成上网这件事情!
3.5 桥接模式(Bridge)
图片来自bilibili遇见狂神说
核心理解
桥接模式做了这样一件事情:
就像图中所示:如果想要一个联想的台式电脑,那么就需要两层继承来拿到这个对象(类),第一这是低效率的,第二这是一种静态的定死的方式,扩展性很差。桥接模式的思想是把抽象化和实现化进行解耦分离,比如说无论有多少个品牌,抽象来看它们都只是品牌,无论有多少种电脑,它们都只是电脑。这样的话可以抽象出两个维度,一个是类型、另一个是品牌。具体的实现就是自由组合:XX品牌的XX种类电脑。
优化:本来如果要这九种电脑需要3*3=9个类,现在需要这些电脑只需要3+3=6个类,如果数量级更大,桥接模式的好处可想而知,可以大大减少子类的个数!
根据依赖倒转原则,实现要依赖抽象,所以首先会有一个抽象的电脑类,这个抽象类的子类是各种类型的电脑,其次需要一个电脑的品牌接口,实现这个接口的类是各种品牌!
这个抽象电脑类和品牌接口类是组合的关系,抽象电脑类通过Setter方法注入一个具体的电脑品牌对象,然后用其方法结合自身的电脑种类获得这个品牌的各种类型的电脑!
补充:【抽象类和接口的区别】
含有abstract修饰符的class即为抽象类,abstract 类不能创建实例对象。含有abstract方法的类必须定义为abstract class,abstract class类中的方法不必是抽象的。abstract class类中定义抽象方法必须在具体(Concrete)子类中实现,所以,不能有抽象构造方法或抽象静态方法。如果子类没有实现抽象父类中的所有抽象方法,那么子类也必须定义为abstract类型。
接口(interface)可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的。接口中的方法定义默认为public abstract类型,接口中的成员变量类型默认为public static final。
下面比较一下两者的语法区别:
- 抽象类可以有构造方法,接口中不能有构造方法。
- 抽象类中可以有普通成员变量,接口中没有普通成员变量
- 抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。
- 抽象类中的抽象方法的访问类型可以是public,protected和(默认类型,虽然
eclipse下不报错,但应该也不行),但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。
- 抽象类中可以包含静态方法,接口中不能包含静态方法
- 抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。
- 一个类可以实现多个接口,但只能继承一个抽象类。
3.6 装饰模式(Decorator)
定义:动态地给一个对象增加一些额外的职责。
角色:
- 抽象构件:Component
- 具体构件:ConcreteComponent
- 抽象装饰类:Decorator
- 具体装饰类:ConcreteDecorator
模式分析:
具体构件类和抽象构件类都实现了抽象构件接口,模式的关键在于抽象装饰类,这个类实现了抽象构件接口并且组合了抽象构件,在其构造函数中设置参数注入具体构件对象,在其装饰方法中调用这个注入的构件类已有的方法,再通过具体装饰类的继承,添加其他方法和功能。
核心理解
装饰模式做的事情是动态修改被装饰者的一些属性方法等等。
根据依赖倒转原则,待装饰的类和装饰者类都要实现自同一个抽象构件接口,在装饰者类的构造方法里要注入一个待装饰者对象,装饰者类和抽象构件接口是组合关系和接口实现关系,在装饰者类中提供一个可扩展的方法供子类重写。
具体的装饰者类继承抽象装饰者类,重写其扩展方法完成对待装饰对象的装饰!
3.7 外观模式(Facade)
定义:外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统的一组接口提供 了一个一致的界面。
实例:一个电源总开关可以控制四盏灯、一个风扇、一台空调和一个电视机的启动和关闭。
类图:
核心理解
外观模式做的事情是这样的:
比如说你现在想把家里的灯关了、把空调关了、把电视机也关了。正常的过程是你要一个个去把它们关闭,但是如果给你一个统一的关闭按钮,只要你按这一个按钮,这三种电器就会同时关闭,这样的一个按钮的实现,就是外观模式的核心!
使用简单的关联关系,实现对多个对象的方法的同时调用,统一分配!
3.8 代理模式(Proxy)
定义:
给某个对象提供一个代理,并由代理对象控制对原对象的引用。
角色:
- 抽象主题角色:里面包含了抽象的业务操作。
- 代理主题角色:实现抽象主题接口,关联真实主题角色,对真实主题角色的一些业务进行一些预先处理和延后处理。
- 真实主题角色:里面包含的真实的业务需求,客户端调用的时只需要面向代理角色,根据不同的客户,代理角色将给出不同的业务实现,代替真实主题角色进行业务的安排。
核心理解
代理模式的关键在于:
首先根据依赖倒转原则:具体主题类和代理主题类都要实现自同一个抽象主题角色。
代理主题类关联真实主题类,代替真实主题针对不同的客户做出不同的处理!
3.9 职责链模式(Chain of Responsibility)
定义:避免请求的发送者和接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,沿着这条链传递请求,直到有对象处理它为止。
角色:
- 抽象处理者:Handler
- 具体处理者:ConcreteHandler
- 客户类:Client
模式分析:
关键在于抽象处理者类的设计:很多对象由每一个对象对其下家的引用而连接在一起。
抽象处理者典型代码:
//审批者类:抽象处理者
abstract class Approver {
protected Approver successor; //定义后继对象
protected String name; //审批者姓名
public Approver(String name) {
this.name = name;
}
//设置后继者
public void setSuccessor(Approver successor) {
this.successor = successor;
}
//抽象请求处理方法
public abstract void processRequest(PurchaseRequest request);
}
具体处理者典型代码:
//董事长类:具体处理者
class ViceManager extends Approver {
public ViceManager(String name) {
super(name);
}
//具体请求处理方法
public void processRequest(PurchaseRequest request) {
if (request.getAmount() < 100000) {
System.out.println("副总经理" + this.name + "审批采购单:" + request.getNumber() + ",金额:" + request.getAmount() + "元,采购目的:" + request.getPurpose() + "。"); //处理请求
}
else {
this.successor.processRequest(request); //转发请求
}
}
}
客户端调用典型代码:轮流设置下家
position1.setSuccessor(position2);
position2.setSuccessor(position3);
position3.setSuccessor(position4);
position4.setSuccessor(meeting);
package com.c015;
public class Client {
public static void main(String[] args) {
Approver position1,position2,position3,position4,meeting; // 多个处理者
position1 = new Director("甲");
position2 = new PartManager("乙");
position3 = new ViceManager("丙");
position4 = new Manager("丁");
meeting = new Congress("职工大会");
//创建职责链
position1.setSuccessor(position2);
position2.setSuccessor(position3);
position3.setSuccessor(position4);
position4.setSuccessor(meeting);
//创建采购单
PurchaseRequest pr1 = new PurchaseRequest(5000,10001,"XXX");
position1.processRequest(pr1);
PurchaseRequest pr2 = new PurchaseRequest(45000,10002,"XXX");
position1.processRequest(pr2);
PurchaseRequest pr3 = new PurchaseRequest(77000,10003,"XXX");
position1.processRequest(pr3);
PurchaseRequest pr4 = new PurchaseRequest(150000,10004,"XXX");
position1.processRequest(pr4);
PurchaseRequest pr5 = new PurchaseRequest(800000,10005,"XXX");
position1.processRequest(pr5);
}
}
核心理解
职责链模式关键在于设置职责的下家!
抽象处理者类要有一个自身的对象作为成员属性变量,并通过一个set方法完成赋值,之后要提供一个具体处理的方法接口供子类重写!
后续的子类重写具体的处理办法,如果处理不了,再次调用父类的处理方法直接把请求交给下家来完成!
3.10 命令模式(Command)
定义:
将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化。
本质上是对命令进行封装,将发出命令的责任和执行命令的责任分隔开。
角色:
- 接收者类:实现了具体的业务操作,拿电视机来说,这个类实现了电视机的开启和关闭的真实操作方法。
- 抽象命令类:定义了一个执行命令的方法接口,由其子类实现。
- 具体命令类(一个命令一个类):实现抽象命令接口,关联接收者类,调用接受者类中具体的一个命令,比如这个具体命令类是要开启电视机,那么执行命令的方法就调用接受者对象中的开启命令。
- 调用者类:相当于遥控器,把所有可能的操作集合在一起,客户端只需要使用遥控器就可以完成所有命令的发起,构造方法(形参是是抽象命令队对象,实参是具体命令对象)完成所有具体命令对象的注入,提供执行命令的方法,用具体命令对象调用具体命令的执行方法。
关键代码:
//接收者:真正执行命令的对象
public class Light {
public void open(){
System.out.println("打开电灯!");
}
}
public interface Command {
public void execute();
}
// 这是一个命令,所以需要实现Command接口
public class LightOnCommand implements Command {
Light light;
// 构造器传入某个电灯,以便让这个命令控制,然后记录在实例变量中
public LightOnCommand(Light light) {
this.light = light;
}
// 这个execute方法调用接收对象的on方法
public void execute() {
light.on();
}
}
public class SimpleRemoteControl {
// 有一个插槽持有命令,而这个命令控制着一个装置
Command slot;
public SimpleRemoteControl() {}
// 这个方法用来设置插槽控制的命令
public void setCommand(Command command) {
slot = command;
}
// 当按下按钮时,这个方法就会被调用,使得当前命令衔接插槽,并调用它的execute方法
public void buttonWasPressed() {
slot.execute();
}
}
客户端使用
public class RemoteControlTest {
public static void main(String[] args) {
// 遥控器就是调用者,会传入一个命令对象,可以用来发出请求
SimpleRemoteControl remote = new SimpleRemoteControl();
// 现在创建一个电灯对象,此对象也就是请求的接收者
Light light = new Light();
// 这里创建一个命令,然后将接收者传给它
LightOnCommand lightOn = new LightOnCommand(light);
// 把命令传给调用者
remote.setCommand(lightOn);
// 模拟按下按钮
remote.buttonWasPressed();
}
}
核心理解
命令模式主要完成的事情是把命令的具体实施和命令的发出解耦。
有一个具体干活的类(命令接收者类),这个类里有所有执行具体命令的方法。
有一个抽象的命令类,这个类定义了一个执行的方法接口,然后它的子类(这些子类的个数和具体命令的个数是一致的,比如说那个具体干活的类需要做两件事,一个是打开电脑,一个是关闭电脑,那么就会有两个不同的子类来继承这个抽象的命令类!)继承这个类并重写它的执行命令的方法,这里有个点需要注意:这些子类需要关联那个命令接收者类,用那个类的方法来重写执行方法!
3.11 迭代器模式(Iterator)
定义:
定义了遍历和访问元素的接口,一般声明如下方法:用于获取第一个元素的first(),用于访问下一个元素的next(),用于判断是否还有下一个元素的hasNext(),用于获取当前元素的currentItem()。
3.12 观察者模式(Observer)
定义:
定义对象之间的一种一对多的依赖关系,使得每当一个对象的状态发生变化时,其相关的依赖对象都可以得到通知并被自动更新。
模式主要用于多个不同的对象对一个对象的某个方法会做出不同的反应!
比如猫叫之后狗会叫老鼠会逃跑,这时候猫就是被观察者,老鼠和狗都是观察者。
角色:
- 抽象目标:这是被观察的对象(抽象)
- 这是核心,里面需要一个成员属性变量存储所有的观察者,需要定义add和remove观察者的方法,需要给出notify方法通知所有的观察者对象。
- 具体目标(具体的被观察者):猫继承抽象目标类,实现里面的方法,写出猫的反应,并且循环输出所有观察者的反应。
- 抽象观察者:接口,定义响应方法。
- 具体观察者:实现抽象观察者方法,重写响应方法。
- 客户端调用:先使用具体目标对象的add方法添加具体观察者对象,然后调用其notify方法通知观察者。
核心理解
观察者模式做的事情是这样的:
有这么一个场景,比如说一个对象的某个变化会造成其他类的不同的反应,比如说股票的涨跌和股民的状态就是一种动态的关联变化,观察者模式就是来描述这样的一个场景的!
具体是这样完成的:
根据依赖倒转原则,首先需要一个抽象的被观察的类,这个类拥有的成员属性变量是和它有关系的那些观察者对象,一般是有多个对象,如果这个属性是一个集合,那么需要定义两个接口方法,一个增加一个删除,最后还需要一个描述自身状态的方法。
具体的被观察者继承自抽象的被观察类, 这个类重写它的状态变化方法!注意这个方法需要遍历所有观察者对象的response方法
观察者同样也需要进行抽象,需要一个观察者接口类,这个类只有一个方法就是response()
具体的观察者实现这个接口,重写response方法!
客户端在调用时,需要把观察者添加到被观察者里,然后调用被观察者的状态变化方法,就会看到它所有的观察者对这个状态做出的不同的反应!