Java笔记二十二——设计模式

使用设计模式的目的是为了可重用代码,提高代码的可扩展性和可维护性,降低代码的耦合度。
设计模式基于以下几个原则:

  1. 里氏替换原则
    ——如果调用一个父类的方法可以成功,那么替换成子类调用也应该完全可以运行。
  2. 开闭原则
    ——对扩展开放,而对修改关闭
    增加新功能的时候,能不改代码就尽量不要改,如果只增加代码就完成了新功能,那是最好的。

创新型模式

创建型模式关注点是如何创建对象,其核心思想是要把对象的创建和使用相分离。
在这里插入图片描述

工厂方法

定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类。
工厂方法即Factory Method,是一种对象创建型模式。
工厂方法的目的是使得创建对象和使用对象是分离的,并且客户端总是引用抽象工厂和抽象产品:
在这里插入图片描述
假设我们希望实现一个解析字符串到Number的Factory,可以定义如下:

public interface NumberFactory {
    Number parse(String s);
}

有了工厂接口,再编写一个工厂的实现类:

public class NumberFactoryImpl implements NumberFactory {
    public Number parse(String s) {
        return new BigDecimal(s);
    }
}

产品接口是Number,NumberFactoryImpl返回的实际产品是BigDecimal。
客户端如何创建NumberFactoryImpl:通常在接口Factory中定义一个静态方法getFactory()来返回真正的子类:

public interface NumberFactory {
    // 创建方法:
    Number parse(String s);

    // 获取工厂实例:
    static NumberFactory getFactory() {
        return impl;
    }

    static NumberFactory impl = new NumberFactoryImpl();
}

在客户端中,只需要和工厂接口NumberFactory以及抽象产品Number打交道:

NumberFactory factory = NumberFactory.getFactory();
Number result = factory.parse("123.456");

调用方可以完全忽略真正的工厂NumberFactoryImpl和实际的产品BigDecimal,这样做的好处是允许创建产品的代码独立地变换,而不会影响到调用方。
一个简单的parse()需要写这么复杂的工厂吗?实际上大多数情况下我们并不需要抽象工厂,而是通过静态方法直接返回产品,即:

public class NumberFactory {
    public static Number parse(String s) {
        return new BigDecimal(s);
    }
}

简化的使用静态方法创建产品的方式称为静态工厂方法(Static Factory Method)。静态工厂方法广泛地应用在Java标准库中。例如:Integer n = Integer.valueOf(100);
Integer既是产品又是静态工厂。它提供了静态方法valueOf()来创建Integer。这种方式和直接写new Integer(100)有何区别?观察valueOf()方法:

public final class Integer {
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    ...
}

valueOf()内部可能会使用new创建一个新的Integer实例,但也可能直接返回一个缓存的Integer实例。对于调用方来说,没必要知道Integer创建的细节。工厂方法可以隐藏创建产品的细节,且不一定每次都会真正创建产品,完全可以返回缓存的产品,从而提升速度并减少内存消耗。
如果调用方直接使用Integer n = new Integer(100),那么就失去了使用缓存优化的可能性。
我们经常使用的另一个静态工厂方法是List.of():List<String> list = List.of("A", "B", "C");
里氏替换原则:返回实现接口的任意子类都可以满足该方法的要求,且不影响调用方。

抽象工厂

提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
抽象工厂模式和工厂方法不太一样,它要解决的问题比较复杂,不但工厂是抽象的,产品是抽象的,而且有多个产品需要创建,因此,这个抽象工厂会对应到多个实际工厂,每个实际工厂负责创建多个实际产品:
在这里插入图片描述
这种模式有点类似于多个供应商负责提供一系列类型的产品。
假设我们希望为用户提供一个Markdown文本转换为HTML和Word的服务,它的接口定义如下:

public interface AbstractFactory {
    // 创建Html文档:
    HtmlDocument createHtml(String md);
    // 创建Word文档:
    WordDocument createWord(String md);
}

HtmlDocument和WordDocument都比较复杂,不知如何实现,只有接口:

// Html文档接口:
public interface HtmlDocument {
    String toHtml();
    void save(Path path) throws IOException;
}

// Word文档接口:
public interface WordDocument {
    void save(Path path) throws IOException;
}

定义好了抽象工厂(AbstractFactory)以及两个抽象产品(HtmlDocument和WordDocument),让供应商来完成:FastDoc Soft和GoodDoc Soft。
FastDoc Soft的产品实现方式:FastDoc Soft必须要有实际的产品,即FastHtmlDocument和FastWordDocument:

//产品类
public class FastHtmlDocument implements HtmlDocument {
    public String toHtml() {
        ...
    }
    public void save(Path path) throws IOException {
        ...
    }
}

public class FastWordDocument implements WordDocument {
    public void save(Path path) throws IOException {
        ...
    }
}

FastDoc Soft必须提供一个实际的工厂来生产这两种产品,即FastFactory:

//抽象类ABstractFactory的实现类
public class FastFactory implements AbstractFactory {
	//抽象方法的具体实现
    public HtmlDocument createHtml(String md) {
        return new FastHtmlDocument(md);
    }
    public WordDocument createWord(String md) {
        return new FastWordDocument(md);
    }
}

使用FastDoc Soft的服务了。客户端编写代码如下:

// 创建AbstractFactory,实际类型是FastFactory:
AbstractFactory factory = new FastFactory();
// 生成Html文档:
HtmlDocument html = factory.createHtml("#Hello\nHello, world!");
html.save(Paths.get(".", "fast.html"));
// 生成Word文档:
WordDocument word = factory.createWord("#Hello\nHello, world!");
word.save(Paths.get(".", "fast.doc"));

客户端代码除了通过new创建了FastFactory或GoodFactory外,其余代码只引用了产品接口,并未引用任何实际产品(例如,FastHtmlDocument),如果把创建工厂的代码放到AbstractFactory中,就可以连实际工厂也屏蔽了:

public interface AbstractFactory {
    public static AbstractFactory createFactory(String name) {
        if (name.equalsIgnoreCase("fast")) {
            return new FastFactory();
        } else if (name.equalsIgnoreCase("good")) {
            return new GoodFactory();
        } else {
            throw new IllegalArgumentException("Invalid factory name");
        }
    }
}

在这里插入图片描述

生成器

生成器模式(Builder)是使用多个“小型”工厂来最终创建出一个完整对象。
使用Builder:创建这个对象的步骤比较多,每个步骤都需要一个零部件,最终组合成一个完整的对象。
以Markdown转HTML为例(直接编写一个完整转换器比较困难,每一行语法不一样)。
把Markdown转HTML看作一行一行的转换,每一行根据语法,使用不同的转换器:

  • 如果以#开头,使用HeadingBuilder转换;
  • 如果以>开头,使用QuoteBuilder转换;
  • 如果以—开头,使用HrBuilder转换;
  • 其余使用ParagraphBuilder转换。

HtmlBuilder写出来如下:

public class HtmlBuilder {
    private HeadingBuilder headingBuilder = new HeadingBuilder();
    private HrBuilder hrBuilder = new HrBuilder();
    private ParagraphBuilder paragraphBuilder = new ParagraphBuilder();
    private QuoteBuilder quoteBuilder = new QuoteBuilder();

    public String toHtml(String markdown) {
        StringBuilder buffer = new StringBuilder();
        markdown.lines().forEach(line -> {
            if (line.startsWith("#")) {
                buffer.append(headingBuilder.buildHeading(line)).append('\n');
            } else if (line.startsWith(">")) {
                buffer.append(quoteBuilder.buildQuote(line)).append('\n');
            } else if (line.startsWith("---")) {
                buffer.append(hrBuilder.buildHr(line)).append('\n');
            } else {
                buffer.append(paragraphBuilder.buildParagraph(line)).append('\n');
            }
        });
        return buffer.toString();
    }
}

