五、Singleton模式:只有一个实例
Singleton 是指只含有一个元素的集合。因为本模式只能生成一个实例,因此以 Singleton命名。
示例程序类图
Singleton.java
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {
System.out.println("生成了一个实例。");
}
public static Singleton getInstance() {
return singleton;
}
}
Main.java
public class Main {
public static void main(String[] args) {
System.out.println("Start.");
Singleton obj1 = Singleton.getInstance();
Singleton obj2 = Singleton.getInstance();
if (obj1 == obj2) {
System.out.println("obj1与obj2是相同的实例。");
} else {
System.out.println("obj1与obj2是不同的实例。");
}
System.out.println("End.");
}
}
运行结果
Start.
生成了一个实例。
obj1与obj2是相同的实例。
End.
扩展思路的要点
何时生成这个唯一的实例
注意示例程序的运行结果,在【Start.】之后就显示出了【生成了一个实例。】
程序运行后,在第一次调用 getInstance方法时,Singleton类会被初始化。也就是在这个时候,static字段singleton被初始化,生成了唯一的一个实例。
相关的设计模式
在以下模式中,多数情况下只会生成一个实例。
AbstractFactory 模式(第8章)
Builder 模式(第7章)
Facade 模式(第15章)
Prototype 模式(第6章)
六、Prototype模式:通过复制生成实例
Prototype模式:不根据类来生成实例(不使用 new ClassName() 的形式),而是根据实例来生成新实例
Prototype有“原型”“模型”的意思。在设计模式中,它是指根据实例原型、实例模型来生成新的实例。
Java可以使用clone创建出实例的副本。本章将学习clone方法与Cloneable接口的使用方法。
示例程序类图
Manager类
package framework;
import java.util.*;
public class Manager {
private HashMap showcase = new HashMap();
public void register(String name, Product proto) {
showcase.put(name, proto);
}
public Product create(String protoname) {
Product p = (Product) showcase.get(protoname);
return p.createClone();
}
}
Product接口
package framework;
import java.lang.Cloneable;
public interface Product extends Cloneable {
public abstract void use(String s);
public abstract Product createClone();
}
MessageBox类
import framework.*;
public class MessageBox implements Product {
private char decochar;
public MessageBox(char decochar) {
this.decochar = decochar;
}
public void use(String s) {
// 业务逻辑:把传入的s用decochar包围打印
}
public Product createClone() {
Product p = null;
try {
p = (Product) clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return p;
}
}
UnderlinePen类
与MessageBox几乎相同,use()方法中对s进行加下划线打印
import framework.*;
public class UnderlinePen implements Product {
private char ulchar;
public UnderlinePen(char ulchar) {
this.ulchar = ulchar;
}
public void use(String s) {
// 业务逻辑:把传入的s用decochar加下划线打印
}
public Product createClone() {
Product p = null;
try {
p = (Product) clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return p;
}
}
Main测试类
import framework.*;
public class Main {
public static void main(String[] args) {
// 准备
Manager manager = new Manager();
UnderlinePen upen = new UnderlinePen('~');
MessageBox mbox = new MessageBox('*');
MessageBox sbox = new MessageBox('/');
manager.register("strong message", upen);
manager.register("warning box", mbox);
manager.register("slash box", sbox);
// 生成
Product p1 = manager.create("strong message");
p1.use("Hello, world.");
Product p2 = manager.create("warning box");
p2.use("Hello, world.");
Product p3 = manager.create("slash box");
p3.use("Hello, world.");
}
}
运行结果
角色
-
Prototype(原型)
负责定义用于复制现有实例来生成新实例的方法。在示例程序中,由
Product
接口扮演此角色。 -
ConcretePrototype(具体的原型)
负责实现复制现有实例并生成新实例的方法。在示例程序中,由
MessageBox
类和UnderlinePen
类扮演此角色。 -
Client(使用者)
负责使用复制实例的方法生成新的实例。在示例程序中,由
Manager
类扮演此角色。
扩展思路的要点
不能根据类来生成实例吗
根据类生成实例:new Something()
不根据类生成实例的场景:
- 对象种类繁多,无法将它们整合到一个类中时
示例程序中,一共出现了3种对字符串处理的样式。但只要想做,能支持更多样式。
但若将每种样式都编写为一个类,类的数量将太多,较难管理源程序。
- 难以根据类生成实例时
本例中感觉不到这一点。试想若要开发一个用户可以使用鼠标进行操作的、类似于图形编辑器的应用程序,可能更容易理解。
假设我们想生成一个和用户通过一系列鼠标操作所创建出来的实例完全一样的实例,此时,与根据类来生成实例相比,根据实例来生成实例要更简单。
- 想解耦框架与生成的实例时
在示例程序中,我们将复制(clone)实例的部分封装在 framework包中了。
在Manager类的create方法中,没使用类名,取而代之使用了strong message
和slash box
等字符串为生成的实例命名。
与Java自带的生成实例的new Something()
方式相比,这种方式具有更好的通用性,而且将框架从类名的束缚中解脱出来了。
类名是束缚吗
在源程序中使用类名到底会有什么问题?在代码中出现要使用的类的名字不是理所当然的吗?
再回忆一下面向对象编程的目标之一:作为组件复用。
一旦在代码中出现要使用的类的名字,就无法与该类分离开来,也就无法实现复用。
虽然可以通过替换源代码或改变类名来解决这个问题。但此处说的“作为组件复用”不包含替换源代码。
以Java来说,重要的是当手边只有class文件(.class)时,该类能否被复用。即使没有Java文件(.java)也能复用该类才是关键。
当多个类必须紧密结合时,代码中出现这些类的名字没问题。但若那些需要被独立出来作为组件复用的类的名字出现在代码中,那就有问题了。
相关的设计模式
Flyweight 模式(第20章)
Prototype 模式可以生成一个与当前实例的状态完全相同的实例。
Flyweight 模式可以在不同的地方使用同一个实例。
Memento 模式(第18章)
Prototype 模式可以生成一个与当前实例的状态完全相同的实例。
Memento 模式可以保存当前实例的状态,以实现快照和撤销功能。
Composite 模式(第11章)以及Decorator模式(第12章)
经常使用Composite模式和Decorator模式时,需要能够动态地创建复杂结构的实例。这时可以使用 Prototype模式,以帮助我们方便地生成实例。
Command 模式(第22章)
想要复制Command 模式中出现的命令时,可以使用 Prototype模式。
补充:Clone注意事项
clone方法所进行的复制只是将被复制实例的字段值直接复制到新的实例中,即它没考虑字段中所保存的实例的内容。
例如,当字段中保存的是数组时,如果使用clone方法进行复制,则只会复制该数组的引用,并不会一一复制数组中的元素。
像上面这样的字段对字段的复制(field-to-field-copy)被称为浅复制(shallow copy)。clone方法所进行的复制就是浅复制。
当使用clone方法进行浅复制无法满足需求时,可以重写 clone方法,实现自己需要的复制功能(重写clone方法时,别忘了使用super.clone()
来调用父类的clone方法)。
clone方法只进行复制,不会调用被复制实例的构造函数。
对于在生成实例时需要进行特殊的初始化处理的类,需要自己去实现clone方法,在其内部进行这些初始化处理。
七、Builder模式:组装复杂的实例
示例程序类图
注意:此案例中Builder是抽象类,但也可以用interface来实现这个模式。
Director类
public class Director {
private Builder builder;
public Director(Builder builder) {
// 接收的参数是Builder类的子类,所以可以将其保存在builder字段中
this.builder = builder;
}
// 编写文档
public void construct() {
// 标题、字符串、条目
builder.makeTitle("Greeting");
builder.makeString("从早上至下午");
builder.makeItems(new String[]{"早上好。", "下午好。"});
// 其他字符串、其他条目
builder.makeString("晚上");
builder.makeItems(new String[]{"晚上好。", "晚安。", "再见。"});
// 完成文档
builder.close();
}
}
TextBuilder类
public class TextBuilder extends Builder {
// 文档内容保存在该字段中
private StringBuffer buffer = new StringBuffer();
// 纯文本的标题
public void makeTitle(String title) {
// 业务逻辑:buffer.append 装饰线、为标题添加『』、换行
}
// 纯文本的字符串
public void makeString(String str) {
// 业务逻辑:buffer.append 为字符串添加■、换行
}
// 纯文本的条目
public void makeItems(String[] items) {
// 业务逻辑:buffer.append 为条目添加・、换行
}
// 完成文档 添加装饰线
public void close() {
buffer.append("==============================\n");
}
// 完成的文档
public String getResult() {
return buffer.toString();
}
}
HTMLBuilder类
import java.io.*;
public class HTMLBuilder extends Builder {
private String filename; // 文件名
private PrintWriter writer; // 用于编写文件的PrintWriter
public void makeTitle(String title) { // HTML文件的标题
filename = title + ".html"; // 将标题作为文件名
try {
writer = new PrintWriter(new FileWriter(filename)); // 生成 PrintWriter
} catch (IOException e) {
e.printStackTrace();
}
writer.println("<html><head><title>" + title + "</title></head><body>"); // 输出标题
writer.println("<h1>" + title + "</h1>");
}
public void makeString(String str) { // HTML文件中的字符串
writer.println("<p>" + str + "</p>"); // 用<p>标签输出
}
public void makeItems(String[] items) { // HTML文件中的条目
writer.println("<ul>"); // 用<ul>和<li>输出
for (int i = 0; i < items.length; i++) {
writer.println("<li>" + items[i] + "</li>");
}
writer.println("</ul>");
}
public void close() { // 完成文档
writer.println("</body></html>"); // 关闭标签
writer.close(); // 关闭文件
}
public String getResult() { // 编写完成的文档
return filename; // 返回文件名
}
}
Main测试类
public class Main {
public static void main(String[] args) {
if (args.length != 1) {
usage();
System.exit(0);
}
if (args[0].equals("plain")) {
TextBuilder textbuilder = new TextBuilder();
Director director = new Director(textbuilder);
director.construct();
String result = textbuilder.getResult();
System.out.println(result);
} else if (args[0].equals("html")) {
HTMLBuilder htmlbuilder = new HTMLBuilder();
Director director = new Director(htmlbuilder);
director.construct();
String filename = htmlbuilder.getResult();
System.out.println(filename + "文件编写完成。");
} else {
usage();
System.exit(0);
}
}
public static void usage() {
System.out.println("Usage: java Main plain 编写纯文本文档");
System.out.println("Usage: java Main html 编写HTML文档");
}
}
角色
-
Builder(建造者)
负责定义用于生成实例的接口(API)。Builder角色中准备了用于生成实例的方法。在示例程序中,由Builder类扮演此角色。
-
ConcreteBuilder(具体的建造者)
是负责实现 Builder角色的接口的类(API),定义了在生成实例时实际被调用的方法、获取最终生成结果的方法。
在示例程序中,由TextBuilder类和HTMLBuilder类扮演此角色。
-
Director(监工)
负责使用Builder角色的接口(API)来生成实例。它并不依赖于ConcreteBuilder角色。
为了确保不论ConcreteBuilder角色是如何被定义的,Director角色都能正常工作,它只调用在Builder角色中被定义的方法。
在示例程序中,由Director类扮演此角色。
-
Client(使用者)
该角色使用了Builder模式(Builder模式并不包含Client角色)。
在示例程序中,由Main类扮演此角色。
相关的设计模式
Template Method模式(第3章)
Builder模式:Director角色控制Builder角色。
Template Method模式:父类控制子类。
Composite 模式(第11章)
有些情况下 Builder模式生成的实例构成了Composite模式。
Abstract Factory模式(第8章)
Builder模式和Abstract Factory模式都用于生成复杂的实例。
Facade模式(第15章)
在 Builder模式中,Director角色通过组合 Builder角色中的复杂方法向外部提供可以简单生成实例的接口(API)(相当于示例程序中的construct方法)。
Facade模式中的 Facade角色则是通过组合内部模块向外部提供可以简单调用的接口(API)。
扩展思路的要点
谁知道什么
Director类不知道自己使用的究竟是Builder类的哪个子类也好。因为“只有不知道子类才能替换”。
正是因为可以替换,组件才具有高价值。作为设计人员,我们必须时刻关注这种“可替换性”。
设计时能够决定的事情和不能决定的事情
在Builder类中,需要声明编辑文档(实现功能)所必需的所有方法,在Builder类中应当定义哪些方法很重要。
而且,Builder类还必须能够应对将来子类可能增加的需求,若将来要新编写一种文档类型的子类实现,是否需要新的方法?
虽然类的设计者无法准确地预测到将来可能发生的变化,但还是有必要让设计出的类能够尽可能灵活地应对近期可能发生的变化。
代码的阅读方法和修改方法
如果没有理解各个类的角色就动手增加和修改代码,在判断到底应该修改哪个类时,就会很容易出错。
八、Abstract Factory模式:将关联零件组装成产品
在 Abstract Factory模式中,不仅有“抽象工厂”,还有“抽象零件”和“抽象产品”。抽象工厂的工作是将“抽象零件”组装为“抽象产品”。
回顾面向对象编程中的“抽象”这个词的具体含义:它指的是“不考虑具体怎样实现,而是仅关注接口(API)"的状态。
例如,抽象方法(Abstract Method)并不定义方法的具体实现,而是仅仅只确定了方法的名字和签名(参数的类型和个数)。
我们不关心零件的具体实现,而只关心接口(API),仅使用该接口(API)将零件组装成为产品。
在 Tempate Method模式和Builder模式中,子类这一层负责方法的具体实现。
在 AbstractFactory模式中也是一样的。在子类这一层中有具体的工厂,它负责将具体的零件组装成为具体的产品。
本章中的示例程序的功能是将带有层次关系的链接的集合制作成HTML文件。
示例程序类图
factory包 - 抽象
Item
package factory;
public abstract class Item {
protected String caption;
public Item(String caption) {
this.caption = caption;
}
public abstract String makeHTML();
}
Link
package factory;
public abstract class Link extends Item {
protected String url;
public Link(String caption, String url) {
super(caption);
this.url = url;
}
}
Tray
package factory;
import java.util.ArrayList;
public abstract class Tray extends Item {
protected ArrayList tray = new ArrayList();
public Tray(String caption) {
super(caption);
}
// 将Link类和Tray类集合在一起。为表示集合的对象是“Link类和Tray类”,我们设置add方法的参数为Link类和Tray类的父类Item类。
public void add(Item item) {
tray.add(item);
}
}
Page
package factory;
import java.io.*;
import java.util.ArrayList;
public abstract class Page {
protected String title;
protected String author;
protected ArrayList content = new ArrayList();
public Page(String title, String author) {
this.title = title;
this.author = author;
}
// 向页面中增加Item(即Link或Tray)。增加的Item将会在页面中显示出来。
public void add(Item item) {
content.add(item);
}
// output方法是一个简单的 Template Method模式 的方法。
public void output() {
try {
// 首先 根据页面标题确定文件名
String filename = title + ".html";
Writer writer = new FileWriter(filename);
// 然后 调用makeHTML抽象方法将自身保存的HTML内容写入到文件中。
writer.write(this.makeHTML());
writer.close();
System.out.println(filename + " 编写完成。");
} catch (IOException e) {
e.printStackTrace();
}
}
public abstract String makeHTML();
}
Factory
package factory;
public abstract class Factory {
// 虽然getFactory方法生成的是具体工厂的实例,但是返回值的类型是抽象工厂类型
public static Factory getFactory(String classname) {
Factory factory = null;
try {
// 根据 类名字符串 生成具体工厂的实例。
factory = (Factory) Class.forName(classname).newInstance();
} catch (ClassNotFoundException e) {
System.err.println("没有找到 " + classname + "类。");
} catch (Exception e) {
e.printStackTrace();
}
return factory;
}
// createLink、createTray、createPage等方法 是用于在抽象工厂中生成零件和产品的方法
public abstract Link createLink(String caption, String url);
public abstract Tray createTray(String caption);
public abstract Page createPage(String title, String author);
}
listfactory - 具体
ListFactory
package listfactory;
import factory.*;
public class ListFactory extends Factory {
// 各个方法内部只是分别简单地new出了对应类的实例(根据实际需求,这里可能需要用 Prototype模式来进行clone)
public Link createLink(String caption, String url) {
return new ListLink(caption, url);
}
public Tray createTray(String caption) {
return new ListTray(caption);
}
public Page createPage(String title, String author) {
return new ListPage(title, author);
}
}
ListLink
package listfactory;
import factory.*;
public class ListLink extends Link {
public ListLink(String caption, String url) {
super(caption, url);
}
public String makeHTML() {
return " <li><a href=\"" + url + "\">" + caption + "</a></li>\n";
}
}
ListTray
package listfactory;
import factory.*;
import java.util.Iterator;
public class ListTray extends Tray {
public ListTray(String caption) {
super(caption);
}
public String makeHTML() {
StringBuffer buffer = new StringBuffer()
.append("<li>\n")
.append(caption + "\n")
.append("<ul>\n");
Iterator it = tray.iterator();
while (it.hasNext()) {
Item item = (Item) it.next();
// 这里并不关心变量item中保存的实例究竟是ListLink的实例还是ListTray的实例,只是简单地调用了item.makeHTML()语句而已。
// 同时这里不能使用 switch语句或if语句去判断变量item中保存的实例的类型,否则就不是面向对象编程了。
buffer.append(item.makeHTML());
}
buffer.append("</ul>\n")
.append("</li>\n");
return buffer.toString();
}
}
ListPage
package listfactory;
import factory.*;
import java.util.Iterator;
public class ListPage extends Page {
public ListPage(String title, String author) {
super(title, author);
}
public String makeHTML() {
StringBuffer buffer = new StringBuffer()
.append("<html><head><title>" + title + "</title></head>\n")
.append("<body>\n")
.append("<h1>" + title + "</h1>\n")
.append("<ul>\n");
// content继承自Page类的字段。
Iterator it = content.iterator();
while (it.hasNext()) {
Item item = (Item) it.next();
buffer.append(item.makeHTML());
}
buffer.append("</ul>\n")
.append("<hr><address>" + author + "</address>")
.append("</body></html>\n");
return buffer.toString();
}
}
Main
import factory.*;
public class Main {
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Usage: java Main class.name.of.ConcreteFactory");
System.out.println("Example 1: java Main listfactory.ListFactory");
System.out.println("Example 2: java Main tablefactory.TableFactory");
System.exit(0);
}
Factory factory = Factory.getFactory(args[0]);
Link people = factory.createLink("人民日报", "http://www.people.com.cn/");
Link gmw = factory.createLink("光明日报", "http://www.gmw.cn/");
Link us_yahoo = factory.createLink("Yahoo!", "http://www.yahoo.com/");
Link jp_yahoo = factory.createLink("Yahoo!Japan", "http://www.yahoo.co.jp/");
Link excite = factory.createLink("Excite", "http://www.excite.com/");
Link google = factory.createLink("Google", "http://www.google.com/");
Tray traynews = factory.createTray("日报");
traynews.add(people);
traynews.add(gmw);
Tray trayyahoo = factory.createTray("Yahoo!");
trayyahoo.add(us_yahoo);
trayyahoo.add(jp_yahoo);
Tray traysearch = factory.createTray("检索引擎");
traysearch.add(trayyahoo);
traysearch.add(excite);
traysearch.add(google);
Page page = factory.createPage("LinkPage", "杨文轩");
page.add(traynews);
page.add(traysearch);
page.output();
}
}
为示例程序增加其他工厂
当只有一个具体工厂的时候,没有必要划分“抽象类”与“具体类”。
可以为示例程序增加其他的具体工厂,比如编写含有表格的HTML格式的文件。
之前的listfactory包的功能是将超链接以条目形式展示出来,
现在可以新建tablefactory包,在其下编写其他工厂将链接以表格形式展示出来:
新增TableFactory、TableLink、TableTray、TablePage,内容与listfactory包下对应的类相似,只是在编写html具体内容时有些要改为table
标签之类的。
角色
-
AbstractProduct(抽象产品)
定义 AbstractFactory 角色所生成的抽象零件和产品的接口(API)。
示例中是 Link类、Tray类、Page类
-
AbstractFactory(抽象工厂)
定义用于生成抽象产品的接口(API)。
示例中是 Factory类
-
Client(委托者)
仅会调用 AbstractFactory角色和AbstractProduct 角色的接口(API)来进行工作,不知道具体的零件、产品、工厂。
示例中是 Main类,上图省略了这一角色。
-
ConcreteProduct(具体产品)
实现AbstractProduct 角色的接口(API)。
示例中是以下
listfactory包:ListLink类、ListTray类、ListPage类
tablefactory包:TableLink类、TableTray类、TablePage类
-
ConcreteFactory(具体工厂)
实现AbstractFactory角色的接口(API)。
示例中是以下
listfactory包:Listfactory类
tablefactory包:Tablefactory类
拓展思路的要点
易于增加具体的工厂
在本模式中很容易增加具体的工厂,因为需要编写哪些类和需要实现哪些方法都非常清楚。
无论要增加多少个具体工厂(或是要修改具体工厂的 Bug),都无需修改抽象工厂和 Main部分。
难以增加新的零件
怎么在 Abstract Factory模式中增加新的零件?
例如,我们要在factory包中增加一个表示图像的Picture零件,必须对所有的具体工厂进行相应的修改才行。
具体来说,在listfactory包中,需做修改:在ListFactory 中加入createPicture方法,新增ListPicture类。
已编写完成的具体工厂越多,修改的工作量就会越大。
相关的设计模式
Builder 模式(第7章)
Abstract Factory 模式:通过调用抽象产品的接口(API)来组装抽象产品,生成具有复杂结构的实例。
Builder 模式:分阶段地制作复杂实例。
Factory Method模式(第4章)
有时Abstract Factory 模式中零件和产品的生成会使用到 Factory Method模式。
Composite模式(第11章)
有时Abstract Factory模式在制作产品时会使用Composite模式。
Singleton 模式(第5章)
有时Abstract Factory 模式中的具体工厂会使用 Singleton模式。