目录
创建型模式概述
Factory Method: 唯一的类创建型模式
Abstract Factory
Builder模式
Prototype模式
Singleton模式
最近在参与一个量化交易系统的项目,里面涉及到用java来重构部分vnpy的开源框架,因为是框架的搭建,所以会涉及到像事件驱动等设计模式的应用,因此不了解基础的设计模式就无法理解框架设计者对于各个模块以及类的设计,而这也正是我目前所欠缺的能力,之前看到大多数是偏编码规则。这也是我选取《设计模式-可复用面向对对象软件的基础》这本书进行学习的原因。
这本书分为三个部分,第一部分是导论,虽然说是导论但我觉得更多的是跳出设计模式的细节来看它具体在实际项目中的工作流以及一些设计模式的抽象概念,这部分我自认为对于一个对于各个设计模式没有实际实现经验的初学者来说过于抽象,所以我准备放到最后来看,可能会有更深的体会。第二部分是一个案例学习,带领你从零开始构建一个文本编辑器的设计模式并实现它,这部分我准备放在第二部分来看。第三部分就是介绍了三类23个典型的设计模式,这部分我准备首先了解,以期对于设计模式先有一个更加深刻的认识。
这篇文章先总结一下三类设计模式中的第一类-创建型模式。
创建型模式概述
首先,要明确这里所说的设计模式重点聚焦系统级的设计,系统本质上是一组类实例的组合,它区别于单一的类或者接口的设计,需要一定的抽象才能够使得后续的编码工作更有效率。创建型模式的最主要作用就是抽象了系统的实例化过程。在没有这种抽象之前,系统的实例化只是单纯的被看作是N个类的实例化,不利于后续的编码执行。
这种抽象在大体上有两个实现的原则。其一是将系统所需要用到的各种类信息给封装起来;其二是把这些类的创建和组合方式也给隐藏起来。总之,整个系统有一套抽象的接口负责统一和外部对接。至于这个系统内各个类的配置可以是静态的(在编译时固定,也就是类创建型模式),或者是动态的(在运行时指定,也就是对象创建型模式)。换句话说,类创建模式就是为每一个系统预先定义好类的框架,每一个框架通过特定的方法完成创建;而对象创建型模式就是没有预先定义系统类框架,系统是在程序执行中动态完成创建的。
Factory Method: 唯一的类创建型模式
Factory Method(工厂方法)首先针对目标系统定义一个抽象接口,然后让具体的系统实现类来决定如何实例化,它要求使用者必须先定义系统类的框架,而把类的实例化延迟到了子类。
文中作者举了一个案例,对于要设计一套面向不同文件类型的应用处理系统,比如对于图像文件要有图像文件处理系统,对于文本文件要有文本文件处理系统等等。
首先我预先定义好文件类型的接口以及具体的实现类:
//抽象接口
public interface Document {
void open();
void close();
void save();
}
//实现类FigureDocument
public class FigureDocument implements Document {
@Override
public void open() {
System.out.println("Opening Figure Document");
}
@Override
public void close() {
System.out.println("Closing Figure Document");
}
@Override
public void save() {
System.out.println("Saving Figure Document");
}
}
//实现类TextDocument
public class TextDocument implements Document {
@Override
public void open(){
System.out.println("Text Document Opened");
}
@Override
public void close() {
System.out.println("Text Document Closed");
}
@Override
public void save() {
System.out.println("Text Document Saved");
}
}
很显然在Application抽象实例中,它是不知道要在什么情况下创建什么文件的,因此我们针对每一个文档都设计对应的application实现类,并且重构对应的document工厂方法:
//抽象接口
public interface Application {
Document createDocument();
}
//实现类FigureApplication
public class FigureApplication implements Application {
@Override
public Document createDocument() {
return new FigureDocument();
}
}
//实现类TextApplication
public class TextApplication implements Application {
public Document createDocument()
{
return new TextDocument();
}
}
然后客户端就可以轻松针对不同的application实现来针对性的创建对应的文件:
public class Client {
public static void main(String[] args) {
Application figureApplication = new FigureApplication();
Document figuredoc = figureApplication.createDocument();
figuredoc.open();
}
}
优点
首先我认为设计模式的优点最主要还是要面向系统框架的应用开发人员,他们是不是能在系统稳定的前提下简化并且高效率的使用这个框架是核心。从这个维度上说工厂模式有以下的优点:它将具体的系统类构建和它的抽象框架分离。使得应用开发人员除了在实例化的时候要关注创建的系统实例类型,其余时候都只需要对着抽象框架的接口来编程。
除此以外,工厂方法有两个注意事项:
工厂方法可以为子类提供一个扩展功能的钩子:通过在AbstractCreator的抽象工厂方法内提供一个缺省的实现,可以扩展工厂方法的功能,并且这个扩展的功能是可以根据不同的子类进行变化的(当然这个缺省的实现要通过super继承到子类的工厂方法中)。
连接系统外的类:只要是引入了对应工厂方法的类,相当于都整体纳入了你所设计系统的抽象体系当中,要注意在设计层面抽象的颗粒度。
Abstract Factory
Abstract Factory(抽象工厂设计模式)是对象创建型的,所以它并不是预先定义好系统的类框架,而是通过设计一个工厂体系来承担起系统的创建,每一个工厂里都包含了组成系统所必需组件的不同实现方式:
相比于工厂方法设计模式,抽象工厂设计模式面向需要更加灵活的系统设计,同时系统的各个组件未来预计会有大量优化迭代需求。比如文中举例的多视感用户界面应用,每一个应用实例内的组件都会有个性化的视觉需求,而且这种视觉需求需要不停的进行优化迭代,以期保持产品的竞争力。 这种场景里,再使用工厂方法在每一个子类中对每一个组件进行硬编码就显得过于耦合,也不利于后续各组件的优化迭代。
在具体实现时,首先对于系统的各个核心组件定义抽象接口(而不再是系统的抽象接口):
//核心组件Button的抽象接口
public interface Button {
void paintButton();
void clickButton();
...
}
//核心组件TextBox的抽象接口
public interface TextBox {
void paintTextBox();
...
}
随后分别去实现不同风格的组件实例:
public class MacOSButton implements Button {
private String ButtonName;
public MacOSButton(String ButtonName)
{
this.ButtonName = ButtonName;
}
public void paintButton()
{
System.out.println("MacOS Button " + ButtonName + " painted");
}
public void clickButton()
{
System.out.println("MacOS Button " + ButtonName + " clicked");
}
}
public class WindowsButton implements Button{
private String ButtonName;
public WindowsButton(String ButtonName)
{
this.ButtonName = ButtonName;
}
public void paintButton(){
System.out.println("Windows Button: " + ButtonName);
}
public void clickButton(){
System.out.println("Windows Button: " + ButtonName + " is clicked");
}
}
public class MacOSTextBox implements TextBox{
private String TextBoxName;
public MacOSTextBox(String TextBoxName)
{
this.TextBoxName = TextBoxName;
}
public void paintTextBox()
{
System.out.println("Paint MacOS TextBox: " + TextBoxName);
}
}
public class WindowsTextBox implements TextBox {
private String textBoxName;
public WindowsTextBox(String textBoxName)
{
this.textBoxName = textBoxName;
}
public void paintTextBox(){
System.out.println("Paint Windows TextBox: " + textBoxName);
}
}
随后定义相应的工厂体系,明确对于每一种风格的系统组件实现的组合:
//抽象工厂接口
public interface GUIFactory {
Button createButton(String ButtonName);
TextBox createTextBox(String TextBoxName);
}
//工厂实例1
public class MacOSFactory implements GUIFactory{
public Button createButton(String ButtonName)
{
return new MacOSButton(ButtonName);
}
public TextBox createTextBox(String TextBoxName)
{
return new MacOSTextBox(TextBoxName);
}
}
//工厂实例2
public class WindoxsFactory implements GUIFactory{
public Button createButton(String ButtonName){
return new WindowsButton(ButtonName);
}
public TextBox createTextBox(String TextBoxName){
return new WindowsTextBox(TextBoxName);
}
}
在客户端具体实现时可以直接通过统一的抽象方法来创建相应的组件,实现应用开发者接口调用的无感化:
public class Application {
public static void main(String[] args) {
GUIFactory macOSFactory = new MacOSFactory();
Button buttonA = macOSFactory.createButton("ButtonA");//工厂方法无需具像化到特定组件类型
TextBox textBoxA = macOSFactory.createTextBox("TextBoxA");//工厂方法无需具像化到特定组件类型
buttonA.paintButton();
textBoxA.paintTextBox();
}
}
优点
其实前文也说明了,抽象工厂设计模式抛弃了系统类的设计,将系统打散成了各个核心的组件来单独设计,最后通过工厂方法将对应的组件实例串起来。可以看出,这个设计模式更加的灵活,体现在一下几个方面:
- 使得产品系列的切换变得非常容易:基于上面的案例,可以更进一步在应用类中设置一个应用的创建方法,将工厂类作为输入,就可以实现只要一个工厂方法就可以完成一套系统所有组件的部署,只要改变一个工厂方法,整个应用的所有组件就会立刻完成变化。
- 有利于产品的一致性:同一个工厂方法定义了所有组件的一套版本,不会出现不同版本冲突的问题。
缺点:
- 难以支持新的组件:新的组件首先要在抽象工厂方法中定义创建接口,然后在每一个具体的工厂方法中分别实现,所需要的工作量比较大。
Builder模式
Builder模式侧重于实现系统的创建流程和具体的系统实例分离,也就是可以实现用一套创建流程来创建多种系统实例。细心的朋友们可以发现,这个功能Abstract Factory也可以实现,其实整体来看这两种方法非常的相似,都是尝试用一层抽象来统一不同系统实例的创建,但是两者存在一些差异,这个放到最后再说。
从这张架构图就可以看出来builder模式和Abstract Factory的相似性之高(Director这个角色在Abstract Factory中也完全可以设置,就是其优点第一点中所提的应用类中一个应用的创建方法)。而且Builder的设计与Factory也非常的类似,我把文中两个关于Maze的C++抽象类贴出来,大家可以对比一下:
首先对于各个组件的构建没有太大的区别,只不过Factory提供了缺省的实现,并且提供了公有的构造器(其实我觉得用protected未尝不可,这是一个抽象类,也不应该被子类之外来调用,大家批评指正)。
唯一MazeBuilder多的是GetMaze()这个方法,他会返回一个完整的Maze对象,这是Factory所没有的,这就引出了这两者最主要的区别,Builder实例中是带着系统实例的,但是Factory实例中没有,它只是单纯的工厂方法集合。 这就决定了他们创建系统实例的逻辑是不一样的,如下对比所示
factory方法是先创建maze实例,然后通过其他的工厂方法去为这个系统实例添加各个组件,而builder模式是一步一步的构建各个组件,最终通过get方法获取系统实例(其实再往下看一层,builder也是先创建系统实例,但是在面向应用开发接口的具体实现这一层(CreateMaze的方法实现),他是最后才获得系统实例,所以如果要说本质的区别,那就是builder相对于abstract factory再抽象了一层吧,至于这一层的抽象有没有意义,我觉得有吧,至少看着更简洁了一点,就像老马的猛禽1和猛禽3)。
知道了这个区别以后就不难理解builder模式的几个优势:
- 面向应用开发人员将系统的创建流程和具体的系统实例分离(这一点Abstract Factory也可以做到)
- 可以使得应用开发人员对于系统实例的创建过程进行更加精确的控制,并且这个控制相对于其他的创建模式更加简洁
Prototype模式
Prototype(原型)模式的核心就是用原型实例指定创建对象的种类,并且通过拷贝的方式创建这些原型新的对象。
原型模式主要应用于系统的各组件需要保持一致的场景,通过克隆的方式高效的保证所有组件都是一致的。
书中举了一个乐谱编辑器的例子,这个编辑器的主要处理对象当然是乐谱,以及在乐谱上的各种音符(以全音符和二分音符为例),这三个东西都是有标准的不会因为不同的乐谱对象而改变。所以适合使用原型模式。
首先定义这三个实现类及其接口,可以看到他们都实现了clone方法(建议还是要自定义一个接口,而不要直接使用Clonable接口,应用研发人员不易看懂):
//组件接口
public interface Graphic {
void draw(Position position);
Graphic clone();
}
//乐谱实现类
public class Staff implements Graphic {
public void draw(Position position){
System.out.println("Drawing staff at line "+position.getLineCount()+", column "+position.getColumnCount());
}
public Graphic clone(){
try {
return (Graphic) super.clone();
} catch (Exception e) {
throw new AssertionError("Clone not supported");
}
}
}
//半分音符实现类
public class HalfNote implements Graphic{
public void draw(Position position){
System.out.println("Drawing HalfNote at line "+position.getLineCount()+", column "+position.getColumnCount());
}
public Graphic clone(){
try {
return (Graphic) super.clone();
} catch (Exception e) {
throw new AssertionError("Clone not supported");
}
}
}
//全音符实现类
public class WholeNote implements Graphic {
public void draw(Position position){
System.out.println("Drawing WholeNote at line "+position.getLineCount()+", column "+position.getColumnCount());
}
public Graphic clone(){
try {
return (Graphic) super.clone();
} catch (Exception e) {
throw new AssertionError("Clone not supported");
}
}
}
然后可以直接定义一个工厂方法,通过已经实现的组件实例作为输入来调用他们的clone方法,达到创建的目的:
public class GraphicCreateFactory{
private Staff staff;
private WholeNote wholeNote;
private HalfNote halfNote;
public GraphicCreateFactory(Staff staff, WholeNote wholeNote, HalfNote halfNote){
this.staff = staff;
this.wholeNote = wholeNote;
this.halfNote = halfNote;
};
public Staff createStaff(){
return staff.clone();
}
public WholeNote createWholeNote(){
return wholeNote.clone();
}
public HalfNote createHalfNote(){
return halfNote.clone();
}
}
所以客户端可以预先创建原型后,作为参数调用相应的创建方法。
Singleton模式
Singleton(单例)模式在之前Effective Java中也介绍过,就是保证一个类仅有一个实例,并且提供一个访问它的全局访问点。它要实现的目的其实和Prototype有些类似,就是要保证组件的唯一性。
下面的单例模式设计可以根据环境变量type的值来定向实例化适当的MazeFactory子类。
public class MazeFactory {
private static MazeFactory instance;
protected MazeFactory(){};
public static MazeFactory getInstance(String type){
if(instance == null){
synchronized(MazeFactory.class){
if (instance==null) {
if (type.equals("Standard")) {
instance = new MazeFactory();
}
if (type.equals("Bombed")) {
instance = new BombMazeFactory();
}
}
}
}
return instance;
}
public Maze createMaze(){
return new Maze();
}
public Room createRoom(int roomNo){
return new Room(roomNo);
}
public Wall createWall(){
return new Wall();
}
public Door createDoor(Room room1, Room room2){
return new Door(room1, room2);
}
}
还有就是单例的实现方式要注意并发的访问设计,这部分在Effective Java学习笔记--单例(Singleton)及其属性的增强有涉及,这里就不展开了。