HtmlBuilder并不是一次性把整个Markdown转换为HTML,而是一行一行转换,并且,它自己并不会将某一行转换为特定的HTML,而是根据特性把每一行都“委托”给一个XxxBuilder去转换,最后,把所有转换的结果组合起来,返回给客户端。
只需要针对每一种类型编写不同的Builder。例如,针对以#开头的行,需要HeadingBuilder:

public class HeadingBuilder {
    public String buildHeading(String line) {
        int n = 0;
        while (line.charAt(0) == '#') {
            n++;
            line = line.substring(1);
        }
        //一个#号就是<h1> </h1>
        //两个#号就是<h2> </h2>
        return String.format("<h%d>%s</h%d>", n, line.strip(), n);
    }
}

JavaMail的MimeMessage就可以看作是一个Builder模式,只不过Builder和最终产品合二为一,都是MimeMessage:

Multipart multipart = new MimeMultipart();
// 添加text:
BodyPart textpart = new MimeBodyPart();
textpart.setContent(body, "text/html;charset=utf-8");
multipart.addBodyPart(textpart);
// 添加image:
BodyPart imagepart = new MimeBodyPart();
imagepart.setFileName(fileName);
imagepart.setDataHandler(new DataHandler(new ByteArrayDataSource(input, "application/octet-stream")));
multipart.addBodyPart(imagepart);

MimeMessage message = new MimeMessage(session);
// 设置发送方地址:
message.setFrom(new InternetAddress("me@example.com"));
// 设置接收方地址:
message.setRecipient(Message.RecipientType.TO, new InternetAddress("xiaoming@somewhere.com"));
// 设置邮件主题:
message.setSubject("Hello", "UTF-8");
// 设置邮件内容为multipart:
message.setContent(multipart);

原型

原型模式,即Prototype,是指创建新对象的时候,根据现有的一个原型来创建。
例如:从旧String[ ]数组创建出一个一模一样的String[ ]数组:

// 原型:
String[] original = { "Apple", "Pear", "Banana" };
// 新对象:
String[] copy = Arrays.copyOf(original, original.length);

普通类实现原型拷贝——>实现Cloneable接口,覆写Object的clone()方法:

public class Student implements Cloneable {
    private int id;
    private String name;
    private int score;

    // 复制新对象并返回:
    public Object clone() {
        Student std = new Student();
        std.id = this.id;
        std.name = this.name;
        std.score = this.score;
        return std;
    }
}

使用的时候,因为clone()的方法签名是定义在Object中,返回类型也是Object,所以要强制转型,比较麻烦:

Student std1 = new Student();
std1.setId(123);
std1.setName("Bob");
std1.setScore(88);
// 复制新对象:强制转型成Student
Student std2 = (Student) std1.clone();
System.out.println(std1);
System.out.println(std2);
System.out.println(std1 == std2); // false,没有实现equals()方法,==判断的是内存地址值是否一致,明显不一致new了都

使用原型模式更好的方式是定义一个copy()方法,返回明确的类型:

public class Student {
    private int id;
    private String name;
    private int score;

    public Student copy() {
        Student std = new Student();
        std.id = this.id;
        std.name = this.name;
        std.score = this.score;
        return std;
    }
}

原型模式应用不是很广泛,因为很多实例会持有类似文件、Socket这样的资源,而这些资源是无法复制给另一个对象共享的,只有存储简单类型的“值”对象可以复制。

单例

单例模式(Singleton)的目的是为了保证在一个进程中,某个类有且仅有一个实例。
类只有一个实例,所以不能让调用方使用new Xyz()来创建实例。单例的构造方法必须是private,防止调用方自己创建实例,但是在类的内部,用一个静态字段来引用唯一创建的实例:

public class Singleton {
    // 静态字段引用唯一实例:静态实例名称全大写
    private static final Singleton INSTANCE = new Singleton();

    // private构造方法保证外部无法实例化:
    private Singleton() {
    }
}

外部获得唯一实例:提供一个静态方法,直接返回实例:

public class Singleton {
    // 静态字段引用唯一实例:
    private static final Singleton INSTANCE = new Singleton();

    // 通过静态方法返回实例:
    public static Singleton getInstance() {
        return INSTANCE;
    }

    // private构造方法保证外部无法实例化:
    private Singleton() {
    }
}

或者也可以把INSTANCE字段设置成public,直接暴露给外部。

另一种实现Singleton的方式是利用Java的enum,因为Java保证枚举类的每个枚举都是单例,所以我们只需要编写一个只有一个枚举的类即可:

public enum World {
    // 唯一枚举:
	INSTANCE;

	private String name = "world";

	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

枚举类也完全可以像其他类那样定义自己的字段、方法,这样上面这个World类在调用方看来就可以这么用:String name = World.INSTANCE.getName();
使用枚举实现Singleton还避免了第一种方式实现Singleton的一个潜在问题:即序列化和反序列化会绕过普通类的private构造方法从而创建出多个实例,而枚举类就没有这个问题。

实际上,很多程序,尤其是Web程序,大部分服务类都应该被视作Singleton,如果全部按Singleton的写法写,会非常麻烦,所以,通常是通过约定让框架(例如Spring)来实例化这些类,保证只有一个实例,调用方自觉通过框架获取实例而不是new操作符:

@Component // 表示一个单例组件
public class MyService {
    ...
}

除非确有必要,否则Singleton模式一般以“约定”为主,不会刻意实现它。Singleton模式既可以严格实现,也可以以约定的方式把普通类视作单例。

结构型模式

结构型模式主要涉及如何组合各种对象以便获得更好、更灵活的结构。结构型模式不仅仅简单地使用继承,而更多地通过组合与运行期的动态组合来实现更灵活的功能。
在这里插入图片描述

适配器

适配器模式是Adapter,也称Wrapper。
Task类,实现了Callable接口(多线程的内容):

public class Task implements Callable<Long> {
    private long num;
    public Task(long num) {
        this.num = num;
    }
	
	//求累加和1~num
    public Long call() throws Exception {
        long r = 0;
        for (long n = 1; n <= this.num; n++) {
            r = r + n;
        }
        System.out.println("Result: " + r);
        return r;
    }
}

通过一个线程去执行它:

Callable<Long> callable = new Task(123450000L);
Thread thread = new Thread(callable); // compile error!
thread.start();

编译不通过——Thread接收Runnable接口,但不接收Callable接口
解决方法一:改写Task类,把实现的Callable改为Runnable,不推荐。Task很可能在其他地方作为Callable被引用。
方法二:用一个Adapter,把这个Callable接口“变成”Runnable接口:

Callable<Long> callable = new Task(123450000L);
Thread thread = new Thread(new RunnableAdapter(callable));
thread.start();

RunnableAdapter类就是Adapter,它接收一个Callable,输出一个Runnable:

public class RunnableAdapter implements Runnable {
    // 引用待转换接口:
    private Callable<?> callable;

    public RunnableAdapter(Callable<?> callable) {
        this.callable = callable;
    }

