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

  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~numpublic 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的话DeniedSystem.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);}}
}

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

Java笔记二十二——设计模式相关推荐

  1. Java笔记(十二) 文件基础技术

    文件基础技术 一.文件概述 一)基本概念 1.文件的分类: 1)文本文件:文件中每个二进制字节都是某个可打印字符的一部分.如.java文件 2)二进制文件:文件中每个二进制字节不一定用来表示字符,也可 ...

  2. 大话设计模式笔记(二十一、二十二、二十三、二十四、二十五、二十六)

    二十一.单例模式(Singleton) 定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点. 1.通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象.一个最好的办法就是 ...

  3. Android开发笔记(一百二十二)循环器视图RecyclerView

    RecyclerView RecyclerView是Android在support-v7库中新推出控件,中文别名为循环器视图,它的功能非常强大,可分别实现ListView.GridView,以及瀑布流 ...

  4. JAVA基础知识总结:一到二十二全部总结

    >一: 一.软件开发的常识 1.什么是软件? 一系列按照特定顺序组织起来的计算机数据或者指令 常见的软件: 系统软件:Windows\Mac OS \Linux 应用软件:QQ,一系列的播放器( ...

  5. Android群英传笔记——第十二章:Android5.X 新特性详解,Material Design UI的新体验

    Android群英传笔记--第十二章:Android5.X 新特性详解,Material Design UI的新体验 第十一章为什么不写,因为我很早之前就已经写过了,有需要的可以去看 Android高 ...

  6. 备忘录模式 Memento 快照模式 标记Token模式 行为型 设计模式(二十二)

    备忘录模式 Memento 沿着脚印,走过你来时的路,回到原点. 苦海翻起爱恨 在世间难逃避命运 相亲竟不可接近 或我应该相信是缘份 一首<一生所爱>触动了多少人的心弦,一段五百年都没有结 ...

  7. 疯狂JAVA讲义---第十二章:Swing编程(五)进度条和滑动条

    http://blog.csdn.net/terryzero/article/details/3797782 疯狂JAVA讲义---第十二章:Swing编程(五)进度条和滑动条 标签: swing编程 ...

  8. tensorflow学习笔记(三十二):conv2d_transpose (解卷积)

    tensorflow学习笔记(三十二):conv2d_transpose ("解卷积") deconv解卷积,实际是叫做conv_transpose, conv_transpose ...

  9. OpenCV学习笔记(二十一)——绘图函数core OpenCV学习笔记(二十二)——粒子滤波跟踪方法 OpenCV学习笔记(二十三)——OpenCV的GUI之凤凰涅槃Qt OpenCV学习笔记(二十

    OpenCV学习笔记(二十一)--绘图函数core 在图像中,我们经常想要在图像中做一些标识记号,这就需要绘图函数.OpenCV虽然没有太优秀的GUI,但在绘图方面还是做得很完整的.这里就介绍一下相关 ...

  10. Windows保护模式学习笔记(十二)—— 控制寄存器

    Windows保护模式学习笔记(十二)-- 控制寄存器 控制寄存器 Cr0寄存器 Cr2寄存器 Cr4寄存器 控制寄存器 描述: 控制寄存器有五个,分别是:Cr0 Cr1 Cr2 Cr3 Cr4 Cr ...

最新文章

  1. 【Groovy】Groovy 环境搭建 ( 下载 Groovy | 安装 Groovy | 配置 Groovy 环境变量 )
  2. 关闭aslr oracle,地址空间布局随机化 (Address Space Layout Randomization, ASLR)
  3. SQLServer自增变量修复
  4. AngularJs angular.bind、angular.bootstrap、angular.copy
  5. PHP 与 Perl 对照表
  6. IBM T410 打开AHCI模式
  7. extjs5(05--主界面上加入顶部和底部区域)
  8. 关于安卓手机在微信浏览器中无法调起相机的原因
  9. armv7 cortex a系列编程手册_STM32、Cortex-M3和ARMv8-M之间的关联
  10. 信息学奥赛C++语言:高个子的人数
  11. Delphi调用Android的.so文件(转)
  12. CVPR2020 论文分类及全部下载!
  13. NP-Hard问题浅谈
  14. paip.FTP服务架设选型
  15. linux apache找不到woff2,IIS Web部署: svg/woff/woff2字体找不到
  16. python有一对兔子,从出生后第3个月起每个月都生一对兔子
  17. 关于微信表情及输入法emoji显示问题解决方案
  18. 手机连接投影机的步骤_手机连接投影机的步骤(投影仪无线网连接步骤)
  19. 如何利用python将NWPU VHR-10目标检测遥感数据集的格式转换成VOC目标检测数据集的格式
  20. 对SingleTask和TaskAffinity的理解

热门文章

  1. PHP 超全局变量详解 $GLOBALS $_SERVER $_GET $_POST $_COOKIE $_FILES $_ENV $_REQUEST $_SESSION
  2. php修改图片为指定大小,php裁剪图片为固定大小步骤详解
  3. 注销后的快手号怎么恢复
  4. WPS中Excel怎么批量合并相同单元格
  5. CentOS7 安装配置FTP服务器详解
  6. 毕亚兹 USB3.0分线器 无法识别多个U盘问题解决
  7. js实现一键复制(可用于复制微信号)
  8. 华为手机8大超实用功能!省心省力省钱!数数你用过几个
  9. 后端小白程序员的axios学习笔记
  10. TIM输出比较(PWM)