Java编程思想(第5版)—26设计模式
⽬录
第⼆⼗五章设计模式
概念
最初,你可以将模式视为解决特定类问题的⼀种特别巧妙且有深刻见解的⽅法。这就像前辈已经从所有⾓度去解决问题,并提出了最通⽤,最灵活的解决⽅案。问题可能是你之前看到并解决过的问题,
但你的解决⽅案可能没有你在模式中体现的那种完整性。
虽然它们被称为“设计模式”,但它们实际上并不与设计领域相关联。模式似乎与传统的分析、设计和实现的思维⽅式不同。相反,模式在程序中体现了⼀个完整的思想,因此它有时会出现在分析阶段或
⾼级设计阶段。因为模式在代码中有⼀个直接的实现,所以你可能不会期望模式在低级设计或实现之前出现(⽽且通常在到达这些阶段之前,你不会意识到需要⼀个特定的模式)。
模式的基本概念也可以看作是程序设计的基本概念:添加抽象层。当你抽象⼀些东西的时候,就像在剥离特定的细节,⽽这背后最重要的动机之⼀是:
将易变的事物与不变的事物分开
另⼀种⽅法是,⼀旦你发现程序的某些部分可能因某种原因⽽发⽣变化,你要保持这些变化不会引起整个代码中其他变化。如果代码更容易理解,那么维护起来会更容易。
通常,开发⼀个优雅且易维护设计中最困难的部分是发现我称之为变化的载体(也就是最易改变的地⽅)。这意味着找到系统中最重要的变化,换⽽⾔之,找到变化会导致最严重后果的地⽅。⼀旦发现
变化载体,就可以围绕构建设计的焦点。
因此,设计模式的⽬标是隔离代码中的更改。如果以这种⽅式去看,你已经在本书中看到了设计模式。例如,继承可以被认为是⼀种设计模式(虽然是由编译器实现的)。它允许你表达所有具有相同接
⼝的对象(即保持相同的⾏为)中的⾏为差异(这就是变化的部分)。组合也可以被视为⼀种模式,因为它允许你动态或静态地更改实现类的对象,从⽽改变类的⼯作⽅式。
你还看到了设计模式中出现的另⼀种模式:迭代器(Java 1.0和1.1随意地将其称为枚举; Java 2 集合才使⽤Iterator)。当你逐个选择元素时并逐步处理,这会隐藏集合的特定实现。迭代器允许你编写通
⽤代码,该代码对序列中的所有元素执⾏操作,⽽不考虑序列的构建⽅式。因此,你的通⽤代码可以与任何可以⽣成迭代器的集合⼀起使⽤。
即使模式是⾮常有⽤的,但有些⼈断⾔:
设计模式代表语⾔的失败。
这是⼀个⾮常重要的见解,因为⼀个模式在 C++ 有意义,可能在JAVA或者其他语⾔中就没有意义。出于这个原因,所以⼀个模式可能出现在设计模式书上,不意味着应⽤于你的编程语⾔是有⽤的。
我认为“语⾔失败”这个观点是有道理的,但是我也认为这个观点过于简单化。如果你试图解决⼀个特定的问题,⽽你使⽤的语⾔没有直接提供⽀持你使⽤的技巧,你可以说这个是语⾔的失败。但是,你
使⽤特定的技巧的频率的是多少呢?也许平衡是对的:当你使⽤特定的技巧的时候,你必须付出更多的努⼒,但是你⼜没有⾜够的理由去使得语⾔⽀持这个技术。另⼀⽅⾯,没有语⾔的⽀持,使⽤这种
技术常常会很混乱,但是在语⾔⽀持下,你可能会改变编程⽅式(例如,Java 8流实现此⽬的)。
单例模式
也许单例模式是最简单的设计模式,它是⼀种提供⼀个且只有⼀个对象实例的⽅法。这在java库中使⽤,但是这有个更直接的⽰例:
// patterns/SingletonPattern.java
interface Resource {
int getValue();
void tValue(int x);
}
/*
* 由于这不是从Cloneable基类继承⽽且没有添加可克隆性,
* 因此将其设置为final可防⽌通过继承添加可克隆性。
* 这也实现了线程安全的延迟初始化:
*/
final class Singleton {
private static final class ResourceImpl implements Resource {
private int i;
private ResourceImpl(int i) {
this.i = i;
}
public synchronized int getValue() {
return i;
}
public synchronized void tValue(int x) {
i = x;
}
}
private static class ResourceHolder {
private static Resource resource = new ResourceImpl(47);
}
public static Resource getResource() {
source;
}
}
public class SingletonPattern {
public static void main(String[] args) {
Resource r = Resource();
System.out.Value());
Resource s2 = Resource();
s2.tValue(9);
System.out.Value());
try {
// 不能这么做,会发⽣:compile-time error(编译时错误).
// Singleton s3 = (Singleton)s2.clone();
} catch(Exception e) {
throw new RuntimeException(e);
}
交货时间
}
} /* Output: 47 9 */
创建单例的关键是防⽌客户端程序员直接创建对象。在这⾥,这是通过在Singleton类中将Resource的实现作为私有类来实现的。
此时,你将决定如何创建对象。在这⾥,它是按需创建的,在第⼀次访问的时候创建。该对象是私有的,只能通过public getResource()⽅法访问。
懒惰地创建对象的原因是它嵌套的私有类resourceHolder在⾸次引⽤之前不会加载(在getResource()中)。当Resource对象加载的时候,静态初始化块将被调⽤。由于JVM的⼯作⽅式,这种静态初
始化是线程安全的。为保证线程安全,Resource中的getter和tter是同步的。
模式分类
“设计模式”⼀书讨论了23种不同的模式,分为以下三种类别(所有这些模式都围绕着可能变化的特定⽅⾯)。
创建型:如何创建对象。这通常涉及隔离对象创建的细节,这样你的代码就不依赖于具体的对象的类型,因此在添加新类型的对象时不会更改。单例模式(Singleton)被归类为创作模式,本章稍后你将
看到Factory Method的⽰例。
构造型:设计对象以满⾜特定的项⽬约束。它们处理对象与其他对象连接的⽅式,以确保系统中的更改不需要更改这些连接。
⾏为型:处理程序中特定类型的操作的对象。这些封装要执⾏的过程,例如解释语⾔、实现请求、遍历序列(如在迭代器中)或实现算法。本章包含观察者和访问者模式的例⼦。
《设计模式》⼀书中每个设计模式都有单独的⼀个章节,每个章节都有⼀个或者多个例⼦,通常使⽤C++,但有时也使⽤SmallTalk。本章不重复设计模式中显⽰的所有模式,因为该书独⽴存在,应单独
研究。相反,你会看到⼀些⽰例,可以为你提供关于模式的理解以及它们如此重要的原因。
构建应⽤程序框架
应⽤程序框架允许您从⼀个类或⼀组类开始,创建⼀个新的应⽤程序,重⽤现有类中的⼤部分代码,并根据需要覆盖⼀个或多个⽅法来定制应⽤程序。
模板⽅法模式
应⽤程序框架中的⼀个基本概念是模板⽅法模式,它通常隐藏在底层,通过调⽤基类中的各种⽅法来驱动应⽤程序(为了创建应⽤程序,您已经覆盖了其中的⼀些⽅法)。
模板⽅法模式的⼀个重要特性是它是在基类中定义的,并且不能更改。它有时是⼀个 private ⽅法,但实际上总是 final。它调⽤其他基类⽅法(您覆盖的那些)来完成它的⼯作,但是它通常只作为初始化过
程的⼀部分被调⽤(因此框架使⽤者不⼀定能够直接调⽤它)。
// patterns/TemplateMethod.java
// Simple demonstration of Template Method
abstract class ApplicationFramework {
ApplicationFramework() {
templateMethod();
}
abstract void customize1();
abstract void customize2(); // "private" means automatically "final": private void templateMethod() { IntStream.range(0, 5).forEach( n -> { customize1(); customize2(); }); }}// Create a new "application": class MyApp extends A void customize2() {
System.out.println("World!");
}
}
public class TemplateMethod {
public static void main(String[] args) {
new MyApp();
}
}
比高矮/*
Output:
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
*/
基类构造函数负责执⾏必要的初始化,然后启动运⾏应⽤程序的“engine”(模板⽅法模式)(在GUI应⽤程序中,这个“engine”是主事件循环)。框架使⽤者只提供
customize1() 和 customize2() 的定义,然后“应⽤程序”已经就绪运⾏。
⾯向实现
代理模式和桥接模式都提供了在代码中使⽤的代理类;完成⼯作的真正类隐藏在这个代理类的后⾯。当您在代理中调⽤⼀个⽅法时,它只是反过来调⽤实现类中的⽅法。这两种模式⾮常相似,所以代理模式只是桥接模式的⼀种特殊情况。⼈们倾向于将两者合并,称为代理模式,但是术语“代理”有⼀个长期的和专门的含义,这可能解释了这两种模式不同的原因。基本思想很简单:从基类派⽣代理,同时派⽣⼀个或多个提供实现的类:创建代理对象时,给它⼀个可以调⽤实际⼯作类的⽅法的实现。
在结构上,代理模式和桥接模式的区别很简单:代理模式只有⼀个实现,⽽桥接模式有多个实现。在设计模式中被认为是不同的:代理模式⽤于控制对其实现的访问,⽽桥接模式允许您动态更改实现。但是,如果您扩展了“控制对实现的访问”的概念,那么这两者就可以完美地结合在⼀起
代理模式
如果我们按照上⾯的关系图实现,它看起来是这样的:
// patterns/ProxyDemo.java
// Simple demonstration of the Proxy pattern
interface ProxyBa {
void f();
void g();
void h();
}
class Proxy implements ProxyBa {
private ProxyBa implementation;
Proxy() {
implementation = new Implementation();
}
// Pass method calls to the implementation:
@Override
public void f() { implementation.f(); }
@Override
public void g() { implementation.g(); }
@Override
public void h() { implementation.h(); }
}
class Implementation implements ProxyBa {
public void f() {
System.out.println("Implementation.f()");
}
public void g() {
System.out.println("Implementation.g()");
}
public void h() {
System.out.println("Implementation.h()");
}
}
public class ProxyDemo {
public static void main(String[] args) {
Proxy p = new Proxy();
p.f();
p.g();
p.h();
}
}
/*
Output:
Implementation.f()
Implementation.g()
Implementation.h()
*/
具体实现不需要与代理对象具有相同的接⼝;只要代理对象以某种⽅式“代表具体实现的⽅法调⽤,那么基本思想就算实现了。然⽽,拥有⼀个公共接⼝是很⽅便的,因此具体实现必须实现代理对象调⽤的所有⽅法。
状态模式
状态模式向代理对象添加了更多的实现,以及在代理对象的⽣命周期内从⼀个实现切换到另⼀种实现的⽅法:
// patterns/StateDemo.java // Simple demonstration of the State pattern
interface StateBa {
void f();
void g();
void h();
void changeImp(StateBa newImp);
}
class State implements StateBa {
private StateBa implementation;
State(StateBa imp) {
implementation = imp;
}
@Override
public void changeImp(StateBa newImp) {
implementation = newImp;
}// Pass method calls to the implementation: @Override public void f() { implementation.f(); } @Override public void g() { implementation.g(); } @Override
public void h() {
implementation.h();
}
}
class Implementation1 implements StateBa {
@Override
public void f() {
System.out.println("Implementation1.f()");
}
@Override
public void g() {
System.out.println("Implementation1.g()");
}
@Override
public void h() {
System.out.println("Implementation1.h()");
}
public void changeImp(StateBa newImp) {
}
}
class Implementation2 implements StateBa {
@Override
public void f() {
System.out.println("Implementation2.f()");
}
@Override
public void g() {
System.out.println("Implementation2.g()");
}
@Override
public void h() {
System.out.println("Implementation2.h()");
}
@Override
public void changeImp(StateBa newImp) {
}
}
public class StateDemo {
static void test(StateBa b) {
b.f();
b.g();
b.h();
}
public static void main(String[] args) {
StateBa b = new State(new Implementation1());
test(b);
b.changeImp(new Implementation2());
test(b);
}
}
/* Output:
Implementation1.f()
Implementation1.g()
Implementation1.h()
Implementation2.f()
Implementation2.g()
Implementation2.h()
*/
在main()中,⾸先使⽤第⼀个实现,然后改变成第⼆个实现。代理模式和状态模式的区别在于它们解决的问题。设计模式中描述的代理模式的常见⽤途如下:
远程代理。它在不同的地址空间中代理对象。远程⽅法调⽤(RMI)编译器rmic会⾃动为您创建⼀个远程代理。
虚拟代理。这提供了“懒加载”来根据需要创建“昂贵”的对象。
保护代理。当您希望对代理对象有权限访问控制时使⽤。
智能引⽤。要在被代理的对象被访问时添加其他操作。例如,跟踪特定对象的引⽤数量,来实现写时复制⽤法,和防⽌对象别名。⼀个更简单的例⼦是跟踪特定⽅法的调⽤数量。您可以将Java引⽤视为⼀种保护代理,因为它控制在堆上实例对象的访问(例如,确保不使⽤空引⽤)。
在设计模式中,代理模式和桥接模式并不是相互关联的,因为它们被赋予(我认为是任意的)不同的结构。桥接模式,特别是使⽤⼀个单独的实现,但这似乎对我来说是不必要的,除⾮你确定该实现是你⽆法控制的(当然有可能,但是如果您编写所有代码,那么没有理由不从单基类的优雅中受益)。此外,只要代理对象控制对其“前置”对象的访问,代模式理就不需要为其实现使⽤相同的基类。不管具体情况如何,在代理模式和桥接模式中,代理对象都将⽅法调⽤传递给具体实现对象。
状态机
桥接模式允许程序员更改实现,状态机利⽤⼀个结构来⾃动地将实现更改到下⼀个。当前实现表⽰系
统所处的状态,系统在不同状态下的⾏为不同(因为它使⽤桥接模式)。基本上,这是⼀个利⽤对象
的“状态机”。将系统从⼀种状态移动到另⼀种状态的代码通常是模板⽅法模式,如下例所⽰:
// patterns/state/StateMachineDemo.java
// The StateMachine pattern and Template method
// {java patterns.state.StateMachineDemo}
package patterns.state;
import onjava.Nap;
interface State {
void run();
}
abstract class StateMachine {
protected State currentState;
Nap(0.5);
System.out.println("Washing"); new
protected abstract boolean changeState();
// Template method:
protected final void runAll() {
while (changeState()) // Customizable
currentState.run();
}
}
// A different subclass for each state:
class Wash implements State {
@Override
public void run() {
}
}
class Spin implements State {
@Override
七夕节是什么意思public void run() {
System.out.println("Spinning");
new Nap(0.5);
}
}
class Rin implements State {
@Override
public void run() {
System.out.println("Rinsing");
new Nap(0.5);
}
}
class Washer extends StateMachine {
private int i = 0;
// The state table:
private State[] states = {new Wash(), new Spin(), new Rin(), new Spin(),};
}
@Override
public boolean changeState() {
if (i < states.length) {
// Change the state by tting the
// surrogate reference to a new object:
currentState = states[i++];
古代丝绸之路
return true;
} el return fal;
}
}
public class StateMachineDemo {
public static void main(String[] args) {
new Washer();
}
}
/*
Output:
Washing
Spinning
Rinsing
Spinning
*/
在这⾥,控制状态的类(本例中是状态机)负责决定下⼀个状态。然⽽,状态对象本⾝也可以决定下⼀步移动到什么状态,通常基于系统的某种输⼊。这是更灵活的解决⽅案。
⼯⼚模式
当你发现必须将新类型添加到系统中时,合理的第⼀步是使⽤多态性为这些新类型创建⼀个通⽤接⼝。这会将你系统中的其余代码与要添加的特定类型的信息分开,使得可以在不改变现有代码的情况下添加新类型……或者看起来如此。起初,在这种设计中,似乎你必须更改代码的唯⼀地⽅就是你继承新类型的地⽅,但这并不是完全正确的。你仍然必须创建新类型的对象,并且在创建时必须指定要使⽤的确切构造器。因此,如果创建对象的代码分布在整个应⽤程序中,那么在添加新类型时,你将遇到相同的问题——你仍然必须追查你代码中新类型碍事的所有地⽅。恰好是类型的创建碍事,⽽不是类型的使⽤(通过多态处理),但是效果是⼀样的:添加新类型可能会引起问题。
解决⽅案是强制对象的创建都通过通⽤⼯⼚进⾏,⽽不是允许创建代码在整个系统中传播。如果你程序中的所有代码都必须执⾏通过该⼯⼚创建你的⼀个对象,那么在添加新类时只需要修改⼯⼚即可。
由于每个⾯向对象的程序都会创建对象,并且很可能会通过添加新类型来扩展程序,因此⼯⼚是最通⽤的设计模式之⼀。
举例来说,让我们重新看⼀下Shape系统。⾸先,我们需要⼀个⽤于所有⽰例的基本框架。如果⽆法创建Shape对象,则需要抛出⼀个合适的异常:
// patterns/shapes/BadShapeCreation.java package patterns.shapes;
public class BadShapeCreation extends RuntimeException {
public BadShapeCreation(String msg) {
super(msg);
}
老舍作品}
接下来,是⼀个Shape基类:
// patterns/shapes/Shape.java
package patterns.shapes;
public class Shape {
private static int counter = 0;
private int id = counter++;
@Override
public String toString(){
return getClass().getSimpleName() + "[" + id + "]";
}
public void draw() {
System.out.println(this + " draw");
}
public void era() {
System.out.println(this + " era");
}
}
该类⾃动为每⼀个Shape对象创建⼀个唯⼀的id。
toString()使⽤运⾏期信息来发现特定的Shape⼦类的名字。
现在我们能很快创建⼀些Shape⼦类了:
// patterns/shapes/Circle.java
package patterns.shapes;
public class Circle extends Shape {}
// patterns/shapes/Square.java
package patterns.shapes;
public class Square extends Shape {}
// patterns/shapes/Triangle.java
package patterns.shapes;
public class Triangle extends Shape {}
⼯⼚是具有能够创建对象的⽅法的类。我们有⼏个⽰例版本,因此我们将定义⼀个接⼝:
// patterns/shapes/FactoryMethod.java
package patterns.shapes;
public interface FactoryMethod {
Shape create(String type);
}
create()接收⼀个参数,这个参数使其决定要创建哪⼀种Shape对象,这⾥是String,但是它其实可以是任何数据集合。对象的初始化数据(这⾥是字符串)可能来⾃系统外部。这个例⼦将测试⼯⼚:// patterns/shapes/FactoryTest.java
package patterns.shapes;
import java.util.stream.*;
public class FactoryTest {
public static void test(FactoryMethod factory) {
Stream.of("Circle", "Square", "Triangle",
"Square", "Circle", "Circle", "Triangle")
.map(factory::create)
.peek(Shape::draw)
.peek(Shape::era)
.count(); // Terminal operation
}
}
在主函数main()⾥,要记住除⾮你在最后使⽤了⼀个终结操作,否则Stream不会做任何事情。在这⾥,count()的值被丢弃了。
创建⼯⼚的⼀种⽅法是显式创建每种类型:
// patterns/ShapeFactory1.java
// A simple static factory method
import java.util.*;
import java.util.stream.*;
import patterns.shapes.*;
public class ShapeFactory1 implements FactoryMethod {
public Shape create(String type) {
switch(type) {
ca "Circle": return new Circle();
ca "Square": return new Square();
ca "Triangle": return new Triangle();
default: throw new BadShapeCreation(type);
}
}
public static void main(String[] args) {
}
}
输出结果:
Circle[0] draw
Circle[0] era
Square[1] draw
Square[1] era
Square[3] draw
Square[3] era
Circle[4] draw
Circle[4] era
Circle[5] draw
Circle[5] era
Triangle[6] draw
Triangle[6] era
create()现在是添加新类型的Shape时系统中唯⼀需要更改的其他代码。
动态⼯⼚
前⾯例⼦中的静态create()⽅法强制所有创建操作都集中在⼀个位置,因此这是添加新类型的Shape时唯⼀必须更改代码的地⽅。这当然是⼀个合理的解决⽅案,因为它把创建对象的过程限制在⼀个框内。但是,如果你在添加新类时⽆需修改任何内容,那就太好了。以下版本使⽤反射在⾸次需要时将Shape的构造器动态加载到⼯⼚列表中:
// patterns/ShapeFactory2.java
import java.util.*;
import flect.*;
import java.util.stream.*;
import patterns.shapes.*;
public class ShapeFactory2 implements FactoryMethod {
Map<String, Constructor> factories = new HashMap<>();
static Constructor load(String id) {
System.out.println("loading " + id);
try {
return Class.forName("patterns.shapes." + id)
.getConstructor();
} catch(ClassNotFoundException |
NoSuchMethodException e) {
throw new BadShapeCreation(id);
}
}
public Shape create(String id) {
try {
return (Shape)factories
.computeIfAbnt(id, ShapeFactory2::load)
.newInstance();
} catch(InstantiationException |
IllegalAccessException |
InvocationTargetException e) {
早婚早育
throw new BadShapeCreation(id);
}
}
public static void main(String[] args) {
}
}
输出结果:
loading Circle
Circle[0] draw
Circle[0] era
loading Square
Square[1] draw
Square[1] era
loading Triangle
Triangle[2] draw
Triangle[2] era
Square[3] draw
Square[3] era
Circle[4] draw
Circle[4] era
Circle[5] draw
Circle[5] era
Triangle[6] draw
Triangle[6] era
和之前⼀样,create()⽅法基于你传递给它的String参数⽣成新的Shapes,但是在这⾥,它是通过在HashMap中查找作为键的String来实现的。返回的值是⼀个构造器,该构造器⽤于通过调
⽤newInstance()创建新的Shape对象。
然⽽,当你开始运⾏程序时,⼯⼚的map为空。create()使⽤map的computeIfAbnt()⽅法来查找构造器(如果该构造器已存在于map中)。如果不存在则使⽤load()计算出该构造器,并将其插⼊
到map中。从输出中可以看到,每种特定类型的Shape都是在第⼀次请求时才加载的,然后只需要从map中检索它。
多态⼯⼚
《设计模式》这本书强调指出,采⽤“⼯⼚⽅法”模式的原因是可以从基本⼯⼚中继承出不同类型的⼯⼚。再次修改⽰例,使⼯⼚⽅法位于单独的类中:
// patterns/ShapeFactory3.java
// Polymorphic factory methods
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import patterns.shapes.*;
interface PolymorphicFactory {
Shape create();
}
class RandomShapes implements Supplier<Shape> {
private final PolymorphicFactory[] factories;
private Random rand = new Random(42);
化学笔记
factories){
this.factories = factories;
}
public Shape get() {
return factories[ Int(factories.length)].create();
}
}
public class ShapeFactory3 {
public static void main(String[] args) {
RandomShapes rs = new RandomShapes(
Circle::new,
Square::new,
Triangle::new);
.limit(6)
.peek(Shape::draw)
.peek(Shape::era)
.count();
}
}
输出结果:
Triangle[0] draw
卤猪大肠的做法
Triangle[0] era
Circle[1] draw
Circle[1] era
Circle[2] draw
Circle[2] era
Triangle[3] draw
Triangle[3] era
Circle[4] draw
Circle[4] era
Square[5] draw
Square[5] era
RandomShapes实现了Supplier <Shape>,因此可⽤于通过ate()创建Stream。它的构造器采⽤PolymorphicFactory对象的可变参数列表。变量参数列表以数组形式出现,因此列表是以数