    // 实现指定接口:
    public void run() {
        // 将指定接口调用委托给转换接口调用:
        try {
            callable.call();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

编写一个Adapter的步骤如下:

  • 实现目标接口,这里是Runnable;
  • 内部持有一个待转换接口的引用,这里是通过字段持有Callable接口;
  • 在目标接口的实现方法内部,调用待转换接口的方法。

这样一来,Thread就可以接收这个RunnableAdapter,因为它实现了Runnable接口。Thread作为调用方,它会调用RunnableAdapter的run()方法,在这个run()方法内部,又调用了Callable的call()方法,相当于Thread通过一层转换,间接调用了Callable的call()方法。

假设我们持有一个InputStream,希望调用readText(Reader)方法,但它的参数类型是Reader而不是InputStream,怎么办?
当然是使用适配器,把InputStream“变成”Reader:

InputStream input = Files.newInputStream(Paths.get("/path/to/file"));
Reader reader = new InputStreamReader(input, "UTF-8");
readText(reader);

InputStreamReader就是Java标准库提供的Adapter,它负责把一个InputStream适配为Reader。类似的还有OutputStreamWriter。
在这里插入图片描述

桥接

将抽象部分与它的实现部分分离,使它们都可以独立地变化。
看例子理解:假设某个汽车厂商生产三种品牌的汽车:Big、Tiny和Boss,每种品牌又可以选择燃油、纯电和混合动力。如果用传统的继承来表示各个最终车型,一共有3个抽象类加9个最终子类:
在这里插入图片描述
如果要新增一个品牌,或者加一个新的引擎(比如核动力),那么子类的数量增长更快。
所以,桥接模式就是为了避免直接继承带来的子类爆炸。

在桥接模式中,首先把Car按品牌进行子类化,但是,每个品牌选择什么发动机,不再使用子类扩充,而是通过一个抽象的“修正”类,以组合的形式引入。(抽象类+接口)我们来看看具体的实现。
首先定义抽象类Car,它引用一个Engine:

public abstract class Car {
    // 引用Engine:
    protected Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public abstract void drive();
}

Engine接口的定义如下:

public interface Engine {
    void start();
}

一个“修正”的抽象类RefinedCar中定义一些额外操作:

public abstract class RefinedCar extends Car {
    public RefinedCar(Engine engine) {
        super(engine);
    }

    public void drive() {
        this.engine.start();
        System.out.println("Drive " + getBrand() + " car...");
    }

    public abstract String getBrand();
}

最终的不同品牌继承自RefinedCar,例如BossCar:

//把Car按照品牌进行子类化
public class BossCar extends RefinedCar {
    public BossCar(Engine engine) {
        super(engine);
    }

    public String getBrand() {
        return "Boss";
    }
}

针对每一种引擎,继承自Engine,例如HybridEngine:

public class HybridEngine implements Engine {
    public void start() {
        System.out.println("Start Hybrid Engine...");
    }
}

客户端通过自己选择一个品牌,再配合一种引擎,得到最终的Car:

RefinedCar car = new BossCar(new HybridEngine());
car.drive();

使用桥接模式的好处在于,如果要增加一种引擎,只需要针对Engine派生一个新的子类,如果要增加一个品牌,只需要针对RefinedCar派生一个子类,任何RefinedCar的子类都可以和任何一种Engine自由组合,即一辆汽车的两个维度:品牌和引擎都可以独立地变化。

桥接模式实现比较复杂,实际应用也非常少,但它提供的设计思想值得借鉴,即不要过度使用继承,而是优先拆分某些部件,使用组合的方式来扩展功能。

组合

将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
Composite模式使得叶子对象和容器对象具有一致性,从而形成统一的树形结构,并用一致的方式去处理它们。

在XML或HTML中,从根节点开始,每个节点都可能包含任意个其他节点,这些层层嵌套的节点就构成了一颗树。
在这里插入图片描述
像文件夹和文件、GUI窗口的各种组件,都符合Composite模式的定义,因为它们的结构天生就是层级结构。

装饰器

装饰器(Decorator)模式,是一种在运行期动态给某个对象的实例增加功能的方法。(比生成子类更灵活)
在IO的Filter模式中已经涉及到装饰器模式了。在Java标准库中,InputStream是抽象类,FileInputStream、ServletInputStream、Socket.getInputStream()这些InputStream都是最终数据源。
如果要给不同的最终数据源增加缓冲功能、计算签名功能、加密解密功能,那么,3个最终数据源、3种功能一共需要9个子类。如果继续增加最终数据源,或者增加新功能,子类会爆炸式增长,这种设计方式显然是不可取的。
Decorator模式的目的就是把一个一个的附加功能,用Decorator的方式给一层一层地累加到原始数据源上,最终,通过组合获得我们想要的功能。
例如:给FileInputStream增加缓冲和解压缩功能,用Decorator模式写出来如下:

// 创建原始的数据源:
InputStream fis = new FileInputStream("test.gz");
// 增加缓冲功能:
InputStream bis = new BufferedInputStream(fis);
// 增加解压缩功能:
InputStream gis = new GZIPInputStream(bis);

或者一次性写成这样:

InputStream input = new GZIPInputStream( // 第二层装饰
                        new BufferedInputStream( // 第一层装饰
                            new FileInputStream("test.gz") // 核心功能
                        ));

BufferedInputStream和GZIPInputStream,它们都是从FilterInputStream继承的,这个FilterInputStream就是一个抽象的Decorator。

图解Decoratro模式:
在这里插入图片描述
假设我们需要渲染一个HTML的文本,但是文本还可以附加一些效果,比如加粗、变斜体、加下划线等。为了实现动态附加效果,可以采用Decorator模式。
定义顶层接口TextNode:

public interface TextNode {
    // 设置text:
    void setText(String text);
    // 获取text:
    String getText();
}

对于核心节点,例如,从TextNode直接继承:

public class SpanNode implements TextNode {
    private String text;

    public void setText(String text) {
        this.text = text;
    }

    public String getText() {
        return "<span>" + text + "</span>";
    }
}

抽象的Decorator类用来实现Decorator模式:

public abstract class NodeDecorator implements TextNode {
    protected final TextNode target;

    protected NodeDecorator(TextNode target) {
        this.target = target;
    }

    public void setText(String text) {
        this.target.setText(text);
    }
}

NodeDecorator类的核心是持有一个TextNode,即将要把功能附加到的TextNode实例。接下来就可以写一个加粗功能:

public class BoldDecorator extends NodeDecorator {
    public BoldDecorator(TextNode target) {
    	//调用父类构造方法
        super(target);
    }

    public String getText() {
        return "<b>" + target.getText() + "</b>";
    }
}

类似的,可以继续加ItalicDecorator、UnderlineDecorator等。客户端可以自由组合这些Decorator:

TextNode n1 = new SpanNode();
TextNode n2 = new BoldDecorator(new UnderlineDecorator(new SpanNode()));
TextNode n3 = new ItalicDecorator(new BoldDecorator(new SpanNode()));
n1.setText("Hello");
n2.setText("Decorated");
n3.setText("World");
System.out.println(n1.getText());
// 输出<span>Hello</span>

System.out.println(n2.getText());
// 输出<b><u><span>Decorated</span></u></b>

System.out.println(n3.getText());
// 输出<i><b><span>World</span></b></i>

使用Decorator模式,可以独立增加核心功能,也可以独立增加附加功能,二者互不影响;
可以在运行期动态地给核心功能增加任意个附加功能。

外观

外观模式,即Facade,基本思想:
如果客户端要跟许多子系统打交道,那么客户端需要了解各个子系统的接口,比较麻烦。如果有一个统一的“中介”,让客户端只跟中介打交道,中介再去跟各个子系统打交道,对客户端来说就比较简单。所以Facade就相当于搞了一个中介。
在这里插入图片描述

// 工商注册:
public class AdminOfIndustry {
    public Company register(String name) {
        ...
    }
}

// 银行开户:
public class Bank {
    public String openAccount(String companyId) {
        ...
    }
}

// 纳税登记:
public class Taxation {
    public String applyTaxCode(String companyId) {
        ...
    }
}

子系统比较复杂,并且客户对流程也不熟悉,那就把这些流程全部委托给中介:

public class Facade {
    public Company openCompany(String name) {
        Company c = this.admin.register(name);
        String bankAccount = this.bank.openAccount(c.getId());
        c.setBankAccount(bankAccount);
        String taxCode = this.taxation.applyTaxCode(c.getId());
        c.setTaxCode(taxCode);
        return c;
    }
}

这样,客户端只跟Facade打交道,一次完成公司注册的所有繁琐流程:Company c = facade.openCompany("Facade Software Ltd.");

很多Web程序,内部有多个子系统提供服务,经常使用一个统一的Facade入口,例如一个RestApiController,使得外部用户调用的时候,只关心Facade提供的接口,不用管内部到底是哪个子系统处理的。
更复杂的Web程序,会有多个Web服务,这个时候,经常会使用一个统一的网关入口来自动转发到不同的Web服务,这种提供统一入口的网关就是Gateway,它本质上也是一个Facade,但可以附加一些用户认证、限流限速的额外服务。

小结
Facade模式是为了给客户端提供一个统一入口,并对外屏蔽内部子系统的调用细节。

享元

享元(Flyweight)的核心思想很简单:如果一个对象实例一经创建就不可变,那么反复创建相同的实例就没有必要,直接向调用方返回一个共享的实例就行,这样即节省内存,又可以减少创建对象的过程,提高运行速度。
享元模式在Java标准库中有很多应用。包装类型如Byte、Integer都是不变类,因此,反复创建同一个值相同的包装类型是没有必要的。以Integer为例,如果我们通过Integer.valueOf()这个静态工厂方法创建Integer实例,当传入的int范围在-128~+127之间时,会直接返回缓存的Integer实例:

//享元模式
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Integer n1 = Integer.valueOf(100);
        Integer n2 = Integer.valueOf(100);
        System.out.println(n1 == n2); // true
    }
}

对于Byte来说,因为它一共只有256个状态,所以,通过Byte.valueOf()创建的Byte实例,全部都是缓存对象。
享元模式就是通过工厂方法创建对象,在工厂方法内部,很可能返回缓存的实例,而不是新创建实例,从而实现不可变实例的复用。
使用工厂方法而不是new操作符创建实例,可获得享元模式的好处。

在实际应用中,享元模式主要应用于缓存,即客户端如果重复请求某些对象,不必每次查询数据库或者读取文件,而是直接返回内存中缓存的数据。
以Student为例,设计一个静态工厂方法,它在内部可以返回缓存的对象:

public class Student {
    // 持有缓存:
    private static final Map<String, Student> cache = new HashMap<>();

