Java 中一般认为有23种设计模式,当然暂时不需要所有的都会,但是其中常见的几种设计模式应该去掌握。
总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
创建型设计模式
1. 单例模式
所谓的单例设计指的是一个类只允许产生一个实例化对象。
最好理解的一种设计模式,分为懒汉式和饿汉式。
饿汉式:构造方法私有化,外部无法产生新的实例化对象,只能通过static方法取得实例化对象
class Singleton {
/**
* 在类的内部可以访问私有结构,所以可以在类的内部产生实例化对象
*/
private static Singleton instance = new Singleton();
/**
* private 声明构造
*/
private Singleton() {
}
/**
* 返回对象实例
*/
public static Singleton getInstance() {
return instance;
}
public void print() {
System.out.println("Hello Singleton...");
}
}
懒汉式:当第一次去使用Singleton对象的时候才会为其产生实例化对象的操作
class Singleton {
/**
* 声明变量
*/
private static volatile Singleton singleton = null;
/**
* 私有构造方法
*/
private Singleton() {
}
/**
* 提供对外方法
* @return
*/
public static Singleton getInstance() {
// 还未实例化
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
public void print() {
System.out.println("Hello World");
}
}
当多个线程并发执行 getInstance 方法时,懒汉式会存在线程安全问题,所以用到了 synchronized 来实现线程的同步,当一个线程获得锁的时候其他线程就只能在外等待其执行完毕。而饿汉式则不存在线程安全的问题。
2. 工厂设计模式
工厂模式分为工厂方法模式和抽象工厂模式。
工厂方法模式
工厂方法模式:
1. 工厂方法模式分为三种:普通工厂模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。
2. 多个工厂方法模式,是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。
3. 静态工厂方法模式,将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
1. 普通工厂模式
建立一个工厂类,对实现了同一接口的一些类进行实例的创建。
interface Sender {
void Send();
}
class MailSender implements Sender {
@Override
public void Send() {
System.out.println("This is mail sender...");
}
}
class SmsSender implements Sender {
@Override
public void Send() {
System.out.println("This is sms sender...");
}
}
public class FactoryPattern {
public static void main(String[] args) {
Sender sender = produce("mail");
sender.Send();
}
public static Sender produce(String str) {
if ("mail".equals(str)) {
return new MailSender();
} else if ("sms".equals(str)) {
return new SmsSender();
} else {
System.out.println("输入错误...");
return null;
}
}
}
2. 多个工厂方法模式
该模式是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。
interface Sender {
void Send();
}
class MailSender implements Sender {
@Override
public void Send() {
System.out.println("This is mail sender...");
}
}
class SmsSender implements Sender {
@Override
public void Send() {
System.out.println("This is sms sender...");
}
}
class SendFactory {
public Sender produceMail() {
return new MailSender();
}
public Sender produceSms() {
return new SmsSender();
}
}
public class FactoryPattern {
public static void main(String[] args) {
SendFactory factory = new SendFactory();
Sender sender = factory.produceMail();
sender.Send();
}
}
3. 静态工厂方法模式
将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
interface Sender {
void Send();
}
class MailSender implements Sender {
@Override
public void Send() {
System.out.println("This is mail sender...");
}
}
class SmsSender implements Sender {
@Override
public void Send() {
System.out.println("This is sms sender...");
}
}
class SendFactory {
public static Sender produceMail() {
return new MailSender();
}
public static Sender produceSms() {
return new SmsSender();
}
}
public class FactoryPattern {
public static void main(String[] args) {
Sender sender = SendFactory.produceMail();
sender.Send();
}
}
抽象工厂模式
工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要扩展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?
那么这就用到了抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。
interface Provider {
Sender produce();
}
interface Sender {
void Send();
}
class MailSender implements Sender {
public void Send() {
System.out.println("This is mail sender...");
}
}
class SmsSender implements Sender {
public void Send() {
System.out.println("This is sms sender...");
}
}
class SendMailFactory implements Provider {
public Sender produce() {
return new MailSender();
}
}
class SendSmsFactory implements Provider {
public Sender produce() {
return new SmsSender();
}
}
public class FactoryPattern {
public static void main(String[] args) {
Provider provider = new SendMailFactory();
Sender sender = provider.produce();
sender.Send();
}
}
建造者模式
工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性。
import java.util.ArrayList;
import java.util.List;
/**
abstract class Builder {
/**
* 第一步:装CPU
*/
public abstract void buildCPU();
/**
* 第二步:装主板
*/
public abstract void buildMainBoard();
/**
* 第三步:装硬盘
*/
public abstract void buildHD();
/**
* 获得组装好的电脑
* @return
*/
public abstract Computer getComputer();
}
/**
* 装机人员装机
*/
class Director {
public void Construct(Builder builder) {
builder.buildCPU();
builder.buildMainBoard();
builder.buildHD();
}
}
/**
* 具体的装机人员
*/
class ConcreteBuilder extends Builder {
Computer computer = new Computer();
@Override
public void buildCPU() {
computer.Add("装CPU");
}
@Override
public void buildMainBoard() {
computer.Add("装主板");
}
@Override
public void buildHD() {
computer.Add("装硬盘");
}
@Override
public Computer getComputer() {
return computer;
}
}
class Computer {
/**
* 电脑组件集合
*/
private List<String> parts = new ArrayList<String>();
public void Add(String part) {
parts.add(part);
}
public void print() {
for (int i = 0; i < parts.size(); i++) {
System.out.println("组件:" + parts.get(i) + "装好了...");
}
System.out.println("电脑组装完毕...");
}
}
public class BuilderPattern {
public static void main(String[] args) {
Director director = new Director();
Builder builder = new ConcreteBuilder();
director.Construct(builder);
Computer computer = builder.getComputer();
computer.print();
}
}
原型模式(Prototype Pattern)
Java原型模式(Prototype Pattern)是一种创建型设计模式,其目的是通过复制现有对象来创建新的对象。
使用场景
- 当对象创建的过程比较耗时或者比较复杂,例如需要进行复杂的计算或者涉及到网络请求等操作,可以使用原型模式来避免重复的初始化过程。
- 当需要创建的对象需要和其他对象进行协同工作时,例如需要创建一个包含多个对象的组合对象,可以使用原型模式来复制一个已有的组合对象,然后进行修改来创建新的组合对象。
- 当需要动态地增加或者删除一些对象时,可以使用原型模式来复制一个已有的对象,然后进行修改来创建新的对象。
- 当需要保护对象的复杂状态时,例如当一个对象的创建需要大量的数据初始化时,可以使用原型模式来保护这些数据,避免因为对象的复制而产生意外的副作用。
代码实现
// 定义一个原型接口
interface Prototype {
public Prototype clone();
}
// 具体的原型类
class ConcretePrototype implements Prototype {
public Prototype clone() {
return new ConcretePrototype();
}
}
// 客户端代码
class Client {
public static void main(String[] args) {
Prototype prototype = new ConcretePrototype();
Prototype clone = prototype.clone();
}
}
使用小结
- Java中的
Object
类实现了Cloneable
接口,这就意味着Java中的任何对象都可以实现原型模式。通过实现Cloneable
接口,并重写Object
类中的clone()
方法,可以实现原型模式。例如ArrayList、HashMap
等集合类都实现了Cloneable
接口,可以通过复制现有对象来创建新的对象。 - Java中的线程池也是使用了原型模式,线程池中的每个线程都是从原型线程中复制而来,而不是每次创建新的线程。
结构型设计模式
适配器设计模式
适配器模式是将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的的类的兼容性问题。主要分三类:类的适配器模式、对象的适配器模式、接口的适配器模式。
1. 类的适配器模式:
class Source {
public void method1() {
System.out.println("This is original method...");
}
}
interface Targetable {
/**
* 与原类中的方法相同
*/
public void method1();
/**
* 新类的方法
*/
public void method2();
}
class Adapter extends Source implements Targetable {
@Override
public void method2() {
System.out.println("This is the targetable method...");
}
}
public class AdapterPattern {
public static void main(String[] args) {
Targetable targetable = new Adapter();
targetable.method1();
targetable.method2();
}
}
2. 对象的适配器模式
基本思路和类的适配器模式相同,只是将Adapter 类作修改,这次不继承Source 类,而是持有Source 类的实例,以达到解决兼容性的问题。
class Source {
public void method1() {
System.out.println("This is original method...");
}
}
interface Targetable {
/**
* 与原类中的方法相同
*/
public void method1();
/**
* 新类的方法
*/
public void method2();
}
class Wrapper implements Targetable {
private Source source;
public Wrapper(Source source) {
super();
this.source = source;
}
@Override
public void method1() {
source.method1();
}
@Override
public void method2() {
System.out.println("This is the targetable method...");
}
}
public class AdapterPattern {
public static void main(String[] args) {
Source source = new Source();
Targetable targetable = new Wrapper(source);
targetable.method1();
targetable.method2();
}
}
3. 接口的适配器模式
接口的适配器是这样的:有时我们写的一个接口中有多个抽象方法,当我们写该接口的实现类时,必须实现该接口的所有方法,这明显有时比较浪费,因为并不是所有的方法都是我们需要的,有时只需要某一些,此处为了解决这个问题,我们引入了接口的适配器模式,借助于一个抽象类,该抽象类实现了该接口,实现了所有的方法,而我们不和原始的接口打交道,只和该抽象类取得联系,所以我们写一个类,继承该抽象类,重写我们需要的方法就行。
/**
* 定义端口接口,提供通信服务
*/
interface Port {
/**
* 远程SSH端口为22
*/
void SSH();
/**
* 网络端口为80
*/
void NET();
/**
* Tomcat容器端口为8080
*/
void Tomcat();
/**
* MySQL数据库端口为3306
*/
void MySQL();
}
/**
* 定义抽象类实现端口接口,但是什么事情都不做
*/
abstract class Wrapper implements Port {
@Override
public void SSH() {
}
@Override
public void NET() {
}
@Override
public void Tomcat() {
}
@Override
public void MySQL() {
}
}
/**
* 提供聊天服务
* 需要网络功能
*/
class Chat extends Wrapper {
@Override
public void NET() {
System.out.println("Hello World...");
}
}
/**
* 网站服务器
* 需要Tomcat容器,Mysql数据库,网络服务,远程服务
*/
class Server extends Wrapper {
@Override
public void SSH() {
System.out.println("Connect success...");
}
@Override
public void NET() {
System.out.println("WWW...");
}
@Override
public void Tomcat() {
System.out.println("Tomcat is running...");
}
@Override
public void MySQL() {
System.out.println("MySQL is running...");
}
}
public class AdapterPattern {
private static Port chatPort = new Chat();
private static Port serverPort = new Server();
public static void main(String[] args) {
// 聊天服务
chatPort.NET();
// 服务器
serverPort.SSH();
serverPort.NET();
serverPort.Tomcat();
serverPort.MySQL();
}
}
装饰模式
顾名思义,装饰模式就是给一个对象增加一些新的功能,而且是动态的,要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例。
interface Shape {
void draw();
}
/**
* 实现接口的实体类
*/
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Shape: Rectangle...");
}
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Shape: Circle...");
}
}
/**
* 创建实现了 Shape 接口的抽象装饰类。
*/
abstract class ShapeDecorator implements Shape {
protected Shape decoratedShape;
public ShapeDecorator(Shape decoratedShape) {
this.decoratedShape = decoratedShape;
}
@Override
public void draw() {
decoratedShape.draw();
}
}
/**
* 创建扩展自 ShapeDecorator 类的实体装饰类。
*/
class RedShapeDecorator extends ShapeDecorator {
public RedShapeDecorator(Shape decoratedShape) {
super(decoratedShape);
}
@Override
public void draw() {
decoratedShape.draw();
setRedBorder(decoratedShape);
}
private void setRedBorder(Shape decoratedShape) {
System.out.println("Border Color: Red");
}
}
/**
* 使用 RedShapeDecorator 来装饰 Shape 对象。
*/
public class DecoratorPattern {
public static void main(String[] args) {
Shape circle = new Circle();
Shape redCircle = new RedShapeDecorator(new Circle());
Shape redRectangle = new RedShapeDecorator(new Rectangle());
System.out.println("Circle with normal border");
circle.draw();
System.out.println("\nCircle of red border");
redCircle.draw();
System.out.println("\nRectangle of red border");
redRectangle.draw();
}
}
代理模式
代理模式指给一个对象提供一个代理对象,并由代理对象控制对原对象的引用。代理可以分为静态代理和动态代理。
通过代理模式,可以利用代理对象为被代理对象添加额外的功能,以此来拓展被代理对象的功能。可以用于计算某个方法执行时间,在某个方法执行前后记录日志等操作。
1. 静态代理
静态代理需要我们写出代理类和被代理类,而且一个代理类和一个被代理类一一对应。代理类和被代理类需要实现同一个接口,通过聚合使得代理对象中有被代理对象的引用,以此实现代理对象控制被代理对象的目的。
/**
* 代理类和被代理类共同实现的接口
*/
interface IService {
void service();
}
/**
* 被代理类
*/
class Service implements IService{
@Override
public void service() {
System.out.println("被代理对象执行相关操作");
}
}
/**
* 代理类
*/
class ProxyService implements IService{
/**
* 持有被代理对象的引用
*/
private IService service;
/**
* 默认代理Service类
*/
public ProxyService() {
this.service = new Service();
}
/**
* 也可以代理实现相同接口的其他类
* @param service
*/
public ProxyService(IService service) {
this.service = service;
}
@Override
public void service() {
System.out.println("开始执行service()方法");
service.service();
System.out.println("service()方法执行完毕");
}
}
//测试类
public class ProxyPattern {
public static void main(String[] args) {
IService service = new Service();
//传入被代理类的对象
ProxyService proxyService = new ProxyService(service);
proxyService.service();
}
}
2. 动态代理
JDK 1.3 之后,Java通过java.lang.reflect包中的三个类Proxy、InvocationHandler、Method来支持动态代理。动态代理常用于有若干个被代理的对象,且为每个被代理对象添加的功能是相同的(例如在每个方法运行前后记录日志)。
动态代理的代理类不需要我们编写,由Java自动产生代理类源代码并进行编译最后生成代理对象。
创建动态代理对象的步骤:
1. 指明一系列的接口来创建一个代理对象
2. 创建一个调用处理器(InvocationHandler)对象
3. 将这个代理指定为某个其他对象的代理对象
4. 在调用处理器的invoke()方法中采取代理,一方面将调用传递给真实对象,另一方面执行各种需要的操作
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 代理类和被代理类共同实现的接口
*/
interface IService {
void service();
}
class Service implements IService{
@Override
public void service() {
System.out.println("被代理对象执行相关操作");
}
}
class ServiceInvocationHandler implements InvocationHandler {
/**
* 被代理的对象
*/
private Object srcObject;
public ServiceInvocationHandler(Object srcObject) {
this.srcObject = srcObject;
}
@Override
public Object invoke(Object proxyObj, Method method, Object[] args) throws Throwable {
System.out.println("开始执行"+method.getName()+"方法");
//执行原对象的相关操作,容易忘记
Object returnObj = method.invoke(srcObject,args);
System.out.println(method.getName()+"方法执行完毕");
return returnObj;
}
}
public class ProxyPattern {
public static void main(String[] args) {
IService service = new Service();
Class<? extends IService> clazz = service.getClass();
IService proxyService = (IService) Proxy.newProxyInstance(clazz.getClassLoader(),
clazz.getInterfaces(), new ServiceInvocationHandler(service));
proxyService.service();
}
}
桥接模式(Bridge)
概述
桥接模式是一种结构性设计模式,它将抽象和实现分开,以便它们可以独立地变化。它通过使用组合来代替继承来实现这个目标。
在桥接模式中,有两个类层次结构:抽象类和实现类。抽象类定义了一个接口,它包含了一些基本的操作方法。实现类实现了这些操作方法,并将它们映射到具体的实现上。通过这种方式,客户端代码和具体的实现之间就解耦了。
模式结构
桥接模式的结构包括四个主要部分:
- 抽象化(
Abstraction
):定义抽象类,并包含一个指向实现化对象的引用,抽象类的接口将调用实现化类中的方法。 - 实现化(
Implementor
):定义实现化接口,具体实现化类将实现此接口。 - 具体抽象化(
Refined Abstraction
):扩展抽象化接口以支持更多功能,实现类将实现这些方法。 - 具体实现化(
Concrete Implementor
):实现实现化接口的类,并提供具体实现。
其结构图如图所示:
优缺点
使用桥接模式的主要优势在于它能够将抽象和实现分离开来,并将它们独立地变化。这意味着,如果你需要改变实现方式,你只需要改变实现类,而不需要改变客户端代码。
此外,使用桥接模式可以提高代码的可读性和可维护性。因为它将功能分解成小的模块,而不是大而复杂的类。这使得代码更容易理解、调试和修改。
桥接模式的主要缺点在于它增加了代码的复杂性。因为它需要将抽象和实现分开,所以需要增加更多的类和层次结构。这可能会使代码难以理解、调试和维护。
使用场景
桥接模式适用于以下场景:
- 当你需要将一个抽象类和它的实现分开时。
- 当你需要支持多种平台或多种操作系统时。
- 当你需要改变一个类的实现方式时,而不想影响到客户端代码。
实现
下面通过一个例子来说明桥接模式的实现过程。
假设我们要设计一个图形库,其中包含两个基本功能:绘制不同种类的图形和使用不同的颜色进行填充。我们可以使用桥接模式来实现这个图形库,其中抽象部分为Shape
,实现部分为Color
,桥接接口为DrawAPI
。
先看一下Shape
的抽象类:
package com.example.javaDesignPattern.bridge;
/**
* 抽象类
*
* @author bug菌
* @version 1.0
* @date 2023/9/19 11:19
*/
public abstract class Shape {
protected DrawAPI drawAPI;
public Shape(DrawAPI drawAPI) {
this.drawAPI = drawAPI;
}
public abstract void draw();
}
可以看到,Shape
包含了一个对DrawAPI
的引用,并且定义了一个抽象的draw
方法,用于绘制图形。
再看一下DrawAPI
的接口:
package com.example.javaDesignPattern.bridge;
/**
* 抽象接口
*
* @author bug菌
* @version 1.0
* @date 2023/9/19 11:19
*/
public interface DrawAPI {
public void draw();
}
DrawAPI
定义了一个draw
方法,Shape
的实现类将通过它来使用不同的颜色进行填充。
然后是Shape
的实现类:Circle
、Rectangle和Triangle
。
package com.example.javaDesignPattern.bridge;
/**
* Shape的实现类
*
* @author bug菌
* @version 1.0
* @date 2023/9/19 11:20
*/
public class Circle extends Shape {
private int x, y, radius;
public Circle(int x, int y, int radius, DrawAPI drawAPI) {
super(drawAPI);
this.x = x;
this.y = y;
this.radius = radius;
}
public void draw() {
drawAPI.draw();
}
}
package com.example.javaDesignPattern.bridge;
/**
* Shape的实现类
*
* @author bug菌
* @version 1.0
* @date 2023/9/19 11:21
*/
public class Rectangle extends Shape {
private int x, y, width, height;
public Rectangle(int x, int y, int width, int height, DrawAPI drawAPI) {
super(drawAPI);
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public void draw() {
drawAPI.draw();
}
}
package com.example.javaDesignPattern.bridge;
/**
* Shape的实现类
*
* @author bug菌
* @version 1.0
* @date 2023/9/19 11:22
*/
public class Triangle extends Shape {
private int x1, y1, x2, y2, x3, y3;
public Triangle(int x1, int y1, int x2, int y2, int x3, int y3, DrawAPI drawAPI) {
super(drawAPI);
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.x3 = x3;
this.y3 = y3;
}
public void draw() {
drawAPI.draw();
}
}
可以看到,它们都继承了Shape
,并实现了draw
方法。在构造方法中,将DrawAPI
对象传递给了Shape
的构造方法,从而实现了抽象部分和实现部分的连接。
最后是Color
的实现类:Red
、Green
和Blue
。
package com.example.javaDesignPattern.bridge;
/**
* Color的实现类
*
* @author bug菌
* @version 1.0
* @date 2023/9/19 11:24
*/
public class Red implements DrawAPI {
public void draw() {
System.out.println("fill with red");
}
}
public class Green implements DrawAPI {
public void draw() {
System.out.println("fill with green");
}
}
public class Blue implements DrawAPI {
public void draw() {
System.out.println("fill with blue");
}
}
它们都实现了DrawAPI
接口,并提供了具体的draw
方法,用于绘制不同的颜色。
最后我们可以编写一个测试用例来验证桥接模式的实现:
package com.example.javaDesignPattern.bridge;
/**
* @author bug菌
* @version 1.0
* @date 2023/9/19 11:15
*/
public class Client {
public static void main(String[] args) {
Shape circle = new Circle(100,100, 10, new Red());
circle.draw();
Shape rectangle = new Rectangle(50, 50, 20, 30, new Green());
rectangle.draw();
Shape triangle = new Triangle(10, 20, 30, 40, 50, 60, new Blue());
triangle.draw();
}
}
执行结果如下:
代码分析:
如上代码是一个简单的示例程序,其中定义了一个Client
类。在main
方法中,创建了三个形状对象circle
、rectangle
和triangle
,并使用draw
方法绘制了它们。
这三个形状对象分别是:
Circle
:圆形,构造函数中参数依次为圆心位置x、y坐标、半径和颜色对象。Rectangle
:矩形,构造函数中参数依次为左上角位置x、y坐标、宽度、高度和颜色对象。Triangle
:三角形,构造函数中参数依次为三个顶点的x、y坐标和颜色对象。
在这里,颜色对象是使用了简单工厂模式创建的,包括Red
、Green
和Blue
。
组合模式
简介
组合模式 : 将 对象 组合成 树形结构 , 表示 " 部分-整体 " 层次结构 ;
组合模式 使 客户端 对 单个对象 和 组合对象 保持一致的 方式处理 ;
如 : 文件系统 , 根目录下有若干文件和目录 , 在二级目录下还有目录和文件 , 这种情况下 , 适合使用组合模式 ;
适用场景
- 忽略差异 : 希望 客户端 可以 忽略 组合对象 与 单个对象 的差异 ;
- 处理树形结构 ;
优缺点
组合模式优点 :
定义层次 : 清楚地 定义 分层次 的 复杂对象 , 表示 对象 的 全部 或 部分 层次 ;
忽略层次 : 让 客户端 忽略 层次之间的差异 , 方便对 整个层次结构 进行控制 ;
简化客户端代码 ;
符合开闭原则 ;
组合模式缺点 :
限制类型复杂 : 限制类型时 , 比较复杂 ;
如 : 某个目录中只能包含文本文件 , 使用组合模式时 , 不能依赖类型系统 , 施加约束 , 它们都来自于节点的抽象层 ; 在这种情况下 , 必须通过在运行时进行类型检查 , 这样就变得比较复杂 ;
使设计变得更加抽象 ;
组合模式和访问者模式 : 两个模式经常结合起来使用 , 使用 访问者模式 , 访问 组合模式 中的 递归结构 ,
示例
业务逻辑 :
书籍 分属于 目录 下 , 如果使 书籍目录 和 书籍 继承同一个抽象类 ( 目录组件 ) , 可以将 书籍 及 由书籍组成的书籍目录 当做同一个类型的对象进行操作 , 操作上的具体的差别 , 进行定制化处理 ;
1、书籍和目录的抽象父类
抽象类中所有的方法 都抛出异常 , 子类重写需要的方法 , 如果子类实例对象调用没有重写的方法 , 就会抛出异常 ;
package composite;
/**
* 目录组件
* 目录 和 书籍 都继承 CatalogComponent 接口
* 子类根据当前的的类型 , 选择性重写接口
*/
public abstract class CatalogComponent {
/**
* 添加元素
* @param catalogComponent
*/
public void add(CatalogComponent catalogComponent) {
throw new UnsupportedOperationException("不支持的操作");
}
/**
* 删除元素
* @param catalogComponent
*/
public void remove(CatalogComponent catalogComponent) {
throw new UnsupportedOperationException("不支持的操作");
}
/**
* 获取名称
* @param catalogComponent
*/
public String getName(CatalogComponent catalogComponent) {
throw new UnsupportedOperationException("不支持的操作");
}
/**
* 获取价格
* @param catalogComponent
* @return
*/
public double getPrice(CatalogComponent catalogComponent) {
throw new UnsupportedOperationException("不支持的操作");
}
/**
* 如果该组件是目录 , 直接打印目录
* 如果该组件是书籍 , 直接打印课程
*/
public void printf() {
throw new UnsupportedOperationException("不支持的操作");
}
}
2、书籍类
书籍类 重写了 CatalogComponent 的 getName / getPrice / printf 方法 , 如果调用其余方法 , 会抛出异常 ;
package composite;
/**
* 书籍类
* 重写了 CatalogComponent 的 getName / getPrice / printf 方法
* 如果调用其余方法 , 会抛出异常
*/
public class Book extends CatalogComponent {
/**
* 书籍名称
*/
private String name;
/**
* 书籍价格
*/
private double price;
public Book(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public String getName(CatalogComponent catalogComponent) {
return this.name;
}
@Override
public double getPrice(CatalogComponent catalogComponent) {
return this.price;
}
@Override
public void printf() {
System.out.println("Book : name = " + name + " , price = " + price);
}
}
3、目录类
目录类 重写了 CatalogComponent 的 getName / add / remove / printf 方法 , 如果调用其余方法 , 会抛出异常 ;
package composite;
import java.util.ArrayList;
/**
* 目录类
* 重写了 CatalogComponent 的 getName / add / remove / printf 方法
* 如果调用其余方法 , 会抛出异常
*/
public class Catalogue extends CatalogComponent {
/**
* 维护书籍列表
*/
private ArrayList<CatalogComponent> items = new ArrayList<>();
/**
* 书籍名称
*/
private String name;
/**
* 存储当前目录的层数
* 最外面的是 0 层
*/
private Integer level;
public Catalogue(String name, Integer level) {
this.name = name;
this.level = level;
}
@Override
public String getName(CatalogComponent catalogComponent) {
return this.name;
}
@Override
public void add(CatalogComponent catalogComponent) {
items.add(catalogComponent);
}
@Override
public void remove(CatalogComponent catalogComponent) {
items.remove(catalogComponent);
}
@Override
public void printf() {
System.out.println(this.name + " : ");
for (CatalogComponent catalogComponent : items) {
// 这样可以将目录层次完整的打印出来
if (this.level != null) {
for (int i = 0; i < level; i++) {
System.out.print(" ");
}
}
catalogComponent.printf();
}
}
}
4、测试类
package composite;
public class Main {
public static void main(String[] args) {
// 认为 书籍 和 目录 都是 CatalogComponent 类型的
CatalogComponent storyBook = new Book("故事书", 2.0);
CatalogComponent novelBook = new Book("小说", 8.0);
CatalogComponent mathBook = new Book("数学书", 7.0);
CatalogComponent englishBook = new Book("英语书", 3.0);
CatalogComponent schoolBooks = new Catalogue("学校课本", 2);
schoolBooks.add(mathBook);
schoolBooks.add(englishBook);
// 主目录 , 包含上述所有内容 , 2 本书 和 1 个目录
CatalogComponent main = new Catalogue("所有书籍", 1);
main.add(storyBook);
main.add(novelBook);
main.add(schoolBooks);
// 打印主目录
main.printf();
}
}
执行结果 :
所有书籍 :
Book : name = 故事书 , price = 2.0
Book : name = 小说 , price = 8.0
学校课本 :
Book : name = 数学书 , price = 7.0
Book : name = 英语书 , price = 3.0
享元模式
简介
在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。
例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源,这就是享元模式的产生背景。
定义与特点
享元(Flyweight)模式的定义:运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。
享元模式的主要优点是:相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。
其主要缺点是:
- 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
- 读取享元模式的外部状态会使得运行时间稍微变长。
结构与实现
享元模式的定义提出了两个要求,细粒度和共享对象。因为要求细粒度,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。
- 内部状态指对象共享出来的信息,存储在享元信息内部,并且不回随环境的改变而改变;
- 外部状态指对象得以依赖的一个标记,随环境的改变而改变,不可共享。
比如,连接池中的连接对象,保存在连接对象中的用户名、密码、连接URL等信息,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态。而当每个连接要被回收利用时,我们需要将它标记为可用状态,这些为外部状态。
享元模式的本质是缓存共享对象,降低内存消耗。
1. 模式的结构
享元模式的主要角色有如下。
- 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
- 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
- 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
- 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
图 1 是享元模式的结构图,其中:
- UnsharedConcreteFlyweight 是非享元角色,里面包含了非共享的外部状态信息 info;
- Flyweight 是抽象享元角色,里面包含了享元方法 operation(UnsharedConcreteFlyweight state),非享元的外部状态以参数的形式通过该方法传入;
- ConcreteFlyweight 是具体享元角色,包含了关键字 key,它实现了抽象享元接口;
- FlyweightFactory 是享元工厂角色,它是关键字 key 来管理具体享元;
- 客户角色通过享元工厂获取具体享元,并访问具体享元的相关方法。
图1 享元模式的结构图
2. 模式的实现
享元模式的实现代码如下:
public class FlyweightPattern {
public static void main(String[] args) {
FlyweightFactory factory = new FlyweightFactory();
Flyweight f01 = factory.getFlyweight("a");
Flyweight f02 = factory.getFlyweight("a");
Flyweight f03 = factory.getFlyweight("a");
Flyweight f11 = factory.getFlyweight("b");
Flyweight f12 = factory.getFlyweight("b");
f01.operation(new UnsharedConcreteFlyweight("第1次调用a。"));
f02.operation(new UnsharedConcreteFlyweight("第2次调用a。"));
f03.operation(new UnsharedConcreteFlyweight("第3次调用a。"));
f11.operation(new UnsharedConcreteFlyweight("第1次调用b。"));
f12.operation(new UnsharedConcreteFlyweight("第2次调用b。"));
}
}
//非享元角色
class UnsharedConcreteFlyweight {
private String info;
UnsharedConcreteFlyweight(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
}
//抽象享元角色
interface Flyweight {
public void operation(UnsharedConcreteFlyweight state);
}
//具体享元角色
class ConcreteFlyweight implements Flyweight {
private String key;
ConcreteFlyweight(String key) {
this.key = key;
System.out.println("具体享元" + key + "被创建!");
}
public void operation(UnsharedConcreteFlyweight outState) {
System.out.print("具体享元" + key + "被调用,");
System.out.println("非享元信息是:" + outState.getInfo());
}
}
//享元工厂角色
class FlyweightFactory {
private HashMap<String, Flyweight> flyweights = new HashMap<String, Flyweight>();
public Flyweight getFlyweight(String key) {
Flyweight flyweight = (Flyweight) flyweights.get(key);
if (flyweight != null) {
System.out.println("具体享元" + key + "已经存在,被成功获取!");
} else {
flyweight = new ConcreteFlyweight(key);
flyweights.put(key, flyweight);
}
return flyweight;
}
}
程序运行结果如下:
具体享元a被创建!
具体享元a已经存在,被成功获取!
具体享元a已经存在,被成功获取!
具体享元b被创建!
具体享元b已经存在,被成功获取!
具体享元a被调用,非享元信息是:第1次调用a。
具体享元a被调用,非享元信息是:第2次调用a。
具体享元a被调用,非享元信息是:第3次调用a。
具体享元b被调用,非享元信息是:第1次调用b。
具体享元b被调用,非享元信息是:第2次调用b。
应用实例
【例1】享元模式在五子棋游戏中的应用。
分析:五子棋同围棋一样,包含多个“黑”或“白”颜色的棋子,所以用享元模式比较好。
本实例中:
- 棋子(ChessPieces)类是抽象享元角色,它包含了一个落子的 DownPieces(Graphics g,Point pt) 方法;
- 白子(WhitePieces)和黑子(BlackPieces)类是具体享元角色,它实现了落子方法;
- Point 是非享元角色,它指定了落子的位置;
- WeiqiFactory 是享元工厂角色,它通过 ArrayList 来管理棋子,并且提供了获取白子或者黑子的 getChessPieces(String type) 方法;
- 客户类(Chessboard)利用 Graphics 组件在框架窗体中绘制一个棋盘,并实现 mouseClicked(MouseEvent e) 事件处理方法,该方法根据用户的选择从享元工厂中获取白子或者黑子并落在棋盘上。
图 2 所示是其结构图。
图2 五子棋游戏的结构图
程序代码如下:
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
public class WzqGame {
public static void main(String[] args) {
new Chessboard();
}
}
//棋盘
class Chessboard extends MouseAdapter {
WeiqiFactory wf;
JFrame f;
Graphics g;
JRadioButton wz;
JRadioButton bz;
private final int x = 50;
private final int y = 50;
private final int w = 40; //小方格宽度和高度
private final int rw = 400; //棋盘宽度和高度
Chessboard() {
wf = new WeiqiFactory();
f = new JFrame("享元模式在五子棋游戏中的应用");
f.setBounds(100, 100, 500, 550);
f.setVisible(true);
f.setResizable(false);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel SouthJP = new JPanel();
f.add("South", SouthJP);
wz = new JRadioButton("白子");
bz = new JRadioButton("黑子", true);
ButtonGroup group = new ButtonGroup();
group.add(wz);
group.add(bz);
SouthJP.add(wz);
SouthJP.add(bz);
JPanel CenterJP = new JPanel();
CenterJP.setLayout(null);
CenterJP.setSize(500, 500);
CenterJP.addMouseListener(this);
f.add("Center", CenterJP);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
g = CenterJP.getGraphics();
g.setColor(Color.BLUE);
g.drawRect(x, y, rw, rw);
for (int i = 1; i < 10; i++) {
//绘制第i条竖直线
g.drawLine(x + (i * w), y, x + (i * w), y + rw);
//绘制第i条水平线
g.drawLine(x, y + (i * w), x + rw, y + (i * w));
}
}
public void mouseClicked(MouseEvent e) {
Point pt = new Point(e.getX() - 15, e.getY() - 15);
if (wz.isSelected()) {
ChessPieces c1 = wf.getChessPieces("w");
c1.DownPieces(g, pt);
} else if (bz.isSelected()) {
ChessPieces c2 = wf.getChessPieces("b");
c2.DownPieces(g, pt);
}
}
}
//抽象享元角色:棋子
interface ChessPieces {
public void DownPieces(Graphics g, Point pt); //下子
}
//具体享元角色:白子
class WhitePieces implements ChessPieces {
public void DownPieces(Graphics g, Point pt) {
g.setColor(Color.WHITE);
g.fillOval(pt.x, pt.y, 30, 30);
}
}
//具体享元角色:黑子
class BlackPieces implements ChessPieces {
public void DownPieces(Graphics g, Point pt) {
g.setColor(Color.BLACK);
g.fillOval(pt.x, pt.y, 30, 30);
}
}
//享元工厂角色
class WeiqiFactory {
private ArrayList<ChessPieces> qz;
public WeiqiFactory() {
qz = new ArrayList<ChessPieces>();
ChessPieces w = new WhitePieces();
qz.add(w);
ChessPieces b = new BlackPieces();
qz.add(b);
}
public ChessPieces getChessPieces(String type) {
if (type.equalsIgnoreCase("w")) {
return (ChessPieces) qz.get(0);
} else if (type.equalsIgnoreCase("b")) {
return (ChessPieces) qz.get(1);
} else {
return null;
}
}
}
程序运行结果如图 3 所示。
图3 五子棋游戏的运行结果
享元模式的应用场景
当系统中多处需要同一组信息时,可以把这些信息封装到一个对象中,然后对该对象进行缓存,这样,一个对象就可以提供给多出需要使用的地方,避免大量同一对象的多次创建,降低大量内存空间的消耗。
享元模式其实是工厂方法模式的一个改进机制,享元模式同样要求创建一个或一组对象,并且就是通过工厂方法模式生成对象的,只不过享元模式为工厂方法模式增加了缓存这一功能。
前面分析了享元模式的结构与特点,下面分析它适用的应用场景。享元模式是通过减少内存中对象的数量来节省内存空间的,所以以下几种情形适合采用享元模式。
- 系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源。
- 大部分的对象可以按照内部状态进行分组,且可将不同部分外部化,这样每一个组只需保存一个内部状态。
- 由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。
享元模式的扩展
在前面介绍的享元模式中,其结构图通常包含可以共享的部分和不可以共享的部分。在实际使用过程中,有时候会稍加改变,即存在两种特殊的享元模式:单纯享元模式和复合享元模式,下面分别对它们进行简单介绍。
(1) 单纯享元模式,这种享元模式中的所有的具体享元类都是可以共享的,不存在非共享的具体享元类,其结构图如图 4 所示。
图4 单纯享元模式的结构图
(2) 复合享元模式,这种享元模式中的有些享元对象是由一些单纯享元对象组合而成的,它们就是复合享元对象。虽然复合享元对象本身不能共享,但它们可以分解成单纯享元对象再被共享,其结构图如图 5 所示。
图5 复合享元模式的结构图
行为型设计模式
策略模式
策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。需要设计一个接口,为一系列实现类提供统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无,属于辅助类),提供辅助函数。策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可。
/**
* 抽象算法的策略类,定义所有支持的算法的公共接口
*/
abstract class Strategy {
/**
* 算法方法
*/
public abstract void AlgorithmInterface();
}
/**
* 具体算法A
*/
class ConcreteStrategyA extends Strategy {
//算法A实现方法
@Override
public void AlgorithmInterface() {
System.out.println("算法A的实现");
}
}
/**
* 具体算法B
*/
class ConcreteStrategyB extends Strategy {
/**
* 算法B实现方法
*/
@Override
public void AlgorithmInterface() {
System.out.println("算法B的实现");
}
}
/**
* 具体算法C
*/
class ConcreteStrategyC extends Strategy {
@Override
public void AlgorithmInterface() {
System.out.println("算法C的实现");
}
}
/**
* 上下文,维护一个对策略类对象的引用
*/
class Context {
Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void contextInterface(){
strategy.AlgorithmInterface();
}
}
/**
* 客户端代码:实现不同的策略
*/
public class StrategyPattern {
public static void main(String[] args) {
Context context;
context = new Context(new ConcreteStrategyA());
context.contextInterface();
context = new Context(new ConcreteStrategyB());
context.contextInterface();
context = new Context(new ConcreteStrategyC());
context.contextInterface();
}
}
更多的策略模式的文章:Java设计模式-策略模式_虾王之五的技术博客_51CTO博客
模板方法
基本介绍
模板方法模式(Template Method Pattern),又叫模板模式(Template Pattern),在一个抽象类公开定义了执行它的方法的模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。
简单说,模板方法模式,定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构,就可以重定义该算法的某些特定步骤,这种类型的设计模式属于行为型模式。
模板方法结构
原理类图
对原理类图的说明:
AbstractClass 抽象类, 类中实现了模板方法(template),定义了算法的骨架,具体子类需要去实现 其它的抽象方法 operation2,3,4
ConcreteClass 实现抽象方法,假设是operation2,3,4, 以完成算法中特定子类的具体业务步骤
具体实现
1.需求
编写制作豆浆的程序,说明如下:
制作豆浆的流程:选材—>添加配料—>浸泡—>放到豆浆机打碎
通过添加不同的配料,可以制作出不同口味的豆浆
选材、浸泡和放到豆浆机打碎这几个步骤对于制作每种口味的豆浆都是一样的
说明:因为模板方法模式,比较简单,很容易就想到这个方案,因此就直接使用,不再使用传统的方案来引出模板方法模式
2. 方案
思路分析 - 类图
具体实现
// 抽象类,表示豆浆 SoyaMilk.java
public abstract class SoyaMilk {
// 模板方法:可以做成final,不让子类去覆盖
final void make() {
select();
addCondiment();
soak();
beat();
}
//选材料
void select() { System.out.println("第一步:选择新鲜的豆子"); }
//添加不同的配料:抽象方法,由子类具体实现
abstract void addCondiment();
//浸泡
void soak() { System.out.println("第三步:豆子和配料开始浸泡3H"); }
//榨汁
void beat() { System.out.println("第四步:豆子和配料放入豆浆机榨汁"); }
}
// RedBeanSoyaMilk.java
public class ReadBeanSoyaMilk extends SoyaMilk {
@Override
void addCondiment() {
System.out.println("第二步:加入上好的红豆");
}
}
// PeanutSoyMilk.java
public class PeanutSoyaMilk extends SoyaMilk {
@Override
void addCondiment() {
System.out.println("第二步:加入上好的花生");
}
}
// Client.java
public class Client {
public static void main(String[] args) {
System.out.println("=======制作红豆豆浆=======");
SoyaMilk redBeanSoyaMilk = new ReadBeanSoyaMilk();
redBeanSoyaMilk.make();
System.out.println("=======制作花生豆浆=======");
SoyaMilk peanutSoyaMilk = new PeanutSoyaMilk();
peanutSoyaMilk.make();
}
}
3.模板方法模式的钩子方法
在模板方法模式的父类中,可以定义一个方法,它默认不做任何事,子类可以视情况要不要覆盖它,该方法称为“钩子”。还是用上面做豆浆的例子来讲解,比如,我们还希望制作纯豆浆,不添加任何的配料,请使用钩子方法对前面的模板方法进行改造。代码实现如下:
// RedBeanSoyaMilk.java/PeanutSoyaMilk.java同上,略
//抽象类,表示豆浆,SoyaMilk
public abstract class SoyaMilk {
//模板方法:可以做成final,不让子类去覆盖
final void make() {
select();
if(customerWantCondiment()) {
addCondiment();
}
soak();
beat();
}
//1.选材料
void select() { System.out.println("第一步:选择新鲜的豆子"); }
//2.添加不同的配料:抽象方法,由子类具体实现
abstract void addCondiment();
//3.浸泡
void soak() { System.out.println("第三步:豆子和配料开始浸泡3H"); }
//4.榨汁
void beat() { System.out.println("第四步:豆子和配料放入豆浆机榨汁"); }
//钩子方法:决定是否需要添加配料
boolean customerWantCondiment() {
return true;//默认情况下是要加配料的
}
}
// PureSoyaMilk.java
public class PureSoyaMilk extends SoyaMilk {
@Override
void addCondiment() {
// 添加配料的方法 空实现 即可
}
@Override
boolean customerWantCondiment() {
return false;
}
}
// Client.java
public class Client {
public static void main(String[] args) {
System.out.println("=制作纯豆浆=");
SoyaMilk pureSoyMilk = new PureSoyaMilk();
pureSoyMilk.make();
}
}
注意事项和细节
基本思想是:算法只存在于一个地方,也就是在父类中,容易修改。需要修改算法时,只要修改父类的模板方法或者已经实现的某些步骤,子类就会继承这些修改
好处:
实现了最大化代码复用。父类的模板方法和已实现的某些步骤会被子类继承而直接使用;
既统一了算法,也提供了很大的灵活性。父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步骤的实现
该模式的不足之处:每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大
注意:一般模板方法都加上 final 关键字, 防止子类重写模板方法
模板方法模式使用场景:
当要完成在某个过程,该过程要执行一系列步骤 ,这一系列的步骤基本相同,但其个别步骤在实现时可能不同,通常考虑用模板方法模式来处理
统计某一段代码的执行时间也可以用模板方法模式:在前面打印出代码执行前的时间,后面再打印出代码执行后的时间,中间部分就是不同的执行代码
观察者模式
观察者模式是一种常用的设计模式,它定义了对象间的一种一对多的依赖关系,使得当一个对象状态改变时,它的所有依赖对象都会被自动通知并且更新。这种模式也被称为发布/订阅模式。
在 Java
中,观察者模式通常由两个接口组成:Subject
和 Observer
。Subject
是被观察的对象,Observer
是观察者。Subject
接口定义了一系列方法,用于添加、删除和通知观察者,Observer
接口定义了一个 update()
方法,当 Subject
状态发生改变时,该方法被调用。
下面是一个使用观察者模式的示例,假设我们有一个主题(Subject
),它需要通知许多观察者(Observers
):
import java.util.ArrayList;
import java.util.List;
public class Subject {
private List<Observer> observers = new ArrayList<Observer>();
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
notifyAllObservers();
}
public void attach(Observer observer){
observers.add(observer);
}
public void notifyAllObservers(){
for (Observer observer : observers) {
observer.update();
}
}
}
public abstract class Observer {
protected Subject subject;
public abstract void update();
}
public class BinaryObserver extends Observer{
public BinaryObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
public void update() {
System.out.println( "Binary String: " + Integer.toBinaryString( subject.getState() ) );
}
}
public class OctalObserver extends Observer{
public OctalObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
public void update() {
System.out.println( "Octal String: " + Integer.toOctalString( subject.getState() ) );
}
}
public class HexaObserver extends Observer{
public HexaObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
public void update() {
System.out.println( "Hex String: " + Integer.toHexString( subject.getState() ).toUpperCase() );
}
}
public class ObserverPatternDemo {
public static void main(String[] args) {
Subject subject = new Subject();
new HexaObserver(subject);
new OctalObserver(subject);
new BinaryObserver(subject);
System.out.println("First state change: 15");
subject.setState(15);
System.out.println("Second state change: 10");
subject.setState(10);
}
}
在这个例子中,我们创建了一个Subject
类,它包含一个列表observers
,其中包含所有注册的观察者。Subject
类还有一个状态state
,它表示主题状态的变化。当状态改变时,notifyAllObservers()
方法被调用,通知所有观察者更新它们的状态。
我们还创建了三个具体的观察者:BinaryObserver
,OctalObserver
和HexaObserver
。每个观察者都需要一个Subject
对象,并注册自己到该对象中。当Subject
状态改变时,每个观察者都会收到通知并更新自己的状态,其中update()
方法用于更新观察者状态。在ObserverPatternDemo
类中,我们创建一个Subject
对象,并向其注册三个观察者。然后,我们对主题进行两次状态更改,并在控制台输出每个观察者的状态。
First state change: 15
Hex String: F
Octal String: 17
Binary String: 1111
Second state change: 10
Hex String: A
Octal String: 12
Binary String: 1010
观察者模式的优点:
- 降低耦合性:观察者模式将主题(
Subject
)和观察者(Observer
)之间的依赖关系解耦,使得它们可以独立地改变。主题不需要知道哪些观察者正在观察它,观察者也不需要知道主题的细节。 - 易于扩展:由于观察者模式是松散耦合的,因此可以很容易地添加或删除观察者,而不会影响主题或其他观察者。这使得代码更加灵活,易于扩展。
- 实现了开放/封闭原则:观察者模式遵循开放/封闭原则,即对于扩展是开放的,对于修改是封闭的。这意味着可以通过添加新的观察者来扩展系统的功能,而不必更改现有代码。
- 实现了可重用性:观察者模式将主题和观察者分离,使它们可以独立地进行测试和重用。
- 提高了灵活性和可维护性:观察者模式使得系统中的对象之间的通信变得更加灵活和可维护。它还使代码更易于理解和维护。
观察者模式的缺点:
- 内存泄漏:在观察者模式中,观察者需要注册到主题对象,当观察者不再需要时,需要从主题中注销,否则会导致内存泄漏。因为如果观察者对象没有被注销,则主题对象将保留对它的引用,这将防止观察者被垃圾回收。
- 性能问题:观察者模式在一些情况下可能会导致性能问题。当主题对象有大量观察者时,每当主题状态发生变化时,所有观察者都会被通知,这可能会导致性能问题。
- 并发问题:如果多个线程同时访问主题对象,并且主题对象在处理一个观察者时状态发生了变化,则可能会导致并发问题。因此,在使用观察者模式时需要小心处理并发问题。
- 对象的生命周期问题:观察者模式中的主题对象和观察者对象之间可能存在生命周期的问题。如果观察者对象在主题对象的生命周期内保持活动状态,这可能会导致一些不必要的问题。
迭代子模式
定义:
Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
提供一种顺序访问聚集/容器对象元素的方法,而又无须暴露聚集内部表象。
迭代器模式(Iterator Pattern) 又称为 游标(Cursor)模式,是对象的行为模式。
迭代器模式 可以顺序地访问一个聚集中的元素,而又不会暴露聚集内部表象。也即 迭代器模式 可以为不同的容器提供一致的遍历行为,而不用关心容器内容元素组成结构。
迭代器模式 本质:抽离聚集对象迭代行为到迭代器种,提供一致访问接口。
主要解决:
我们把多个对象聚在一起形成的总体称之为聚集(Aggregate),聚集对象是能够包容一组对象的容器对象。不同的聚集其内部元素的聚合结构可能不同,而 迭代器模式 屏蔽了内部元素获取细节,为外部提供一致的元素访问行为,解耦了元素迭代与集合对象间的耦合,并且通过提供不同的迭代器,可以为同个聚集对象提供不同顺序的元素访问行为,扩展了聚集对象元素迭代功能,符合 开闭原则。
使用场景
- 遍历一个容器对象时;
具体实现:
迭代子模式(Iterator)
顾名思义,迭代器模式就是顺序访问聚集中的对象,一般来说,集合中非常常见,如果对集合类比较熟悉的话,理解本模式会十分轻松。这句话包含两层意思:一是需要遍历的对象,即聚集对象,二是迭代器对象,用于对聚集对象进行遍历访问。我们看下关系图:
这个思路和我们常用的一模一样,MyCollection中定义了集合的一些操作,MyIterator中定义了一系列迭代操作,且持有Collection实例,我们来看看实现代码:
两个接口:
两个实现:
测试类:
输出:A B C D E
此处我们貌似模拟了一个集合类的过程,感觉是不是很爽?其实JDK中各个类也都是这些基本的东西,加一些设计模式,再加一些优化放到一起的,只要我们把这些东西学会了,掌握好了,我们也可以写出自己的集合类,甚至框架!
优缺点 :
优点
- 多态迭代:为不同的聚合结构提供一致的遍历接口,即一个迭代接口可以访问不同的聚集对象;
- 简化聚集对象接口:迭代器模式 将聚集对象本身应该提供的元素迭代接口抽取到了迭代器中,使聚集对象无须关心具体迭代行为;
- 元素迭代功能多样化:每个聚集对象都可以提供一个或多个不同的迭代器,使的同种元素聚合结构可以有不同的迭代行为;
- 解耦迭代与聚集:迭代器模式 封装了具体的迭代算法,迭代算法的变化,不会影响到聚集对象的架构;
缺点
- 对于比较简单的遍历(像数组或者有序列表),使用迭代器方式遍历较为繁琐;
责任链设计模式
类继承关系图如下:
改进之后的代码基本上概括了责任链设计模式的使用,但是上述客户端的代码其实也是很繁琐的,后面我会带着大家继续优化责任链设计模式,将它的方方面面争取讲透彻。下面我们趁热打铁来看几个责任链设计模式的概念。
什么是责任链设计模式
- 客户端发出一个请求,链上的对象都有机会来处理这一请求,而客户端不需要知道谁是具体的处理对象。
- 多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。
- 将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。其过程实际上是一个递归调用。
总结一下上面的几个概念:
- 有多个对象共同对一个任务进行处理。
- 这些对象使用链式存储结构,形成一个链,每个对象知道自己的下一个对象。
- 一个对象对任务进行处理,可以添加一些操作后将对象传递个下一个任务。也可以在此对象上结束任务的处理,并结束任务。
- 客户端负责组装链式结构,但是客户端不需要关心最终是谁来处理了任务。
这个概念很抽象,估计大家看了也很难理解,其实不用想的那么复杂。相信大家都有做过表单校验的工作,假如此时需要做一个登陆校验(用户名、密码、验证码),首先肯定是校验用户名,校验通过则进入下一步校验密码,否则提示用户,校验密码用过,则校验验证码,否则提示用户,表单全部校验通过,才开始提交到后台。其实这也是一个典型的责任链设计模式的运用。如下图:
责任链模式的类结构图
在上述类结构图中,最上层是一个抽象类,抽象类持有自己的引用,其实是用来接收下一个处理者的。当然,大家也可以在抽象类的上层定义一个接口,这样扩展性在一定场景下会更优。
应用场景
- 多条件流程判断:权限控制
- ERP 系统流程审批:总经理、人事经理、项目经理
- Java 过滤器的底层实现 Filter
就我个人理解而言,如果一个逻辑是按照一定的步骤进行的,而步骤之间存在复杂的逻辑计算,那么可以考虑使用责任链设计模式。或者说,当你的代码中出现这种情况的时候,你也可以考虑通过责任链设计模式来改进。
if(true){
//这里有很多的逻辑
return;
}
if(true){
//这里有很多的逻辑
return;
}
if(true){
//这里有很多的逻辑
return;
}
if(true){
//这里有很多的逻辑
return;
}
基本使用
1. 抽象处理者(Handler)角色
定义出一个处理请求的接口(或者抽象类)。如果需要,接口可以定义出一个方法以设定和返回对下家的引用。这个角色通常由一个 Java 抽象类或者 Java 接口实现。下图中 AbstractHandler 类的聚合关系给出了具体子类对下家的引用,抽象方法 handleRequest() 规范了子类处理请求的操作。
public abstract class AbstractHandler {
protected AbstractHandler next;
public void setNext(AbstractHandler next) {
this.next = next;
}
public abstract void handlerRequest();
}
2. 具体处理者(ConcreteHandler)角色
具体处理者接到请求后,可以选择将请求处理掉,或者将请求传给下一个处理者。
/**
* 具体处理者A
*/
public class ConcreteHandler_A extends AbstractHandler{
public void handlerRequest() {
if(this.next != null){
this.next.handlerRequest();
}
}
}
/**
* 具体处理者B
*/
public class ConcreteHandler_B extends AbstractHandler{
public void handlerRequest() {
if(this.next != null){
this.next.handlerRequest();
}
}
}
3. 责任链客户端
责任链客户端设置处理者链,并且返回第一个处理者:
public class HandlerClient {
public static void main(String[] args) {
AbstractHandler firstHandler = new HandlerClient().getFirstHandler();
// 调用第一个处理者的handler方法
firstHandler.handlerRequest();
}
/**
* 设置责任链 并返回第一个处理者
* @return
*/
public AbstractHandler getFirstHandler(){
AbstractHandler a = new ConcreteHandler_A();
AbstractHandler b = new ConcreteHandler_B();
a.setNext(b);
return a;
}
}
应用实例
关卡游戏中,只有当你通过第一关才能进入第二关,通过第二关才能进入第三关。以此类推。
下面我将通过关卡游戏的小实例来进入责任链模式,在进入代码之前我们先来明确几点游戏要求。
- 游戏一共 3 个关卡
- 进入第二关需要第一关的游戏得分大于等于 80
- 进入第三关需要第二关的游戏得分大于等于 90
关卡流程图
下面我们就来完成这个代码,首先定义 3 个关卡(类),代码非常简单,每个类的方法都一样,只是返回的结果不相同。
第一关:返回游戏得分 80
/**
*第一关
*/
public class FirstPassHandler {
public int handler(){
System.out.println("第一关-->FirstPassHandler");
return 80;
}
}
第二关:返回游戏得分 90
/**
*第二关
*/
public class SecondPassHandler {
public int handler(){
System.out.println("第二关-->SecondPassHandler");
return 90;
}
}
第三关:返回游戏得分 95
/**
*第三关
*/
public class ThirdPassHandler {
public int handler(){
System.out.println("第三关-->ThirdPassHandler,这是最后一关啦");
return 95;
}
}
客户端
public class HandlerClient {
public static void main(String[] args) {
FirstPassHandler firstPassHandler = new FirstPassHandler();//第一关
SecondPassHandler secondPassHandler = new SecondPassHandler();//第二关
ThirdPassHandler thirdPassHandler = new ThirdPassHandler();//第三关
int firstScore = firstPassHandler.handler();
//第一关的分数大于等于80则进入第二关
if(firstScore >= 80){
int secondScore = secondPassHandler.handler();
//第二关的分数大于等于90则进入第二关
if(secondScore >= 90){
thirdPassHandler.handler();
}
}
}
}
客户端代码也是非常简单,代码执行也没有任何问题。但是这段代码扩展性非常不好,有很大的问题。问题主要如下:
1. 假如现在要增加一个关卡,那么需要在 if 嵌套中增加一个 if 分支。如果关卡变多了,代码结构就会变成下面这样,if 嵌套将会一直循环下去,会非常糟糕的。
if(第1关通过){
// 第2关 游戏
if(第2关通过){
// 第3关 游戏
if(第3关通过){
// 第4关 游戏
if(第4关通过){
// 第5关 游戏
if(第5关通过){
// 第6关 游戏
if(第6关通过){
//...
}
}
}
}
}
}
2. 如果此时我想更改关卡的顺序,比如将第 3 关放到第 1 关,第 4 关放到第 2 关,每次更改关卡的顺序非常不便,而且更改关卡的顺序,对应关卡的逻辑也要跟着一起改变位置,非常麻烦,而且容易改出问题。
if(第1关通过(更改之前的第3关)){
// 第2关 游戏
if(第2关通过(更改之前的第4关)){
// 第3关 游戏
if(第3关通过(更改之前的第1关)){
// 第4关 游戏
if(第4关通过(更改之前的第2关)){
// 第5关 游戏
if(第5关通过){
// 第6关 游戏
if(第6关通过){
//...
}
}
}
}
}
}
那么,怎么解决上面的问题呢?答案就是我们今天要讲的责任链设计模式。责任链责任链,主要是体现在一个链字上面。也就是关卡与关卡之间将要形成一条链。但是这里有一个问题,这些关卡之间怎么样形成一条链呢?其实这样想也很简单,第一关通过需要进入第二关,那么是不是说第一关需要知道自己的下一关是第二关呢?同理,第二关需要知道自己的下一关是谁。用面向对象的思路来说,就是第一关要有一个属性(第二关)。在代码中的表现如下。
责任链中的第一关:
/**
*第一关
*/
public class FirstPassHandler {
/**
* 第一关的下一关是 第二关
*/
private SecondPassHandler secondPassHandler;
public void setSecondPassHandler(SecondPassHandler secondPassHandler) {
this.secondPassHandler = secondPassHandler;
}
//本关卡游戏得分
private int play(){
return 80;
}
public int handler(){
System.out.println("第一关-->FirstPassHandler");
if(play() >= 80){
//分数>=80 并且存在下一关才进入下一关
if(this.secondPassHandler != null){
return this.secondPassHandler.handler();
}
}
return 80;
}
}
责任链中的第二关:
/**
*第二关
*/
public class SecondPassHandler {
/**
* 第二关的下一关是 第三关
*/
private ThirdPassHandler thirdPassHandler;
public void setThirdPassHandler(ThirdPassHandler thirdPassHandler) {
this.thirdPassHandler = thirdPassHandler;
}
//本关卡游戏得分
private int play(){
return 90;
}
public int handler(){
System.out.println("第二关-->SecondPassHandler");
if(play() >= 90){
//分数>=90 并且存在下一关才进入下一关
if(this.thirdPassHandler != null){
return this.thirdPassHandler.handler();
}
}
return 90;
}
}
责任链中的第三关:
/**
*第三关
*/
public class ThirdPassHandler {
//本关卡游戏得分
private int play(){
return 95;
}
/**
*
* 这是最后一关,因此没有下一关
*/
public int handler(){
System.out.println("第三关-->ThirdPassHandler,这是最后一关啦");
return play();
}
}
责任链中的客户端:
public class HandlerClient {
public static void main(String[] args) {
FirstPassHandler firstPassHandler = new FirstPassHandler();//第一关
SecondPassHandler secondPassHandler = new SecondPassHandler();//第二关
ThirdPassHandler thirdPassHandler = new ThirdPassHandler();//第三关
firstPassHandler.setSecondPassHandler(secondPassHandler);//第一关的下一关是第二关
secondPassHandler.setThirdPassHandler(thirdPassHandler);//第二关的下一关是第三关
//说明:因为第三关是最后一关,因此没有下一关
//开始调用第一关 每一个关卡是否进入下一关卡 在每个关卡中判断
firstPassHandler.handler();
}
}
在上面的责任链中的客户端代码中,我们拿到第一个关卡,然后调用第一个关卡的 handler 方法,就可以让这一条链上的关卡都有机会被执行到。也就是说,在责任链设计模式中,我们只需要拿到链上的第一个处理者,那么链上的每个处理者都有机会处理相应的请求。
如果大家在之前学过链表的话理解起来就非常的简单。责任链设计模式和链表非常相似。
在上面的代码中,虽然我们将 3 个处理者(关卡)形成了一条链,但是代码扩展性非常不好,而且形成链很不方便。 首先,每个关卡中都有下一关的成员变量并且是不一样的,其次对应的 get、set 方法也不一样了,所以设置成为一条链的生活很不方便。
下面我们就想办法来解决上面 2 个问题,让每个关卡的下一关的引用是一样的。这个时候就需要一点抽象的思维了。在关卡之上抽象出来一个父类或者一个接口,然后每个具体的关卡继承或者实现,是不是就 OK 了的,答案是肯定的。接下来我们就来改造上面的三个关卡。
第一步:抽象出来一个抽象类
public abstract class AbstractHandler {
/**
* 下一关用当前抽象类来接收
*/
protected AbstractHandler next;
public void setNext(AbstractHandler next) {
this.next = next;
}
public abstract int handler();
}
第二步:每个关卡实现抽象类
1. 责任链中的第一关(改进之后)
/**
*第一关
*/
public class FirstPassHandler extends AbstractHandler{
private int play(){
return 80;
}
@Override
public int handler(){
System.out.println("第一关-->FirstPassHandler");
int score = play();
if(score >= 80){
//分数>=80 并且存在下一关才进入下一关
if(this.next != null){
return this.next.handler();
}
}
return score;
}
}
2. 责任链中的第二关(改进之后)
/**
*第二关
*/
public class SecondPassHandler extends AbstractHandler{
private int play(){
return 90;
}
public int handler(){
System.out.println("第二关-->SecondPassHandler");
int score = play();
if(score >= 90){
//分数>=90 并且存在下一关才进入下一关
if(this.next != null){
return this.next.handler();
}
}
return score;
}
}
3. 责任链中的第三关(改进之后)
/**
*第三关
*/
public class ThirdPassHandler extends AbstractHandler{
private int play(){
return 95;
}
public int handler(){
System.out.println("第三关-->ThirdPassHandler");
int score = play();
if(score >= 95){
//分数>=95 并且存在下一关才进入下一关
if(this.next != null){
return this.next.handler();
}
}
return score;
}
}
第三步:配置责任链
public class HandlerClient {
public static void main(String[] args) {
FirstPassHandler firstPassHandler = new FirstPassHandler();//第一关
SecondPassHandler secondPassHandler = new SecondPassHandler();//第二关
ThirdPassHandler thirdPassHandler = new ThirdPassHandler();//第三关
// 和上面没有更改的客户端代码相比,只有这里的set方法发生变化,其他都是一样的
firstPassHandler.setNext(secondPassHandler);//第一关的下一关是第二关
secondPassHandler.setNext(thirdPassHandler);//第二关的下一关是第三关
//说明:因为第三关是最后一关,因此没有下一关
//从第一个关卡开始
firstPassHandler.handler();
}
}
责任链模式的优缺点
优点
- 动态组合,使请求者和接受者解耦。
- 请求者和接受者松散耦合:请求者不需要知道接受者,也不需要知道如何处理。每个职责对象只负责自己的职责范围,其他的交给后继者。各个组件间完全解耦。
- 动态组合职责:职责链模式会把功能分散到单独的职责对象中,然后在使用时动态的组合形成链,从而可以灵活的分配职责对象,也可以灵活的添加改变对象职责。
缺点
- 产生很多细粒度的对象:因为功能处理都分散到了单独的职责对象中,每个对象功能单一,要把整个流程处理完,需要很多的职责对象,会产生大量的细粒度职责对象。
- 不一定能处理:每个职责对象都只负责自己的部分,这样就可以出现某个请求,即使把整个链走完,都没有职责对象处理它。这就需要提供默认处理,并且注意构造链的有效性。
上面都是责任链设计模式的基本概念和一些简单的运用,接下来我将带领大家进入实战部分,让大家彻底的吃透责任链设计模式。
实战:责任链设计模式实现网关权限控制
如果现在让你设计一个网关,你会怎么设计呢?在一般的网关中,都会经过 API 接口限流、黑名单拦截、用户会话、参数过滤等这么几个关键点。下面我们就通过责任链设计模式来实现网关权限控制。
首先定义一个抽象处理者,并持有对自己的引用(用于接收下一个处理者:
/**
* 网关抽象处理者
*/
public abstract class GetewayHandler {
protected GatewayHandler next;
public void setNext(GatewayHandler next) {
this.next = next;
}
public abstract void service();
}
定义具体处理者:API 接口限流
/**
* api接口限流
*/
public class ApiLimitGetewayHandler extends GetewayHandler{
public void service() {
System.out.println("第一步,api接口限流校验");
if(this.next != null){
this.next.service();
}
}
}
定义具体处理者:黑名单拦截
/**
* 黑名单拦截
*/
public class BlacklistGetwayHandler extends GatewayHandler{
public void service() {
System.out.println("第二步,黑名单拦截校验");
if(this.next != null){
this.next.service();
}
}
}
定义具体处理者:用户会话拦截
/**
* 用户会话拦截
*/
public class SessionGetwayHandler extends GetewayHandler{
public void service() {
System.out.println("第三步,用户会话拦截校验");
if(this.next != null){
this.next.service();
}
}
}
定义具体处理者:参数果过滤拦截
/**
* 参数过滤拦截
*/
public class ParamGetwayHandler extends GetewayHandler{
public void service() {
System.out.println("第四步,参数过滤拦截");
if(this.next != null){
this.next.service();
}
}
}
定义网关客户端:设置网关请求链
public class GetewayClient {
public static void main(String[] args) {
//api接口限流
GetewayHandler apiLimitGetewayHandler = new ApiLimitGetewayHandler();
//黑名单拦截
GetewayHandler blacklistGetwayHandler = new BlacklistGetwayHandler();
//用户会话拦截
GetewayHandler sessionGetwayHandler = new SessionGetwayHandler();
//参数过滤
GetewayHandler paramGetwayHandler = new ParamGetwayHandler();
apiLimitGetewayHandler.setNext(blacklistGetwayHandler);//api接口限流的下一步是黑名单拦截
blacklistGetwayHandler.setNext(sessionGetwayHandler);//杯名单拦截的下一步是用户会话拦截
sessionGetwayHandler.setNext(paramGetwayHandler);//用户会话拦截的下一步是参数果过滤拦截
apiLimitGetewayHandler.service();
}
}
运行结果如下:
实战:使用工厂模式实现责任链设计模式
在上面的网关客户端中,对责任链进行了初始化设置,实际上对于客户端而言,并不需要和如此复杂的设置链交互。对于客户端,只要拿到链上的第一个处理者就可以了。下面我们就结合工厂设计模式来实现责任链,简化客户端的交互。
首先我们来定义一个工厂类,返回第一个请求处理者:
/**
* 网关责任链工厂 设置请求链
*/
public class GetewayHandlerFactory {
public static GetewayHandler getFirstGetewayHandler(){
//api接口限流
GetewayHandler apiLimitGetewayHandler = new ApiLimitGetewayHandler();
//黑名单拦截
GetewayHandler blacklistGetwayHandler = new BlacklistGetwayHandler();
//用户会话拦截
GetewayHandler sessionGetwayHandler = new SessionGetwayHandler();
//参数过滤
GetewayHandler paramGetwayHandler = new ParamGetwayHandler();
apiLimitGetewayHandler.setNext(blacklistGetwayHandler);//api接口限流的下一步是黑名单拦截
blacklistGetwayHandler.setNext(sessionGetwayHandler);//杯名单拦截的下一步是用户会话拦截
sessionGetwayHandler.setNext(paramGetwayHandler);//用户会话拦截的下一步是参数果过滤拦截
return apiLimitGetewayHandler;
}
}
接下来,客户端通过工厂获取到第一个请求处理者:
public class GetewayClient {
public static void main(String[] args) {
GetewayHandler firstGetewayHandler = GetewayHandlerFactory.getFirstGetewayHandler();
firstGetewayHandler.service();
}
}
对于上面的请求链,我们也可以把这个关系维护到配置文件中或者一个枚举中。我将使用枚举来教会大家怎么动态的配置请求链并且将每个请求者形成一条调用链。
首先定义一个枚举,枚举通过 GetewayEntity 来存储配置项,枚举如下:
public enum GetewayEnum {
/**
* 在这里大家需要注意到一个点 api接口限流是第一个处理者 因此没有 prehandlerId,也就是 它的prehandlerId = null
*/
API_HANDLER(new GetewayEntity(1,"api接口限流","com.simple.handler.demo003.ApiLimitGetewayHandler",null,2)),
BLACKLIST_HANDLER(new GetewayEntity(2,"黑名单拦截","com.simple.handler.demo003.BlacklistGetwayHandler",1,3)),
SESSION_HANDLER(new GetewayEntity(3,"用户会话拦截","com.simple.handler.demo003.SessionGetwayHandler",2,4)),
/**
* 这是最后一个处理者,因此没有下一个 nexthandlerId 也就是它的 nexthandlerId = null
*/
PARAM_HANDLER(new GetewayEntity(4,"参数过滤拦截","com.simple.handler.demo003.ParamGetwayHandler",3,null)),
;
GetewayEntity getewayEntity;
public GetewayEntity getGetewayEntity() {
return getewayEntity;
}
GetewayEnum(GetewayEntity getewayEntity) {
this.getewayEntity = getewayEntity;
}
}
大家不要把这个枚举想的那么复杂,其实就相当于数据库中的几条记录。下面我们通过一个 dao 操作来模拟从数据库获取配置信息,其实是从刚才定义的枚举中获取值。
1. 定义一个 Dao
public interface GetewayDao {
/**
* 根据 handlerId 获取配置项
* @param handlerId
* @return
*/
GetewayEntity getGetewayEntity(Integer handlerId);
/**
* 获取第一个处理者
* @return
*/
GetewayEntity getFirstGetewayEntity();
}
2. 定义 Dao 的实现类
public class GetewayDaoImpl implements GetewayDao{
/**
* 初始化,将枚举中配置的handler 初始化到map中,方便获取
*/
private static Map<Integer, GetewayEntity> getewayEntityHashMap = new HashMap<Integer, GetewayEntity>();
static {
GetewayEnum[] values = GetewayEnum.values();
for (GetewayEnum value : values) {
GetewayEntity getewayEntity = value.getGetewayEntity();
getewayEntityHashMap.put(getewayEntity.getHandlerId(),getewayEntity);
}
}
@Override
public GetewayEntity getGetewayEntity(Integer handlerId) {
return getewayEntityHashMap.get(handlerId);
}
/**
* 获取第一个handler的配置项 枚举
* @return
*/
@Override
public GetewayEntity getFirstGetewayEntity(){
for(Map.Entry<Integer,GetewayEntity> entry : getewayEntityHashMap.entrySet()){
GetewayEntity value = entry.getValue();
// 没有上一个handler的就是第一个
if(value.getPrehandlerId() == null){
return value;
}
}
return null;
}
}
3. 接下来改造责任链工厂的实现
/**
* 网关责任链工厂 设置请求链
*/
public class GetewayHandlerEnumFactory {
private static GetewayDao getewayDao = new GetewayDaoImpl();
public static GetewayHandler getFirstGetewayHandler(){
// 1\. 获取第一处理者 那么哪个是第一个处理者呢
// prehandlerId == null 的就是第一个handler
GetewayEntity firstGetewayEntity = getewayDao.getFirstGetewayEntity();
GetewayHandler firstGetewayHandler = newGetewayHandler(firstGetewayEntity);
if(firstGetewayHandler == null){
return null;
}
GetewayEntity tempGetewayEntity = firstGetewayEntity;
Integer nexthandlerId = null;//表示下一个处理者的id 唯一标识
GetewayHandler tempGetewayHandler = firstGetewayHandler;//临时变量 用于做递归设置下一个handler
//递归获取到tempGetewayHandler的下一个handlerId
while((nexthandlerId = tempGetewayEntity.getNexthandlerId()) != null){
GetewayEntity getewayEntity = getewayDao.getGetewayEntity(nexthandlerId);
GetewayHandler getewayHandler = newGetewayHandler(getewayEntity);
tempGetewayHandler.setNext(getewayHandler);
tempGetewayHandler = getewayHandler;
tempGetewayEntity = getewayEntity;
}
return firstGetewayHandler;
}
/**
* 反射实体化具体的处理者
* @param getewayEntity
* @return
*/
public static GetewayHandler newGetewayHandler(GetewayEntity getewayEntity){
String handlerClassName = getewayEntity.getHandlerClassName();
try {
Class<?> clazz = Class.forName(handlerClassName);
return (GetewayHandler) clazz.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
在上面工厂代码中,设置责任链稍微有点复杂,如果大家觉得比较吃力,建议大家稍微看一看递归调用和单项链表这个数据结构,然后再回过头来看看这段代码就不会那么吃力了。
4. 客户端调用工厂获取到第一个 handler
public class GetewayClient {
public static void main(String[] args) {
GetewayHandler firstGetewayHandler = GetewayHandlerEnumFactory.getFirstGetewayHandler();
firstGetewayHandler.service();
}
}
5. 接下来通过 Debug 看一下获取到的第一个 handler 的结构。
从上面这张图中,可以很清晰的看到整个请求链。
ApiLimitGetewayHandler → BlacklistGetwayHandler → SessionGetwayHandler → ParamGetwayHandler
在上面的工厂中,设置整个请求链相对复杂一些,下面我再给大家扩展一种通过方法递归的方式来设置请求链。
/**
* 网关责任链工厂 设置请求链
*/
public class GetewayHandlerEnumFactory {
private static GetewayDao getewayDao = new GetewayDaoImpl();
public static GetewayHandler getFirstGetewayHandler1(){
// 1\. 获取第一处理者 那么那个是第一个处理者呢 prehandlerId == null 的就是第一个handler
GetewayEntity firstGetewayEntity = getewayDao.getFirstGetewayEntity();
GetewayHandler firstGetewayHandler = newGetewayHandler(firstGetewayEntity);
if(firstGetewayHandler == null){
return null;
}
setNextGetewayHandler(firstGetewayEntity,firstGetewayHandler);
return firstGetewayHandler;
}
private static void setNextGetewayHandler(GetewayEntity getewayEntity,GetewayHandler getewayHandler){
if(getewayHandler != null && getewayEntity != null){
Integer nexthandlerId = getewayEntity.getNexthandlerId();
GetewayEntity nextGetewayEntity = getewayDao.getGetewayEntity(nexthandlerId);
GetewayHandler nextGetewayHandler = newGetewayHandler(getewayEntity);
getewayHandler.setNext(nextGetewayHandler);
//递归 设置
setNextGetewayHandler(nextGetewayEntity,nextGetewayHandler);
}
}
/**
* 反射实体化具体的处理者
* @param getewayEntity
* @return
*/
private static GetewayHandler newGetewayHandler(GetewayEntity getewayEntity){
if(getewayEntity == null){
return null;
}
String handlerClassName = getewayEntity.getHandlerClassName();
try {
Class<?> clazz = Class.forName(handlerClassName);
return (GetewayHandler) clazz.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
通过递归方法设置责任链 Debug 结果如下:
在上面截图中,我们调用的是 getFirstGetewayHandler1 方法,结果和通过 while 循环递归是一样的,但是这种方式相对看着更加简洁一些。在实际工作过程中,大家可用根据自己的一个实际情况进行选择使用哪种方法,我比较推荐大家使用方法递归的方式实现。
在这里再提醒一下大家,如果需要更改请求链的执行顺序,直接更改枚举中的配置项,也就是 preHandler 或者 nextHandlerId 就可以更改请求链的顺序。
实战:使用数据库方式实现责任链设计模式
这一部分我不在通过具体的方式带着大家实现,因为这一部分和上面的通过枚举配置设置项很相似,其实就是把通过枚举配置这种方式换到了通过数据库来实现配置,再从数据库中读取出来。下面我和大家讲一下具体的实现思路。
- 将 GetewayEntity 实体类中的属性转换为数据库表的字段,相信这个对大家来说也不难。
- 将 GetewayDaoImpl 换成从数据库读取配置数据,其他的都不变就可以实现通过数据的方式来实现责任链设计模式
责任链模式总结
- 首先定义一个抽象类,该抽象类定义了请求处理方法和持有对自己的引用(接收下一个处理者)
- 继承上面的抽象类,定义具体的请求处理者
- 设置处理请求链,处理请求链,可以在代码中写死,也可以通过存储在数据库中再读取出来。
命令模式
命令模式(Command)的定义:将发送命令者封装成对象(类比电视遥控器),使发送请求的职责与处理请求的职责分离,即解耦,使两者通过命令进行通讯。这样,当需要管理这些对象的工作情况,就可以直接管理这些命令实例的集合,而无需修改具体的处理逻辑。命令模式能够方便对象之间的调用,提高系统的可拓展性。
命令模式的结构
命令模式主要包含以下结构:
- 抽象命令(Command):定义了命令的接口和执行命令的抽象函数
execute()
。 - 具体命令(ConcreteCommand):实现了抽象命令中的所有方法,并管理一个接收者(Receiver)对象,通过调用接收者(Receiver)的功能来完成命令的具体操作。
- 接收者(Receiver):接收者负责接收并执行命令的具体操作,是业务逻辑的处理类。
- 调用者(Invoker):调用者即发送请求的类,它管理着命令对象,并通过调用命令对象来执行请求,请注意,它不能直接访问接收者。
命令模式的实际应用场景
- 与硬件的串口通讯:在于硬件进行串口通信时,常常会有大量命令需要传输,如果使用
if else
语句进行判断后传输给硬件,会造成代码杂乱无章,难以拓展。使用命令模式将重要指令进行封装,能够将命令的发送和处理进行逻辑分离,有利于系统的解耦。 - 复杂文本编辑器的操作:文本编辑器通常需要各种功能,例如最基本的保存、复制、粘贴操作。如果体量很小,我们当然无需使用命令模式来增加系统的复杂度。但通常实际项目的体量往往比我们设想的大得多,所以我们需要提前考虑项目的拓展。例如文本编辑器还有撤销、重做等功能。这个时候使用命令模式就是必要的。
注:在使用命令模式时,我们还可以将一系列命令以
java.util.List
即列表的形式进行保存,这样我们还可以轻松维护工作的历史记录,只需要维护List
实例即可。在需要执行历史命令时,只需要从实例列表中取出。
示例
在本文示例中,我们将模拟使用命令模式实现一个股票交易买卖Demo。股票的操作有许多种,Demo实现股票的买和卖两种操作,使用命令模式完成,方便后续其他操作模式的拓展。
首先,我们创建一个抽象命令接口。
Order.java
package com.yeliheng.command;
/**
* 抽象命令
*/
public interface Order {
void execute();
}
在抽象命令中,具有一个execute()
方法,待会的具体命令将实现这个execute()
方法。
接着,我们需要定义一个命令的接收者,即负责处理命令的具体逻辑操作。
StockCommandReceiver.java
package com.yeliheng.command;
/**
* 命令接收者,处理具体的买卖逻辑
*/
public class StockCommandReceiver {
private final String stockCode = "123456"; //股票代码
private final int quantity = 100; //买入数量
public void buy() {
System.out.println("[买入] 股票代码: " + stockCode + " 数量(股): " + quantity);
}
public void sell() {
System.out.println("[卖出] 股票代码: " + stockCode + " 数量(股): " + quantity);
}
}
在这个类中,我们定义了两种操作的方法,分别是buy()
和sell()
。在两个方法中分别输出股票代码和股票数量。
然后我们就可以来实现具体命令了。
BuyCommand.java
package com.yeliheng.command;
/**
* 具体命令
*/
public class BuyCommand implements Order{
private StockCommandReceiver receiver;
public BuyCommand(StockCommandReceiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
receiver.buy();
}
}
BuyCommand类实现了Order订单的execute()
方法,并且管理了一个命令接收者实例,方便调用者Invoker
进行调用。
SellCommand与BuyCommand同理,具体代码如下:
SellCommand.java
package com.yeliheng.command;
/**
* 具体命令
*/
public class SellCommand implements Order{
private StockCommandReceiver receiver;
public SellCommand(StockCommandReceiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
receiver.sell();
}
}
最后,我们要实现一个命令的调用者即Invoker类,该类中含有下单和处理订单的具体逻辑。并管理了一个命令列表,方便记录历史。
Invoker.java
package com.yeliheng.command;
import java.util.ArrayList;
import java.util.List;
/**
* 调用者
*/
public class Invoker {
private List<Order> orderList = new ArrayList<>();
//下单
public void takeOrder(Order order) {
orderList.add(order);
}
//处理订单
public void handleOrders() {
for(Order order : orderList) {
order.execute();
}
orderList.clear();
}
}
在Invoker类中,我们实现了两个方法,分别是takeOrder()
和handleOrders()
。takeOrder()
方法会将所有订单加入到订单列表中。而handleOrders()
方法将遍历所有订单,负责请求具体命令,达到订单处理的效果。
完成了命令模式的基本结构后,我们在main函数中进行使用:
Main.java
package com.yeliheng.command;
public class Main {
public static void main(String[] args) {
//注册接收者
StockCommandReceiver receiver = new StockCommandReceiver();
//注册命令
BuyCommand buyCommand = new BuyCommand(receiver);
SellCommand sellCommand = new SellCommand(receiver);
//下单
Invoker invoker = new Invoker();
invoker.takeOrder(buyCommand);
invoker.takeOrder(sellCommand);
//处理订单
invoker.handleOrders();
}
}
在Main类中,我们首先实例化了一个接受者,再实例化具体命令,将具体命令交付给调用者实例进行管理,并执行具体业务逻辑。
最终程序的输出结果如下图所示:
这样就完成了一个完整命令模式的示例。
命令模式的优缺点
优点
- 命令模式通过抽象接口来降低系统的耦合度。在命令的发送与具体业务的执行上进行了分离,有利于系统的解耦和后期的拓展。当有具体命令产生时,只需要增加新的具体命令类即可,无需修改命令处理的具体代码,符合开闭原则。
- 可与多种设计模式结合使用,实现强大的事件处理系统。
注:命令也可认为是一种特殊的事件(event),合理使用命令模式能够很好地实践“事件驱动编程”的思想。
缺点
- 过度使用命令模式可能会造成具体命令类大幅增长,从而提升系统的复杂度与维护难度。
总结
本文通过股票交易系统示例进行命令模式的讲解。命令模式在Web开发中可能用得并不多,但作为一个基础的设计模式,我们也必须熟练掌握。它在底层框架上和客户端上的应用较为广泛,掌握命令模式有助于深入理解开源框架。
备忘录模式
游戏角色状态恢复问题
游戏角色有攻击力和防御力,在大战Boss前保存自身的状态(攻击力和防御力),当大战Boss后攻击力和防御力下降,从备忘录对象恢复到大战前的状态。
传统方案解决游戏角色恢复
传统的方式的问题分析
1)一个对象,就对应一个保存对象状态的对象,这样当我们游戏的对象很多时,不利于管理,开销很大。
2)传统的方式是简单的做备份,new出另外一个对象出来,再把需要备份的数据放到这个新对象,但这就暴露了对象内部的细节
备忘录模式基本介绍
1)备忘录模式(Memento Pattern)在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
2)可以这里理解备忘录模式 :现实生活中的备忘录是用来记录某些要去做的事情,或者是记录已经达成的共同意见的事情,以防忘记。而在软件层面,备忘录模式有着相同的含义,备忘录对象主要用来记录一个对象某种状态,或者某些数据,当要做回退时,可以从备忘录对象里获取原来的数据进行恢复操作。
3)备忘录模式属于行为型模式。
备忘录模式原理类图
对原理类图的说明 :
1)originator :对象(需要保存状态的对象)
2)Memento :备忘录对象,负责保存好记录,即Originator内部状态
3)Caretaker :守护着对象,负责保存多个备忘录对象,使用集合管理,提高效率
4)说明 :如果希望保存多个originator对象的不同时间的状态,也可以,只需要HashMap<String, 集合>
package com.example.demo.memento.theory;
public class Memento {
private String state;
public Memento(String state) {
super();
this.state = state;
}
public String getState() {
return state;
}
}
package com.example.demo.memento.theory;
import java.util.ArrayList;
import java.util.List;
public class Caretaker {
/**
* 在list 集合中会有很多的备忘录对象
*/
private List<Memento> mementos = new ArrayList<Memento>();
public void add(Memento memento) {
mementos.add(memento);
}
/**
* 获取到第index个Originator 的 备忘录对象(即保存状态)
* @param index
* @return
*/
public Memento get(int index) {
return mementos.get(index);
}
}
package com.example.demo.memento.theory;
public class Originator {
/**
* 状态信息
*/
private String state;
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
// 编写一个方法,可以保存一个状态对象 Memento
// 因此编写一个方法,返回Memento
public Memento saveStateMemento() {
return new Memento(state);
}
public void getStateFromMemento(Memento memento) {
state = memento.getState();
}
}
package com.example.demo.memento.theory;
public class Client {
public static void main(String[] args) {
// TODO Auto-generated method stub
Originator originator = new Originator();
Caretaker caretaker = new Caretaker();
originator.setState(" 状态1 攻击力100 ");
// 保存当前状态
caretaker.add(originator.saveStateMemento());
originator.setState(" 状态2 攻击力80 ");
// 保存当前状态
caretaker.add(originator.saveStateMemento());
originator.setState(" 状态3 攻击力50 ");
// 保存当前状态
caretaker.add(originator.saveStateMemento());
System.out.println(" 现在状态是 " + originator.getState());
// 希望得到状态1,将originator 恢复到状态1
originator.getStateFromMemento(caretaker.get(0));
System.out.println(" 恢复状态 1");
System.out.println("当前的状态是 = " + originator.getState());
}
}
游戏角色恢复状态实例
1)应用实例要求
游戏角色有攻击力和防御力,在大战Boss前保存自身的状态(攻击力和防御力),当大战Boss后攻击力和防御力下降,从备忘录对象恢复到大战前的状态
2)类图
package com.example.demo.memento.game;
public class Memento {
/**
* 攻击力
*/
private int vit;
/**
* 防御力
*/
private int def;
public Memento(int vit, int def) {
super();
this.vit = vit;
this.def = def;
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
package com.example.demo.memento.game;
import java.util.List;
import java.util.Map;
import javax.activation.MailcapCommandMap;
/**
* 守护者对象,保存游戏角色的状态
* @author zhaozhaohai
*
*/
public class Caretaker {
/**
* 如果只保存一次状态
*/
private Memento memento;
/**
* 对GameRole 保存多次状态
*/
private List<Memento> list;
/**
* 对多个游戏角色保存多个状态
*/
private Map<String, List<Memento>> rMap;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
package com.example.demo.memento.game;
public class GameRole {
private int vit;
private int def;
/**
* 创建Memento,即根据当前的状态得到Memento
* @return
*/
public Memento createMemento() {
return new Memento(vit, def);
}
/**
* 从备忘录对象,恢复GameRole的状态
* @param memento
*/
public void recoverGameRoleFromMemento(Memento memento) {
this.vit = memento.getVit();
this.def = memento.getDef();
}
/**
* 显示当前游戏角色的状态
*/
public void display() {
System.out.println("游戏角色当前的攻击力 :" + this.vit + " 防御力 : " + this.def);
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
package com.example.demo.memento.game;
public class Client {
public static void main(String[] args) {
//创建游戏角色
GameRole gameRole = new GameRole();
gameRole.setVit(100);
gameRole.setDef(100);
System.out.println("和 boss 大战前的状态");
gameRole.display();
//把当前状态保存 caretaker
Caretaker caretaker = new Caretaker();
caretaker.setMemento(gameRole.createMemento());
System.out.println("和 boss 大战~~~");
gameRole.setDef(30);
gameRole.setVit(30);
gameRole.display();
System.out.println("大战后,使用备忘录对象恢复到站前");
gameRole.recoverGameRoleFromMemento(caretaker.getMemento());
System.out.println("恢复后的状态");
gameRole.display();
}
}
备忘录模式的注意事项和细节
1)给用户提供了一种可以恢复状态的机制,可以使用户能过比较方便地回到某个历史的状态。
2)实现了信息的封装,使得用户不需要关心状态的保存细节。
3)如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存,这个需要注意。
4)使用的应用场景 :1、后悔药;2、打游戏时的存档;3、Windows里的ctri + z。4、IE中的后退。4、数据库的事务管理。
5)为了节约内存,备忘录模式可以和原型模式陪着使用。
状态模式
状态模式(State Pattern)中,类的行为是基于它的状态改变的,状态之间的切换,在状态A执行完毕后自己控制状态指向状态B,状态模式是不停的切换状态执行,这种类型的设计模式属于行为型模式。
状态模式解决的问题
解决内在状态的改变而引起行为改变的问题,它的出发点是事物的状态,封装状态而暴露行为,一个对象的状态改变,从外界来看就好像是行为改变。
态模式角色
State: 抽象状态类,定义一个接口以封装与context的一个状态相关的行为
ConcreteState: 具体状态,每一子类实现一个与Context的一个状态相关的行为
Context: 状态上下文,维护一个ConcreteState子类的实例,这个实例定义当前的状态。
状态模式抽象类方法类型
上下文抽象方法:request,上下文处理请求。
状态抽象方法:handle,状态行为方法,不同的状态,行为不同。
状态模式和策略模式的区别
状态是系统自身的固有的,调用者不能控制系统的状态转移。比如,一个请假单有“部长审批”-“经理审批”-“审批通过”-“审批不通过”等状态,请假者没有办法将一个部长都还没审批完的请假单提交给经理,这个状态转换只能系统自己完成。
策略是外界给的,策略怎么变,是调用者考虑的事情,系统只是根据所给的策略做事情。
环境角色的职责不同
两者都有一个叫做Context环境角色的类,但是两者的区别很大,策略模式的环境角色只是一个委托作用,负责算法的替换;而状态模式的环境角色不仅仅是委托行为,它还具有登记状态变化的功能,与具体的状态类协作,共同完成状态切换行为随之切换的任务。
解决问题的重点不同
策略模式旨在解决内部算法如何改变的问题,也就是将内部算法的改变对外界的影响降低到最小,它保证的是算法可以自由地切换;而状态模式旨在解决内在状态的改变而引起行为改变的问题,它的出发点是事物的状态,封装状态而暴露行为,一个对象的状态改变,从外界来看就好像是行为改变。
解决问题的方法不同
策略模式只是确保算法可以自由切换,但是什么时候用什么算法它决定不了;而状态模式对外暴露的是行为,状态的变化一般是由环境角色和具体状态共同完成的,也就是说状态模式封装了状态的变化而暴露了不同的行为或行为结果。
复杂度不同
通常策略模式比较简单,这里的简单指的是结构简单,扩展比较容易,而且代码也容易阅读。状态模式则通常比较复杂,因为它要从两个角色看到一个对象状态和行为的改变,也就是说它封装的是变化,要知道变化是无穷尽的,因此相对来说状态模式通常都比较复杂,涉及面很多,虽然也很容易扩展,但是一般不会进行大规模的扩张和修正
代码实现
销售员工报销,需要主管、经理、总监审批,审批通过之后财务打款。
/**
* 抽象状态类
*/
public abstract class State {
protected Context context;
public Context getContext() {
return context;
}
public void setContext(Context context) {
this.context = context;
}
public abstract void handle();
}
/**
* 状态上下文
*/
public class Context {
private State state;
public Context() {
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
this.state.setContext(this);
}
public void request(){
this.state.handle();
}
}
/**
* 主管审批状态
*/
public class SupervisorState extends State{
public void handle() {
System.out.println("主管审批通过,下一个经理审批");
context.setState(new ManagerState());
}
}
/**
* 经理审批状态
*/
public class ManagerState extends State{
public void handle() {
System.out.println("经理审批通过,下一个总监审批");
context.setState(new DirectorState());
}
}
/**
* 总监审批状态
*/
public class DirectorState extends State{
public void handle() {
System.out.println("总监审批通过,我是最后一个审批者");
//审核通过之后的逻辑
System.out.println("财务打款500元");
}
}
/**
* 状态模式测试类
*/
public class StateModeTest {
public static void main(String[] args) {
Context context = new Context();
context.setState(new SupervisorState());
context.request();
context.request();
context.request();
}
}
运行结果:
以上代码只是为了简单的展示状态模式,如果审批工作流是一个复杂的过程,有专门的工作流框架,这里推荐activiti。BPMN:业务流程建模与标注,包括这些图元如何组合成一个业务流程图,让业务工作流更灵活,用户可以根据自己的业务场景,定义自己特有的流程图。
优缺点
优点:封装了转换规则。枚举可能的状态,在枚举状态之前需要确定状态种类。允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
缺点:状态模式的使用必然会增加系统类和对象的个数。状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的代码。
生活中的状态模式
审批工作流,报销审批,销售发起审批,如果金额小于200,只需要主管审批,如果金额在200-500之间需要经理审批,金额大于500小于2000,需要总监审批,大于2000需要老板审批。每一个审批人审批都对应着状态的变化。
订单状态的变化,下单、待支付、支付、发货、待收货、收货、取消、待退款、已退款等等,每一个订单背后承载着很多不同的状态。每一个状态,都有不一样的业务逻辑。
我的启发
状态模式,策略模式的孪生兄弟,两者之间极其相似,但是如果我们用心去观察体会,一定能找出它们之间的不同。
访问者模式
概述
访问者模式可以说是GOF23中设计模式中最复杂的一个,但日常开发中使用频率却不高,所以说上帝喜欢简洁!增删改查虽然简单,却是大部分程序员日常主要工作,是可以混饭吃的家伙式。你技术再牛逼,企业用不到,那对于企业来说也没屌用,所以说合适的才是最好的。但不常用不等于没有用,这一点的认识到。
访问者模式试图解决如下问题: 一个类农场里面包含各种元素,例如有大雁,狗子,鸭子。而每个元素的操作却经常变换,一会让大雁排成一字,一会让大雁排成人字。当大雁排成一字的时候狗子要排成S形状,鸭子要排成B形状,当大雁排成人字时候狗子要叫两声,鸭子要跳起来...。但对农场这类有要求,第一:可以迭代这些元素,第二:里面的元素不能频繁变动,你不能一会把鸭子杀了吃了,一会又买回一匹马...,如果是这样的话就不适合使用Visitor模式
如果我们不采用设计模式,那么就要频繁的修改这些元素类,违背了开闭原则,降低代码的可维护和扩展性。
定义
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于其内部各个元素的新操作
使用场景
当你有个类,里面的包含各种类型的元素,这个类结构比较稳定,不会经常增删不同类型的元素。而需要经常给这些元素添加新的操作的时候,考虑使用此设计模式。
UML 类图
Visitor包含5个角色
- ObjectStructure
这个角色就是我们的对象结构,对应上面的大忽悠科技有限公司:BigHuYouCompany
。此对象结构包含各种元素,而且要求元素稳定且可以迭代访问这些元素。
- Visitor
大名鼎鼎的访问者,它是一个接口。里面定义了与元素对应的visite(Element)
方法,一般是有几个元素就相应的有几个visite方法。
- ConcreteVisitor
visitor的实现类
- Element
是一个接口,代表在ObjectStructure里面的元素。里面定义了一个accept(Visiotr)
的方法,通过此方法元素可以将自己交给Visitor访问。
- ConcreteElement
element 的实现类
实例
王二狗刚参加工作那会由于社会经验不足误入了一个大忽悠公司,公司老板不舍得花钱就给公司招了3个人,一个Hr,一个程序员,一个测试,但关键是老板总想追风口,啥都想做,一会社交,一会短视频。二狗多次提出说人太少,申请加几个人,至少加个保洁阿姨啊,每天都自己打扫卫生,累屁了。每到此时老板就画大饼:你现在刚毕业正是要奋斗的时候,此时不奋斗什么时候奋斗?过两年公司上市了,你作为元老就财富自由拉...balabala
这个场景就很适合使用访问者模式:
大忽悠公司结构很稳定,老板舍不得花钱招人,总共就那么3个人,还是3种角色,即只有3个元素。 大忽悠公司老板想法多,这就要求这3个人承担各种新技能,即不断的给元素增加新的算法。
第一步:构建Element
毕竟改变的是元素的算法,所以这里我们先构建元素。
社畜类只有一个accept
方法,它需要一个访问者接口类型的参数
public interface CorporateSlave {
void accept(CorporateSlaveVisitor visitor);
}
构建3个社畜的实现类:
- 程序员:
public class Programmer implements CorporateSlave {
private String name;
public Programmer(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void accept(CorporateSlaveVisitor visitor) {
visitor.visit(this);
}
}
- 测试:
public class Tester implements CorporateSlave {
private String name;
public Tester(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void accept(CorporateSlaveVisitor visitor) {
visitor.visit(this);
}
}
- 人力
同上,省略
...
注意在element类里面将自己传递给visitor的visit()
方法
@Override
public void accept(CorporateSlaveVisitor visitor) {
visitor.visit(this);
}
第二步:构建ObjectStructure
BigHuYouCompany
类里面需要包含相对稳定的元素(大忽悠老板就招这3个人,再也不肯招人),而且要求可以对这些元素迭代访问。此处我们以集合存储3位员工。
/**
* Copyright (C) 2021 ShuSheng007
* 完全享有此软件的著作权
*
* @author ShuSheng007
* @time 2021/6/7 23:01
* @description
*/
public class BigHuYouCompany {
private List<CorporateSlave> employee= new ArrayList<>();
public BigHuYouCompany() {
employee.add(new Programmer("王二狗"));
employee.add(new HumanSource("上官无需"));
employee.add(new Tester("牛翠花"));
}
public void startProject(CorporateSlaveVisitor visitor){
for (CorporateSlave slave : employee) {
slave.accept(visitor);
}
}
}
第三步:构建Visitor
Visitor 接口里面一般会存在与各元素对应的visit
方法,例如此例我们有3个角色,所以这里就有3个方法。
/**
* Copyright (C) 2021 ShuSheng007
* 完全享有此软件的著作权
*
* @author ShuSheng007
* @time 2021/6/7 21:54
* @description
*/
public interface CorporateSlaveVisitor {
void visit(Programmer programmer);
void visit(HumanSource humanSource);
void visit(Tester tester);
}
Visitor实现类
因为老板觉得社交是人类永恒的需求,所以开始想做社交App,他觉得他能成为微信第二。
这就相当于要为每一个元素定义一套新的算法,让程序员仿照微信开发设计app,让测试完成即时通信的测试,让人力发软文。
public class SocialApp implements CorporateSlaveVisitor {
@Override
public void visit(Programmer programmer) {
System.out.println(String.format("%s: 给你一个月,先仿照微信搞个类似的APP出来,要能语音能发红包,将来公司上市了少不了你的,好好干...",programmer.getName()));
}
@Override
public void visit(HumanSource humanSource) {
System.out.println(String.format("%s: 咱现在缺人,你暂时就充当了陪聊吧,在程序员开发APP期间,你去发发软文,积攒点粉丝...",humanSource.getName()));
}
@Override
public void visit(Tester tester) {
System.out.println(String.format("%s: 这是咱创业的第一炮,一定要打响,测试不能掉链子啊,不能让APP带伤上战场,以后给你多招点人,你就是领导了...",tester.getName()));
}
}
过了一段时间,老板又觉的短视频很火,又要做短视频,这就要求给每一员工增加一套新的算法。
public class LiveApp implements CorporateSlaveVisitor {
@Override
public void visit(Programmer programmer) {
System.out.println(String.format("%s: 最近小视频很火啊,咱能不能抄袭下抖音,搞他一炮,将来公司上市了,你的身价至少也是几千万,甚至上亿...",programmer.getName()));
}
@Override
public void visit(HumanSource humanSource) {
System.out.println(String.format("%s: 咱公司就数你长得靓,哪天化化妆,把你的事业线适当露一露,要是火了你在北京买房都不是梦...",humanSource.getName()));
}
@Override
public void visit(Tester tester) {
System.out.println(String.format("%s: 你也开个账户,边测试边直播,两不耽误...",tester.getName()));
}
}
再过段时间老板可能要开KTV,程序员王二狗可能要下海当鸭子,其他两位也需要解锁新技能...
客户端使用
public class VisitorClient {
public void startProject(){
BigHuYouCompany bigHuYou= new BigHuYouCompany();
//可以很轻松的更换Visitor,但是要求BigHuYouCompany的结构稳定
System.out.println("-----------------启动社交APP项目--------------------");
bigHuYou.startProject(new SocialApp());
System.out.println("-----------------启动短视频APP项目--------------------");
bigHuYou.startProject(new LiveApp());
}
}
输出:
-----------------启动社交APP项目--------------------
王二狗: 给你一个月,先仿照微信搞个类似的APP出来,要能语音能发红包,将来公司上市了少不了你的,好好干...
上官无需: 咱现在缺人,你暂时就充当了陪聊吧,在程序员开发APP期间,你去发发软文,积攒点粉丝...
牛翠花: 这是咱创业的第一炮,一定要打响,测试不能掉链子啊,不能让APP带伤上战场,以后给你多招点人,你就是领导了...
-----------------启动短视频APP项目--------------------
王二狗: 最近小视频很火啊,咱能不能抄袭下抖音,搞他一炮,将来公司上市了,你的身价至少也是几千万,甚至上亿...
上官无需: 咱公司就数你长得靓,哪天化化妆,把你的事业线适当露一露,要是火了你在北京买房都不是梦...
牛翠花: 你也开个账户,边测试边直播,两不耽误...
你看虽然大忽悠老板的需求变化这么快,但至始至终我们只是在增加新的Visitor实现类,而没有去修改任何一个Element类,这就很好的符合了开闭原则。
总结
- 准确识别出Visitor实用的场景,如果一个对象结构不稳定决不可使用,不然在增删元素时改动将非常巨大。
- 对象结构中的元素要可以迭代访问
- Visitor里一般存在与元素个数相同的visit方法。
- 元素通过
accept
方法通过this
将自己传递给了Visitor。
双分派(dispatch)
访问者模式存在一个叫"伪动态双分派”的技术,这个还是比较难懂的,访问者模式之所以是最复杂的设计模式与其有很大的关系。
什么叫分派?根据对象的类型而对方法进行的选择,就是分派(Dispatch)。
发生在编译时的分派叫静态分派,例如重载(overload),发生在运行时的分派叫动态分派,例如重写(overwrite)。
单分派与多分派
- 单分派
依据单个宗量进行方法的选择就叫单分派,Java 动态分派只根据方法的接收者一个宗量进行分配,所以其是单分派
- 多分派
依据多个宗量进行方法的选择就叫多分派,Java 静态分派要根据方法的接收者与参数这两个宗量进行分配,所以其是多分派
好了,理论的只是罗列出来了,那具体到访问者模式是个什么情况呢?
先看在BigHuYouCompany
类里的分派代码:slave.accept(visitor);
中accept方法的分派是由slave的运行时类型决定的。若slave是Programer
就执行Programer
的accept方法。若slave是Tester
那么就执行Tester
的accept方法。
public void startProject(CorporateSlaveVisitor visitor){
for (CorporateSlave slave : employee) {
slave.accept(visitor);
}
}
通过此步骤就完成了一次动态单分派。
再看一下具体的Element
里的分派代码:visitor.visit(this);
中visit方法的分派是由参数 this
的运行时类型决定的。若this是Programer
就执行Visitor
中的visit(Programer)
方法。若slave是Tester
那么就执行Visitor
的visit(Tester)
方法。
@Override
public void accept(CorporateSlaveVisitor visitor) {
visitor.visit(this);
}
通过这一步又完成了一次动态单分派。
两次动态单分派结合起来就完成了一次伪动态双分派,为什么叫伪动态装派呢?因为在Java中动态分派是单分派的,而此处是通过两次动态单分派达到了双分派的效果,所以说是伪的!
优缺点
优点
- 使得给结构稳定的对象增加新算法变得容易,提搞了代码的可维护性,可扩展性。
缺点
- 太复杂,特别是伪动态双分派,不仔细理解很难想清楚。
访问者模式使用频率较低,而且复杂,可酌情掌握
中介者模式
定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。
主要解决: 对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。
何时使用: 多个类相互耦合,形成了网状结构。
如何解决: 将上述网状结构分离为星型结构。
中介者模式的主要优点有:
- 类之间各司其职,符合迪米特法则。
- 降低了对象之间的耦合性,使得对象易于独立地被复用。
- 将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。
中介者模式的主要缺点有:
中介者模式将原本多个对象直接的相互依赖变成了中介者和多个同事类的依赖关系。当同事类越多时,中介者就会越臃肿,变得复杂且难以维护。
中介者模式的结构与实现
中介者模式实现的关键是找出“中介者”,下面对它的结构和实现进行分析。
模式的结构
中介者模式包含以下主要角色:
抽象中介者角色(Mediator)
:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。具体中介者角色(Concrete Mediator)
:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。抽象同事类角色(Colleague)
:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。具体同事类角色(Concrete Colleague
):是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
中介者模式的结构图如下图所示:
模式的实现
中介者模式的实现代码如下:
package net.biancheng.c.mediator;
import java.util.*;
public class MediatorPattern {
public static void main(String[] args) {
Mediator md = new ConcreteMediator();
Colleague c1, c2;
c1 = new ConcreteColleague1();
c2 = new ConcreteColleague2();
md.register(c1);
md.register(c2);
c1.send();
System.out.println("-------------");
c2.send();
}
}
//抽象中介者
abstract class Mediator {
public abstract void register(Colleague colleague);
public abstract void relay(Colleague cl); //转发
}
//具体中介者
class ConcreteMediator extends Mediator {
private List<Colleague> colleagues = new ArrayList<Colleague>();
public void register(Colleague colleague) {
if (!colleagues.contains(colleague)) {
colleagues.add(colleague);
colleague.setMedium(this);
}
}
public void relay(Colleague cl) {
for (Colleague ob : colleagues) {
if (!ob.equals(cl)) {
((Colleague) ob).receive();
}
}
}
}
//抽象同事类
abstract class Colleague {
protected Mediator mediator;
public void setMedium(Mediator mediator) {
this.mediator = mediator;
}
public abstract void receive();
public abstract void send();
}
//具体同事类
class ConcreteColleague1 extends Colleague {
public void receive() {
System.out.println("具体同事类1收到请求。");
}
public void send() {
System.out.println("具体同事类1发出请求。");
mediator.relay(this); //请中介者转发
}
}
//具体同事类
class ConcreteColleague2 extends Colleague {
public void receive() {
System.out.println("具体同事类2收到请求。");
}
public void send() {
System.out.println("具体同事类2发出请求。");
mediator.relay(this); //请中介者转发
}
}
程序的运行结果如下:
具体同事类1发出请求。
具体同事类2收到请求。
-------------
具体同事类2发出请求。
具体同事类1收到请求。
中介者模式的应用实例
【例1】用中介者模式编写一个“韶关房地产交流平台”程序。
说明:韶关房地产交流平台是“房地产中介公司”提供给“卖方客户”与“买方客户”进行信息交流的平台,比较适合用中介者模式来实现。
首先,定义一个中介公司(Medium)接口,它是抽象中介者,它包含了客户注册方法 register(Customer member) 和信息转发方法 relay(String from,String ad);再定义一个韶关房地产中介(EstateMedium)公司,它是具体中介者类,它包含了保存客户信息的 List 对象,并实现了中介公司中的抽象方法。
然后,定义一个客户(Customer)类,它是抽象同事类,其中包含了中介者的对象,和发送信息的 send(String ad) 方法与接收信息的 receive(String from,String ad) 方法的接口,由于本程序是窗体程序,所以本类继承 JPmme 类,并实现动作事件的处理方法 actionPerformed(ActionEvent e)。
最后,定义卖方(Seller)类和买方(Buyer)类,它们是具体同事类,是客户(Customer)类的子类,它们实现了父类中的抽象方法,通过中介者类进行信息交流,其结构图如下图所示:
程序代码如下:
package net.biancheng.c.mediator;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
public class DatingPlatform {
public static void main(String[] args) {
Medium md = new EstateMedium(); //房产中介
Customer member1, member2;
member1 = new Seller("张三(卖方)");
member2 = new Buyer("李四(买方)");
md.register(member1); //客户注册
md.register(member2);
}
}
//抽象中介者:中介公司
interface Medium {
void register(Customer member); //客户注册
void relay(String from, String ad); //转发
}
//具体中介者:房地产中介
class EstateMedium implements Medium {
private List<Customer> members = new ArrayList<Customer>();
public void register(Customer member) {
if (!members.contains(member)) {
members.add(member);
member.setMedium(this);
}
}
public void relay(String from, String ad) {
for (Customer ob : members) {
String name = ob.getName();
if (!name.equals(from)) {
((Customer) ob).receive(from, ad);
}
}
}
}
//抽象同事类:客户
abstract class Customer extends JFrame implements ActionListener {
private static final long serialVersionUID = -7219939540794786080L;
protected Medium medium;
protected String name;
JTextField SentText;
JTextArea ReceiveArea;
public Customer(String name) {
super(name);
this.name = name;
}
void ClientWindow(int x, int y) {
Container cp;
JScrollPane sp;
JPanel p1, p2;
cp = this.getContentPane();
SentText = new JTextField(18);
ReceiveArea = new JTextArea(10, 18);
ReceiveArea.setEditable(false);
p1 = new JPanel();
p1.setBorder(BorderFactory.createTitledBorder("接收内容:"));
p1.add(ReceiveArea);
sp = new JScrollPane(p1);
cp.add(sp, BorderLayout.NORTH);
p2 = new JPanel();
p2.setBorder(BorderFactory.createTitledBorder("发送内容:"));
p2.add(SentText);
cp.add(p2, BorderLayout.SOUTH);
SentText.addActionListener(this);
this.setLocation(x, y);
this.setSize(250, 330);
this.setResizable(false); //窗口大小不可调整
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
}
public void actionPerformed(ActionEvent e) {
String tempInfo = SentText.getText().trim();
SentText.setText("");
this.send(tempInfo);
}
public String getName() {
return name;
}
public void setMedium(Medium medium) {
this.medium = medium;
}
public abstract void send(String ad);
public abstract void receive(String from, String ad);
}
//具体同事类:卖方
class Seller extends Customer {
private static final long serialVersionUID = -1443076716629516027L;
public Seller(String name) {
super(name);
ClientWindow(50, 100);
}
public void send(String ad) {
ReceiveArea.append("我(卖方)说: " + ad + "\n");
//使滚动条滚动到最底端
ReceiveArea.setCaretPosition(ReceiveArea.getText().length());
medium.relay(name, ad);
}
public void receive(String from, String ad) {
ReceiveArea.append(from + "说: " + ad + "\n");
//使滚动条滚动到最底端
ReceiveArea.setCaretPosition(ReceiveArea.getText().length());
}
}
//具体同事类:买方
class Buyer extends Customer {
private static final long serialVersionUID = -474879276076308825L;
public Buyer(String name) {
super(name);
ClientWindow(350, 100);
}
public void send(String ad) {
ReceiveArea.append("我(买方)说: " + ad + "\n");
//使滚动条滚动到最底端
ReceiveArea.setCaretPosition(ReceiveArea.getText().length());
medium.relay(name, ad);
}
public void receive(String from, String ad) {
ReceiveArea.append(from + "说: " + ad + "\n");
//使滚动条滚动到最底端
ReceiveArea.setCaretPosition(ReceiveArea.getText().length());
}
}
程序的运行结果如下图所示:
中介者模式的应用场景
前面分析了中介者模式的结构与特点,下面分析其以下应用场景:
- 当对象之间存在复杂的网状结构关系而导致依赖关系混乱且难以复用时。
- 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。
中介者模式的扩展
在实际开发中,通常采用以下两种方法来简化中介者模式,使开发变得更简单:
- 不定义中介者接口,把具体中介者对象实现成为单例。
- 同事对象不持有中介者,而是在需要的时候直接获取中介者对象并调用。
下图所示是简化中介者模式的结构图:
程序代码如下:
package net.biancheng.c.mediator;
import java.util.*;
public class SimpleMediatorPattern {
public static void main(String[] args) {
SimpleColleague c1, c2;
c1 = new SimpleConcreteColleague1();
c2 = new SimpleConcreteColleague2();
c1.send();
System.out.println("-----------------");
c2.send();
}
}
//简单单例中介者
class SimpleMediator {
private static SimpleMediator smd = new SimpleMediator();
private List<SimpleColleague> colleagues = new ArrayList<SimpleColleague>();
private SimpleMediator() {
}
public static SimpleMediator getMedium() {
return (smd);
}
public void register(SimpleColleague colleague) {
if (!colleagues.contains(colleague)) {
colleagues.add(colleague);
}
}
public void relay(SimpleColleague scl) {
for (SimpleColleague ob : colleagues) {
if (!ob.equals(scl)) {
((SimpleColleague) ob).receive();
}
}
}
}
//抽象同事类
interface SimpleColleague {
void receive();
void send();
}
//具体同事类
class SimpleConcreteColleague1 implements SimpleColleague {
SimpleConcreteColleague1() {
SimpleMediator smd = SimpleMediator.getMedium();
smd.register(this);
}
public void receive() {
System.out.println("具体同事类1:收到请求。");
}
public void send() {
SimpleMediator smd = SimpleMediator.getMedium();
System.out.println("具体同事类1:发出请求...");
smd.relay(this); //请中介者转发
}
}
//具体同事类
class SimpleConcreteColleague2 implements SimpleColleague {
SimpleConcreteColleague2() {
SimpleMediator smd = SimpleMediator.getMedium();
smd.register(this);
}
public void receive() {
System.out.println("具体同事类2:收到请求。");
}
public void send() {
SimpleMediator smd = SimpleMediator.getMedium();
System.out.println("具体同事类2:发出请求...");
smd.relay(this); //请中介者转发
}
}
程序运行结果如下:
具体同事类1:发出请求...
具体同事类2:收到请求。
-----------------
具体同事类2:发出请求...
具体同事类1:收到请求。
解释器模式
四则运算问题
通过解释器模式来实现四则运算,如计算 a + b + c 的值,具体要求
1)先输入表达式的形式,比如 a + b + c + d + e,要求表达式的字母不能重复
2)在分别输入a,b,c,d,e的值
3)最后求出结果
传统方案解决四则运算问题分析
1)编写一个方法,接收表达式的形式,然后根据用户输入的数值进行解析,得到结果
2)问题分析 :如果加入新的运算符,比如 */ (等等,不利于扩展,另外让一个方法来解析会造成程序结构混乱,不够清晰。
3)解决方案:可以考虑使用解释器模式,即 :表达式 -》解释器可以有多种 -》结果
解释器模式基本介绍
1)在编译原理中,一个算术表达式通过词法分析器形成词法单元,而后这些词法单元再通过词法分析器构建语法分析树,最终形成一颗抽象的语法分析树。这里的词法分析器和语法分析器都可以看做是解释器。
2)解释器模式(Interpreter Pattern):是指给定一个语言表达式,定义它的文法的一种表示,并定义一个解释器,使用该解释器来解释语言中的句子(表达式)
3)应用场景
应用可以将一个需要解释执行的语言中的句子表示为一个抽象语法树
一些重复出现的问题可以用一种简单的语言来表达
一个简单语法需要解释的场景
4)这样的例子还有,比如编译器、运算表达式计算、正则表达式、机器人等。
1)Context :是环境角色,含有解释器之外的全局信息。
2)AbstractExpression :抽象表达式,声明一个抽象的解释操作,这个方法为抽象语法树中所有的节点所共享。
3)TerminalExpression :为终结符表达式,实现与文法中的终结符相关的解释操作。
4)NonTermialExpression :为非终结符表达式,为文法中的非终结符实现解释操作。
5)说明 :输入Context 和TerminalExpression 信息通过Client 输入即可。
解释器模式来实现四则
1)应用实例要求
通过解释器模式来实现四则运算,如计算a + b + c 的值。
package com.example.demo.interpreter;
import java.util.HashMap;
/**
* 加法解释器
* @author Administrator *
*/
public class AddExpression extends SymbolExpression{
public AddExpression(Expression left, Expression right) {
super(left, right);
}
//处理相加
//var 仍然是 {a=10,b=20}..
//一会我们 debug 源码,就 ok
public int interpreter(HashMap<String, Integer> var) {
//super.left.interpreter(var) : 返回 left 表达式对应的值 a = 10
//super.right.interpreter(var): 返回 right 表达式对应值 b = 20
return super.left.interpreter(var) + super.right.interpreter(var);
}
}
package com.example.demo.interpreter;
import java.util.HashMap;
import java.util.Stack;
public class Calculator {
// 定义表达式
private Expression expression;
// 构造函数传参,并解析
public Calculator(String expStr) { // expStr = a+b // 安排运算先后顺序
Stack<Expression> stack = new Stack<>(); // 表达式拆分成字符数组
char[] charArray = expStr.toCharArray();// [a, +, b]
Expression left = null;
Expression right = null; //遍历我们的字符数组, 即遍历 [a, +, b] //针对不同的情况,做处理
for (int i = 0; i < charArray.length; i++) {
switch (charArray[i]) { case '+': //
left = stack.pop();// 从 stack 取出 left => "a"
right = new VarExpression(String.valueOf(charArray[++i]));// 取出右表达式 "b"
stack.push(new AddExpression(left, right));// 然后根据得到 left 和 right 构建 AddExpresson 加入stack
break;
case '-': //
left = stack.pop();
right = new VarExpression(String.valueOf(charArray[++i])); stack.push(new SubExpression(left, right));
break;
default:
//如果是一个 Var 就创建要给 VarExpression 对象,并 push 到 stack
stack.push(new VarExpression(String.valueOf(charArray[i])));
break;
}
}
//当遍历完整个 charArray 数组后,stack 就得到最后 Expression this.expression = stack.pop();
}
public int run(HashMap<String, Integer> var) { // 最后将表达式 a+b 和 var = {a=10,b=20} //然后传递给 expression 的 interpreter
// 进行解释执行
return this.expression.interpreter(var);
}
}
package com.example.demo.interpreter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
public class Client {
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
// TODO Auto-generated method stub
String expStr = getExpStr(); // a+b
HashMap<String, Integer> var = getValue(expStr);// var {a=10, b=20}
Calculator calculator = new Calculator(expStr);
System.out.println("运算结果:" + expStr + "=" + calculator.run(var));
}
// 获得表达式
public static String getExpStr() throws IOException {
System.out.print("请输入表达式:");
return (new BufferedReader(new InputStreamReader(System.in))).readLine();
}
// 获得值映射
public static HashMap<String, Integer> getValue(String expStr) throws IOException {
HashMap<String, Integer> map = new HashMap<>();
for (char ch : expStr.toCharArray()) {
if (ch != '+' && ch != '-') {
if (!map.containsKey(String.valueOf(ch))) {
System.out.print("请输入" + String.valueOf(ch) + "的值:");
String in = (new BufferedReader(new InputStreamReader(System.in))).readLine();
map.put(String.valueOf(ch), Integer.valueOf(in));
}
}
}
return map;
}
}
package com.example.demo.interpreter;
import java.util.HashMap;
/**
* 抽象类表达式,通过 HashMap 键值对, 可以获取到变量的值 *
* @author Administrator
*
*/
public abstract class Expression {
// a + b - c
// 解释公式和数值, key 就是公式(表达式) 参数[a,b,c], value 就是就是具体值
// HashMap {a=10, b=20}
public abstract int interpreter(HashMap<String, Integer> var);
}
package com.example.demo.interpreter;
import java.util.HashMap;
public class SubExpression extends SymbolExpression{
public SubExpression(Expression left, Expression right) {
super(left, right);
}
//求出 left 和 right 表达式相减后的结果
public int interpreter(HashMap<String, Integer> var) {
return super.left.interpreter(var) - super.right.interpreter(var);
}
}
package com.example.demo.interpreter;
import java.util.HashMap;
/**
* 抽象运算符号解析器 这里,每个运算符号,都只和自己左右两个数字有关系,
* 但左右两个数字有可能也是一个解析的结果,无论何种类型,都是 Expression 类的实现类 *
* @author Administrator
*
*/
public class SymbolExpression extends Expression{
protected Expression left;
protected Expression right;
public SymbolExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
//因为 SymbolExpression 是让其子类来实现,因此 interpreter 是一个默认实现
@Override
public int interpreter(HashMap<String, Integer> var) {
// TODO Auto-generated method stub
return 0;
}
}
package com.example.demo.interpreter;
import java.util.HashMap;
/**
* 变量的解释器
* @author Administrator *
*/
public class VarExpression extends Expression {
private String key; // key=a,key=b,key=c
public VarExpression(String key) {
this.key = key;
}
// var 就是{a=10, b=20}
// interpreter 根据 变量名称,返回对应值 @Override
public int interpreter(HashMap<String, Integer> var) {
return var.get(this.key);
}
}
解析器模式在Spring框架中的源码分析
解释器模式的注意事项和细节
1)当有一个语言需要解释执行,可将该语言中的句子表示为一个抽象语法树,就可以考虑使用解释器模式,让程序具有良好的扩展性。
2)应用场景 :编译器、运算表达式计算、正则表达式、机器人等。
3)使用解释器可能带来的问题 :解释器模式会引起类膨胀、解释器模型采用递归调用方法,将会导致调试非常复杂、效率可能降低。