    // 静态工厂方法:
    public static Student create(int id, String name) {
        String key = id + "\n" + name;
        // 先查找缓存:
        Student std = cache.get(key);
        if (std == null) {
            // 未找到,创建新对象:
            System.out.println(String.format("create new Student(%s, %s)", id, name));
            std = new Student(id, name);
            // 放入缓存:
            cache.put(key, std);
        } else {
            // 缓存中存在:
            System.out.println(String.format("return cached Student(%s, %s)", std.id, std.name));
        }
        return std;
    }

    private final int id;
    private final String name;

    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

在实际应用中,我们经常使用成熟的缓存库,例如Guava的Cache,因为它提供了最大缓存数量限制、定时过期等实用功能。

代理

代理模式,即Proxy,它和Adapter模式很类似。Adapter模式,用于把A接口转换为B接口。代理模式,即Proxy,它和Adapter模式很类似。我们先回顾Adapter模式,它用于把A接口转换为B接口:

public AProxy implements A {
    private A a;
    public AProxy(A a) {
        this.a = a;
    }
	public void a() {
	//实现了权限检查,只有符合要求的用户,才会真正调用目标方法,否则,会直接抛出异常。
    	if (getCurrentUser().isRoot()) {
        	this.a.a();
    	} else {
        	throw new SecurityException("Forbidden");
    	}
	}
}

用Proxy实现这个权限检查,我们可以获得更清晰、更简洁的代码:

  • A接口:只定义接口;
  • ABusiness类:只实现A接口的业务逻辑;
  • APermissionProxy类:只实现A接口的权限检查代理。

如果我们希望编写其他类型的代理,可以继续增加类似ALogProxy,而不必对现有的A接口、ABusiness类进行修改。
实际上权限检查只是代理模式的一种应用。Proxy还广泛应用在:

远程代理

远程代理即Remote Proxy,本地的调用者持有的接口实际上是一个代理,这个代理负责把对接口的方法访问转换成远程调用,然后返回结果。Java内置的RMI机制就是一个完整的远程代理模式。

虚代理

虚代理即Virtual Proxy,它让调用者先持有一个代理对象,但真正的对象尚未创建。如果没有必要,这个真正的对象是不会被创建的,直到客户端需要真的必须调用时,才创建真正的对象。JDBC的连接池返回的JDBC连接(Connection对象)就可以是一个虚代理,即获取连接时根本没有任何实际的数据库连接,直到第一次执行JDBC查询或更新操作时,才真正创建实际的JDBC连接。

保护代理

保护代理即Protection Proxy,它用代理对象控制对原始对象的访问,常用于鉴权。

智能引用

智能引用即Smart Reference,它也是一种代理对象,如果有很多客户端对它进行访问,通过内部的计数器可以在外部调用者都不使用后自动释放它。

除了第一次打开了一个真正的JDBC Connection,后续获取的Connection实际上是同一个JDBC Connection。但是,对于调用方来说,完全不需要知道底层做了哪些优化。
我们实际使用的DataSource,例如HikariCP,都是基于代理模式实现的,原理同上,但增加了更多的如动态伸缩的功能(一个连接空闲一段时间后自动关闭)。

有的童鞋会发现Proxy模式和Decorator模式有些类似。确实,这两者看起来很像,但区别在于:Decorator模式让调用者自己创建核心类,然后组合各种功能,而Proxy模式决不能让调用者自己创建再组合,否则就失去了代理的功能。Proxy模式让调用者认为获取到的是核心类接口,但实际上是代理类。
在这里插入图片描述

行为型模式

行为型模式主要涉及算法和对象间的职责分配。
在这里插入图片描述

责任链

责任链模式(Chain of Responsibility)是一种处理请求的模式,它让多个处理器都有机会处理该请求,直到其中某个处理成功为止。责任链模式把多个处理器串成链,然后让请求在链上传递:
在这里插入图片描述
在实际场景中,财务审批就是一个责任链模式。假设某个员工需要报销一笔费用,审核者可以分为:
Manager:只能审核1000元以下的报销;
Director:只能审核10000元以下的报销;
CEO:可以审核任意额度。
用责任链模式设计此报销流程时,每个审核者只关心自己责任范围内的请求,并且处理它。对于超出自己责任范围的,扔给下一个审核者处理,这样,将来继续添加审核者的时候,不用改动现有逻辑。

实现责任链模式,首先抽象出请求对象,它在责任链上传递:

public class Request {
    private String name;
    private BigDecimal amount;

    public Request(String name, BigDecimal amount) {
        this.name = name;
        this.amount = amount;
    }

    public String getName() {
        return name;
    }

    public BigDecimal getAmount() {
        return amount;
    }
}

然后抽象出处理器:

public interface Handler {
    // 返回Boolean.TRUE = 成功
    // 返回Boolean.FALSE = 拒绝
    // 返回null = 交下一个Hander处理
	Boolean process(Request request);
}

然后,依次编写ManagerHandler、DirectorHandler和CEOHandler。以ManagerHandler为例:

public class ManagerHandler implements Handler {
    public Boolean process(Request request) {
        // 如果超过1000元,处理不了,交下一个处理:
        if (request.getAmount().compareTo(BigDecimal.valueOf(1000)) > 0) {
            return null;
        }
        // 对Bob有偏见:
        return !request.getName().equalsIgnoreCase("bob");
    }
}

有了不同的Handler后,再把这些Handler组合起来,变成一个链,并通过一个统一入口处理:

public class HandlerChain {
    // 持有所有Handler:
    private List<Handler> handlers = new ArrayList<>();

    public void addHandler(Handler handler) {
        this.handlers.add(handler);
    }

    public boolean process(Request request) {
        // 依次调用每个Handler:
        for (Handler handler : handlers) {
            Boolean r = handler.process(request);
            if (r != null) {
                // 如果返回TRUE或FALSE,处理结束:true的话Arrpoved  false的话Denied
                System.out.println(request + " " + (r ? "Approved by " : "Denied by ") + handler.getClass().getSimpleName());
                return r;
            }
        }
        throw new RuntimeException("Could not handle request: " + request);
    }
}

在客户端组装出责任链,然后用责任链来处理请求:

// 构造责任链:
HandlerChain chain = new HandlerChain();
chain.addHandler(new ManagerHandler());
chain.addHandler(new DirectorHandler());
chain.addHandler(new CEOHandler());
// 处理请求:
chain.process(new Request("Bob", new BigDecimal("123.45")));
chain.process(new Request("Alice", new BigDecimal("1234.56")));
chain.process(new Request("Bill", new BigDecimal("12345.67")));
chain.process(new Request("John", new BigDecimal("123456.78")));

责任链模式有很多变种。有些责任链的实现方式是通过某个Handler手动调用下一个Handler来传递Request,例如:

public class AHandler implements Handler {
    private Handler next;
    public void process(Request request) {
        if (!canProcess(request)) {
            // 手动交给下一个Handler处理:
            next.process(request);
        } else {
            ...
        }
    }
}

还有一些责任链模式,每个Handler都有机会处理Request,通常这种责任链被称为拦截器(Interceptor)或者过滤器(Filter),它的目的不是找到某个Handler处理掉Request,而是每个Handler都做一些工作,比如:
记录日志;检查权限;准备相关资源;……
例如,JavaEE的Servlet规范定义的Filter就是一种责任链模式,它不但允许每个Filter都有机会处理请求,还允许每个Filter决定是否将请求“放行”给下一个Filter:

public class AuditFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        log(req);
        if (check(req)) {
            // 放行:
            chain.doFilter(req, resp);
        } else {
            // 拒绝:
            sendError(resp);
        }
    }
}

这种模式不但允许一个Filter自行决定处理ServletRequest和ServletResponse,还可以“伪造”ServletRequest和ServletResponse以便让下一个Filter处理,能实现非常复杂的功能。

命令

命令模式(Command)是指,把请求封装成一个命令,然后执行该命令。
以一个编辑器为例子,看看如何实现简单的编辑操作:

public class TextEditor {
	//用一个StringBuilder模拟一个文本编辑器
    private StringBuilder buffer = new StringBuilder();

    public void copy() {
        ...
    }

    public void paste() {
        String text = getFromClipBoard();
        add(text);
    }

    public void add(String s) {
        buffer.append(s);
    }

    public void delete() {
        if (buffer.length() > 0) {
            buffer.deleteCharAt(buffer.length() - 1);
        }
    }

    public String getState() {
        return buffer.toString();
    }
}

正常情况下调用TextEditor:

TextEditor editor = new TextEditor();
editor.add("Command pattern in text editor.\n");
editor.copy();
editor.paste();
System.out.println(editor.getState());

直接调用方法,调用方需要了解TextEditor的所有接口信息。如果改用命令模式,我们就要把调用方发送命令和执行方执行命令分开。解决方案是引入一个Command接口:

public interface Command {
    void execute();
}

调用方创建一个对应的Command,然后执行,并不关心内部是如何具体执行的。
为了支持CopyCommand和PasteCommand这两个命令,我们从Command接口派生:

public class CopyCommand implements Command {
    // 持有执行者对象:
    private TextEditor receiver;

    public CopyCommand(TextEditor receiver) {
        this.receiver = receiver;
    }

    public void execute() {
        receiver.copy();
    }
}

public class PasteCommand implements Command {
    private TextEditor receiver;

    public PasteCommand(TextEditor receiver) {
        this.receiver = receiver;
    }

    public void execute() {
        receiver.paste();
    }
}

把Command和TextEditor组装一下,客户端这么写:

TextEditor editor = new TextEditor();
editor.add("Command pattern in text editor.\n");
// 执行一个CopyCommand:
Command copy = new CopyCommand(editor);
copy.execute();
editor.add("----\n");
// 执行一个PasteCommand:
Command paste = new PasteCommand(editor);
paste.execute();
System.out.println(editor.getState());

在这里插入图片描述
使用命令模式,确实增加了系统的复杂度。如果需求很简单,那么直接调用显然更直观而且更简单。如果TextEditor复杂到一定程度,并且需要支持Undo、Redo的功能时,就需要使用命令模式,因为我们可以给每个命令增加undo():

public interface Command {
    void execute();
    void undo();
}

把执行的一系列命令用List保存起来,就既能支持Undo,又能支持Redo。这个时候,我们又需要一个Invoker对象,负责执行命令并保存历史命令:
在这里插入图片描述
模式带来的设计复杂度的增加是随着需求而增加的,它减少的是系统各组件的耦合度。

解释器

类比编译原理的词法分析器
解释器模式(Interpreter)是一种针对特定问题设计的一种解决方案。例如,匹配字符串的时候,由于匹配条件非常灵活,使得通过代码来实现非常不灵活。因此,需要一种通用的表示方法——正则表达式来进行匹配。正则表达式就是一个字符串,但要把正则表达式解析为语法树,然后再匹配指定的字符串,就需要一个解释器。实现一个完整的正则表达式的解释器非常复杂,但是使用解释器模式却很简单:

String s = "+861012345678";
System.out.println(s.matches("^\\+\\d+$"));

类似的,当我们使用JDBC时,执行的SQL语句虽然是字符串,但最终需要数据库服务器的SQL解释器来把SQL“翻译”成数据库服务器能执行的代码,这个执行引擎也非常复杂,但对于使用者来说,仅仅需要写出SQL字符串即可。

迭代器

迭代器模式(Iterator)实际上在Java的集合类中已经广泛使用了。我们以List为例,要遍历ArrayList,即使我们知道它的内部存储了一个Object[]数组,也不应该直接使用数组索引去遍历,因为这样需要了解集合内部的存储结构。如果使用Iterator遍历,那么,ArrayList和LinkedList都可以以一种统一的接口来遍历:

List<String> list = ...
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
    String s = it.next();
}

实际上,因为Iterator模式十分有用,因此,Java允许我们直接把任何支持Iterator的集合对象用foreach循环写出来:

List<String> list = ...
for (String s : list) {

}

如何实现一个Iterator模式?以一个自定义的集合为例,通过Iterator模式实现倒序遍历:

public class ReverseArrayCollection<T> implements Iterable<T> {
    // 以数组形式持有集合:
    private T[] array;

    public ReverseArrayCollection(T... objs) {
        this.array = Arrays.copyOfRange(objs, 0, objs.length);
    }

    public Iterator<T> iterator() {
        return ???;
    }
}

实现Iterator模式的关键是返回一个Iterator对象,该对象知道集合的内部结构,因为它可以实现倒序遍历。使用Java的内部类实现这个Iterator:

public class ReverseArrayCollection<T> implements Iterable<T> {
    private T[] array;

    public ReverseArrayCollection(T... objs) {
        this.array = Arrays.copyOfRange(objs, 0, objs.length);
    }

    public Iterator<T> iterator() {
        return new ReverseIterator();
    }

    class ReverseIterator implements Iterator<T> {
        // 索引位置:
        int index;

        public ReverseIterator() {
            // 创建Iterator时,索引在数组末尾:
            this.index = ReverseArrayCollection.this.array.length;
        }

        public boolean hasNext() {
            // 如果索引大于0,那么可以移动到下一个元素(倒序往前移动):
            return index > 0;
        }

        public T next() {
            // 将索引移动到下一个元素并返回(倒序往前移动):
            index--;
            return array[index];
        }
    }
}

使用内部类的好处是内部类隐含地持有一个它所在对象的this引用,可以通过ReverseArrayCollection.this引用到它所在的集合。上述代码实现的逻辑非常简单,但是实际应用时,如果考虑到多线程访问,当一个线程正在迭代某个集合,而另一个线程修改了集合的内容时,是否能继续安全地迭代,还是抛出ConcurrentModificationException,就需要更仔细地设计。
在这里插入图片描述

中介

中介模式(Mediator)又称调停者模式,它的目的是把多方会谈变成双方会谈,从而实现多方的松耦合。看例子
在这里插入图片描述
它的复杂性在于,当多选框变化时,它会影响“选择全部”和“取消所有”按钮的状态(是否可点击),当用户点击某个按钮时,例如“反选”,除了会影响多选框的状态,它又可能影响“选择全部”和“取消所有”按钮的状态。
这是一个多方会谈,逻辑写起来很复杂:
在这里插入图片描述
引入一个中介,把多方会谈变成多个双方会谈,虽然多了一个对象,但对象之间的关系就变简单了:
在这里插入图片描述
用中介模式来实现各个UI组件的交互。首先把UI组件给画出来:

public class Main {
    public static void main(String[] args) {
        new OrderFrame("Hanburger", "Nugget", "Chip", "Coffee");
    }
}

class OrderFrame extends JFrame {
    public OrderFrame(String... names) {
        setTitle("Order");
        setSize(460, 200);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Container c = getContentPane();
        c.setLayout(new FlowLayout(FlowLayout.LEADING, 20, 20));
        c.add(new JLabel("Use Mediator Pattern"));
        List<JCheckBox> checkboxList = addCheckBox(names);
        JButton selectAll = addButton("Select All");
        JButton selectNone = addButton("Select None");
        selectNone.setEnabled(false);
        JButton selectInverse = addButton("Inverse Select");
        new Mediator(checkBoxList, selectAll, selectNone, selectInverse);
        setVisible(true);
    }

    private List<JCheckBox> addCheckBox(String... names) {
        JPanel panel = new JPanel();
        panel.add(new JLabel("Menu:"));
        List<JCheckBox> list = new ArrayList<>();
        for (String name : names) {
            JCheckBox checkbox = new JCheckBox(name);
            list.add(checkbox);
            panel.add(checkbox);
        }
        getContentPane().add(panel);
        return list;
    }

    private JButton addButton(String label) {
        JButton button = new JButton(label);
        getContentPane().add(button);
        return button;
    }
}

设计一个Mediator类,它引用4个UI组件,并负责跟它们交互:

public class Mediator {
    // 引用UI组件:
    private List<JCheckBox> checkBoxList;
    private JButton selectAll;
    private JButton selectNone;
    private JButton selectInverse;

    public Mediator(List<JCheckBox> checkBoxList, JButton selectAll, JButton selectNone, JButton selectInverse) {
        this.checkBoxList = checkBoxList;
        this.selectAll = selectAll;
        this.selectNone = selectNone;
        this.selectInverse = selectInverse;
        // 绑定事件:
        this.checkBoxList.forEach(checkBox -> {
            checkBox.addChangeListener(this::onCheckBoxChanged);
        });
        this.selectAll.addActionListener(this::onSelectAllClicked);
        this.selectNone.addActionListener(this::onSelectNoneClicked);
        this.selectInverse.addActionListener(this::onSelectInverseClicked);
    }

    // 当checkbox有变化时:
    public void onCheckBoxChanged(ChangeEvent event) {
        boolean allChecked = true;
        boolean allUnchecked = true;
        for (var checkBox : checkBoxList) {
            if (checkBox.isSelected()) {
                allUnchecked = false;
            } else {
                allChecked = false;
            }
        }
        selectAll.setEnabled(!allChecked);
        selectNone.setEnabled(!allUnchecked);
    }

    // 当点击select all:
    public void onSelectAllClicked(ActionEvent event) {
        checkBoxList.forEach(checkBox -> checkBox.setSelected(true));
        selectAll.setEnabled(false);
        selectNone.setEnabled(true);
    }

    // 当点击select none:
    public void onSelectNoneClicked(ActionEvent event) {
        checkBoxList.forEach(checkBox -> checkBox.setSelected(false));
        selectAll.setEnabled(true);
        selectNone.setEnabled(false);
    }

    // 当点击select inverse:
    public void onSelectInverseClicked(ActionEvent event) {
        checkBoxList.forEach(checkBox -> checkBox.setSelected(!checkBox.isSelected()));
        onCheckBoxChanged(null);
    }
}

在这里插入图片描述
使用Mediator模式后,我们得到了以下好处:
1.各个UI组件互不引用,这样就减少了组件之间的耦合关系;
2.Mediator用于当一个组件发生状态变化时,根据当前所有组件的状态决定更新某些组件;
3.如果新增一个UI组件,我们只需要修改Mediator更新状态的逻辑,现有的其他UI组件代码不变。
Mediator模式经常用在有众多交互组件的UI上。为了简化UI程序,MVC模式以及MVVM模式都可以看作是Mediator模式的扩展。

备忘录

类比计算机组成原理函数调用保存堆栈状态
备忘录模式(Memento),主要用于捕获一个对象的内部状态,以便在将来的某个时候恢复此状态。
我们使用的几乎所有软件都用到了备忘录模式。最简单的备忘录模式就是保存到文件,打开文件。对于文本编辑器来说,保存就是把TextEditor类的字符串存储到文件,打开就是恢复TextEditor类的状态。对于图像编辑器来说,原理是一样的,只是保存和恢复的数据格式比较复杂而已。Java的序列化也可以看作是备忘录模式。
在使用文本编辑器的时候,我们还经常使用Undo、Redo这些功能。这些其实也可以用备忘录模式实现,即不定期地把TextEditor类的字符串复制一份存起来,这样就可以Undo或Redo。
在这里插入图片描述

观察者

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
观察者模式(Observer)又称发布-订阅模式(Publish-Subscribe:Pub/Sub)。它是一种通知机制,让发送通知的一方(被观察方)和接收通知的一方(观察者)能彼此分离,互不影响。
看例子理解观察者模式:
假设一个电商网站,有多种Product(商品),同时,Customer(消费者)和Admin(管理员)对商品上架、价格改变都感兴趣,希望能第一时间获得通知。于是,Store(商场)可以这么写:

public class Store {
    Customer customer;
    Admin admin;

    private Map<String, Product> products = new HashMap<>();

    public void addNewProduct(String name, double price) {
        Product p = new Product(name, price);
        products.put(p.getName(), p);
        // 通知用户:
        customer.onPublished(p);
        // 通知管理员:
        admin.onPublished(p);
    }

    public void setProductPrice(String name, double price) {
        Product p = products.get(name);
        p.setPrice(price);
        // 通知用户:
        customer.onPriceChanged(p);
        // 通知管理员:
        admin.onPriceChanged(p);
    }
}

上述Store类的问题:它直接引用了Customer和Admin。先不考虑多个Customer或多个Admin的问题,上述Store类最大的问题是,如果要加一个新的观察者类型,例如工商局管理员,Store类就必须继续改动。

上述问题的本质是Store希望发送通知给那些关心Product的对象,但Store并不想知道这些人是谁。观察者模式就是要分离被观察者和观察者之间的耦合关系。实现方式:Store不能直接引用Customer和Admin,相反,它引用一个ProductObserver接口,任何人想要观察Store,只要实现该接口,并且把自己注册到Store即可:

public class Store {
    private List<ProductObserver> observers = new ArrayList<>();
    private Map<String, Product> products = new HashMap<>();

    // 注册观察者:
    public void addObserver(ProductObserver observer) {
        this.observers.add(observer);
    }

    // 取消注册:
    public void removeObserver(ProductObserver observer) {
        this.observers.remove(observer);
    }

    public void addNewProduct(String name, double price) {
        Product p = new Product(name, price);
        products.put(p.getName(), p);
        // 通知观察者:
        observers.forEach(o -> o.onPublished(p));
    }

    public void setProductPrice(String name, double price) {
        Product p = products.get(name);
        p.setPrice(price);
        // 通知观察者:
        observers.forEach(o -> o.onPriceChanged(p));
    }
}

小小的改动,使得观察者类型就可以无限扩充,而且,观察者的定义可以放到客户端:

// observer:
Admin a = new Admin();
Customer c = new Customer();
// store:
Store store = new Store();
// 注册观察者:
store.addObserver(a);
store.addObserver(c);

甚至可以注册匿名观察者:

store.addObserver(new ProductObserver() {
    public void onPublished(Product product) {
        System.out.println("[Log] on product published: " + product);
    }

    public void onPriceChanged(Product product) {
        System.out.println("[Log] on product price changed: " + product);
    }
});

在这里插入图片描述
广义的观察者模式包括所有消息系统。所谓消息系统,就是把观察者和被观察者完全分离,通过消息系统本身来通知
在这里插入图片描述
消息发送方称为Producer,消息接收方称为Consumer,Producer发送消息的时候,必须选择发送到哪个Topic。Consumer可以订阅自己感兴趣的Topic,从而只获得特定类型的消息。

使用消息系统实现观察者模式时,Producer和Consumer甚至经常不在同一台机器上,并且双方对对方完全一无所知,因为注册观察者这个动作本身都在消息系统中完成,而不是在Producer内部完成。

状态

允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
状态模式(State)经常用在带有状态的对象中。
在这里插入图片描述
定义一个enum就可以表示不同的状态。但不同的状态需要对应不同的行为,比如收到消息时:

if (state == ONLINE) {
    // 闪烁图标
} else if (state == BUSY) {
    reply("现在忙,稍后回复");
} else if ...

状态模式的目的是为了把上述一大串if…else…的逻辑给分拆到不同的状态类中,使得将来增加状态比较容易。
例如,我们设计一个聊天机器人,它有两个状态:未连线;已连线。对于未连线状态,我们收到消息也不回复:

public class DisconnectedState implements State {
    public String init() {
        return "Bye!";
    }

    public String reply(String input) {
        return "";
    }
}

对于已连线状态,我们回应收到的消息:

public class ConnectedState implements State {
    public String init() {
        return "Hello, I'm Bob.";
    }

    public String reply(String input) {
        if (input.endsWith("?")) {
            return "Yes. " + input.substring(0, input.length() - 1) + "!";
        }
        if (input.endsWith(".")) {
            return input.substring(0, input.length() - 1) + "!";
        }
        return input.substring(0, input.length() - 1) + "?";
    }
}

状态模式的关键设计思想在于状态切换,引入一个BotContext完成状态切换:

public class BotContext {
	private State state = new DisconnectedState();

	public String chat(String input) {
		if ("hello".equalsIgnoreCase(input)) {
            // 收到hello切换到在线状态:
			state = new ConnectedState();
			return state.init();
		} else if ("bye".equalsIgnoreCase(input)) {
            /  收到bye切换到离线状态:
			state = new DisconnectedState();
			return state.init();
		}
		return state.reply(input);
	}
}

一个价值千万的AI聊天机器人就诞生了:

Scanner scanner = new Scanner(System.in);
BotContext bot = new BotContext();
for (;;) {
    System.out.print("> ");
    String input = scanner.nextLine();
    String output = bot.chat(input);
    System.out.println(output.isEmpty() ? "(no reply)" : "< " + output);
}

在这里插入图片描述
在这里插入图片描述

策略

策略模式:Strategy,是指,定义一组算法,并把其封装到一个对象中。然后在运行时,可以灵活的使用其中的一个算法。
策略模式在Java标准库中应用非常广泛,以排序为例,看看如何通过Arrays.sort()实现忽略大小写排序:

import java.util.Arrays;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        String[] array = { "apple", "Pear", "Banana", "orange" };
        Arrays.sort(array, String::compareToIgnoreCase);
        System.out.println(Arrays.toString(array));
    }
}

想忽略大小写排序,就传入String::compareToIgnoreCase,如果我们想倒序排序,就传入(s1, s2) -> -s1.compareTo(s2),这个比较两个元素大小的算法就是策略。
们观察Arrays.sort(T[] a, Comparator<? super T> c)这个排序方法,它在内部实现了TimSort排序,但是,排序算法在比较两个元素大小的时候,需要借助我们传入的Comparator对象,才能完成比较。因此,这里的策略是指比较两个元素大小的策略,可以是忽略大小写比较,可以是倒序比较,也可以根据字符串长度比较。

上述排序使用到了策略模式,它实际上指,在一个方法中,流程是确定的,但是,某些关键步骤的算法依赖调用方传入的策略,这样,传入不同的策略,即可获得不同的结果,大大增强了系统的灵活性。

一个完整的策略模式要定义策略以及使用策略的上下文。我们以购物车结算为例,假设网站针对普通会员、Prime会员有不同的折扣,同时活动期间还有一个满100减20的活动,这些就可以作为策略实现。先定义打折策略接口:

public interface DiscountStrategy {
    // 计算折扣额度:
    //BigDecimal大小数类,用于高精度计算
    BigDecimal getDiscount(BigDecimal total);
}

实现各种策略。普通用户策略如下:

public class UserDiscountStrategy implements DiscountStrategy {
    public BigDecimal getDiscount(BigDecimal total) {
        // 普通会员打九折:
        return total.multiply(new BigDecimal("0.1")).setScale(2, RoundingMode.DOWN);
    }
}

满减策略如下:

public class OverDiscountStrategy implements DiscountStrategy {
    public BigDecimal getDiscount(BigDecimal total) {
        // 满100减20优惠:
        return total.compareTo(BigDecimal.valueOf(100)) >= 0 ? BigDecimal.valueOf(20) : BigDecimal.ZERO;
    }
}

要应用策略,我们需要一个DiscountContext:

public class DiscountContext {
    // 持有某个策略:
    private DiscountStrategy strategy = new UserDiscountStrategy();

    // 允许客户端设置新策略:
    public void setStrategy(DiscountStrategy strategy) {
        this.strategy = strategy;
    }
	//计算折后价
    public BigDecimal calculatePrice(BigDecimal total) {
        return total.subtract(this.strategy.getDiscount(total)).setScale(2);
    }
}

调用方必须首先创建一个DiscountContext,并指定一个策略(或者使用默认策略),即可获得折扣后的价格:

DiscountContext ctx = new DiscountContext();

// 默认使用普通会员折扣:
BigDecimal pay1 = ctx.calculatePrice(BigDecimal.valueOf(105));
System.out.println(pay1);

// 使用满减折扣:
ctx.setStrategy(new OverDiscountStrategy());
BigDecimal pay2 = ctx.calculatePrice(BigDecimal.valueOf(105));
System.out.println(pay2);

// 使用Prime会员折扣:
ctx.setStrategy(new PrimeDiscountStrategy());
BigDecimal pay3 = ctx.calculatePrice(BigDecimal.valueOf(105));
System.out.println(pay3);

在这里插入图片描述

模板方法

模板方法(Template Method)是一个比较简单的模式。它的主要思想是,定义一个操作的一系列步骤,对于某些暂时确定不下来的步骤,就留给子类去实现好了,这样不同的子类就可以定义出不同的步骤。

假设开发了一个从数据库读取设置的类:

public class Setting {
    public final String getSetting(String key) {
        String value = readFromDatabase(key);
        return value;
    }

	private String readFromDatabase(String key) {
        // TODO: 从数据库读取
    }
}

由于从数据库读取数据较慢,可以考虑把读取的设置缓存起来,这样下一次读取同样的key就不必再访问数据库了。但是怎么实现缓存,暂时没想好,但不妨碍我们先写出使用缓存的代码:

public class Setting {
    public final String getSetting(String key) {
        // 先从缓存读取:
        String value = lookupCache(key);
        if (value == null) {
            // 在缓存中未找到,从数据库读取:
            value = readFromDatabase(key);
            System.out.println("[DEBUG] load from db: " + key + " = " + value);
            // 放入缓存:
            putIntoCache(key, value);
        } else {
            System.out.println("[DEBUG] load from cache: " + key + " = " + value);
        }
        return value;
    }
}

lookupCache(key)和putIntoCache(key, value)这两个方法没有实现编译不会通过——声明抽象方法:

public abstract class AbstractSetting {
    public final String getSetting(String key) {
        String value = lookupCache(key);
        if (value == null) {
            value = readFromDatabase(key);
            putIntoCache(key, value);
        }
        return value;
    }

    protected abstract String lookupCache(String key);

    protected abstract void putIntoCache(String key, String value);
}

声明了抽象方法,自然整个类也必须是抽象类。如何实现lookupCache(key)和putIntoCache(key, value)这两个方法就交给子类了。子类并不关心核心代码getSetting(key)的逻辑,只需要关心如何完成两个子任务就可以了。
假设我们希望用一个Map做缓存,那么可以写一个LocalSetting:

public class LocalSetting extends AbstractSetting {
    private Map<String, String> cache = new HashMap<>();

    protected String lookupCache(String key) {
        return cache.get(key);
    }

    protected void putIntoCache(String key, String value) {
        cache.put(key, value);
    }
}

使用Redis做缓存,那么可以再写一个RedisSetting:

public class RedisSetting extends AbstractSetting {
    private RedisClient client = RedisClient.create("redis://localhost:6379");

    protected String lookupCache(String key) {
        try (StatefulRedisConnection<String, String> connection = client.connect()) {
            RedisCommands<String, String> commands = connection.sync();
            return commands.get(key);
        }
    }

    protected void putIntoCache(String key, String value) {
        try (StatefulRedisConnection<String, String> connection = client.connect()) {
            RedisCommands<String, String> commands = connection.sync();
            commands.set(key, value);
        }
    }

客户端代码使用本地缓存的代码这么写:

AbstractSetting setting1 = new LocalSetting();
System.out.println("test = " + setting1.getSetting("test"));
System.out.println("test = " + setting1.getSetting("test"));

要改成Redis缓存,只需要把LocalSetting替换为RedisSetting:

AbstractSetting setting2 = new RedisSetting();
System.out.println("autosave = " + setting2.getSetting("autosave"));
System.out.println("autosave = " + setting2.getSetting("autosave"));

模板方法的核心思想是:父类定义骨架,子类实现某些细节。
为了防止子类重写父类的骨架方法,可以在父类中对骨架方法使用final。对于需要子类实现的抽象方法,一般声明为protected,使得这些方法对外部客户端不可见。

访问者

访问者模式(Visitor)是一种操作一组对象的操作,它的目的是不改变对象的定义,但允许新增不同的访问者,来定义新的操作。
访问者模式的设计比较复杂,查看GoF原始的访问者模式,它是这么设计的:
在这里插入图片描述
上述模式的复杂之处在于上述访问者模式为了实现所谓的“双重分派”,设计了一个回调再回调的机制。因为Java只支持基于多态的单分派模式,这里强行模拟出“双重分派”反而加大了代码的复杂性。

这里我们只介绍简化的访问者模式。假设我们要递归遍历某个文件夹的所有子文件夹和文件,然后找出.java文件,正常的做法是写个递归:

void scan(File dir, List<File> collector) {
    for (File file : dir.listFiles()) {
        if (file.isFile() && file.getName().endsWith(".java")) {
            collector.add(file);
        } else if (file.isDir()) {
            // 递归调用:
            scan(file, collector);
        }
    }
}

问题在于,扫描目录的逻辑和处理.java文件的逻辑混在了一起。如果下次需要增加一个清理.class文件的功能,就必须再重复写扫描逻辑。
因此,访问者模式先把数据结构(这里是文件夹和文件构成的树型结构)和对其的操作(查找文件)分离开,以后如果要新增操作(例如清理.class文件),只需要新增访问者,不需要改变现有逻辑。

用访问者模式改写上述代码步骤如下:
首先,需要定义访问者接口,即该访问者能够干的事情:

public interface Visitor {
    // 访问文件夹:
    void visitDir(File dir);
    // 访问文件:
    void visitFile(File file);
}

定义能持有文件夹和文件的数据结构FileStructure:

public class FileStructure {
    // 根目录:
    private File path;
    public FileStructure(File path) {
        this.path = path;
    }
}

然后,我们给FileStructure增加一个handle()方法,传入一个访问者:

public class FileStructure {
    ...

    public void handle(Visitor visitor) {
		scan(this.path, visitor);
	}

	private void scan(File file, Visitor visitor) {
		if (file.isDirectory()) {
            // 让访问者处理文件夹:
			visitor.visitDir(file);
			for (File sub : file.listFiles()) {
                // 递归处理子文件夹:
				scan(sub, visitor);
			}
		} else if (file.isFile()) {
            // 让访问者处理文件:
			visitor.visitFile(file);
		}
	}
}

这样就把访问者的行为抽象出来了。如果我们要实现一种操作,例如,查找.java文件,就传入JavaFileVisitor:

FileStructure fs = new FileStructure(new File("."));
fs.handle(new JavaFileVisitor());

这个JavaFileVisitor实现如下:

public class JavaFileVisitor implements Visitor {
    public void visitDir(File dir) {
        System.out.println("Visit dir: " + dir);
    }

    public void visitFile(File file) {
        if (file.getName().endsWith(".java")) {
            System.out.println("Found java file: " + file);
        }
    }
}

访问者模式的核心思想是为了访问比较复杂的数据结构,不去改变数据结构,而是把对数据的操作抽象出来,在“访问”的过程中以回调形式在访问者中处理操作逻辑。如果要新增一组操作,那么只需要增加一个新的访问者。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值