Design Pattern/设计模式: Part III

Author Avatar
Cesare (MINGJU LI) Feb 01, 2020

设计模式以及综述

设计模式对于一名软件开发工程师来说可谓是必备的技能和知识,然而在程序员社区中也充斥着不少对于设计模式的反对之音。有人认为设计模式的知识对于工程开发至关重要,但也有许多经验丰富的程序员认为设计模式应当是长期的软件开发的积累所留下的不可名状的抽象艺术,对于市面上不少介绍设计模式的书籍嗤之以鼻。

对于刚入门的软件开发工程师而言,你若问他(包括现在的我自己)“我们为什么要使用工厂模式?使用工厂模式能够带来什么好处?”之类的问题,他也是很难回答的。所以盲目的掌握设计模式中那些拗口的名称,或者干巴巴的“方便继承”之类的描述是没有意义的。

这一系列的博客的目的并不是一蹴而就的讲解全部的设计模式知识,这样的知识没有日积月累的开发经验是很难去对其有深入的体会的。我只是希望能在这一系列的博客中,借助《图解设计模式》一书,跟随书中的顺序对其中的设计模式知识进行某种程度的理解和总结,以至于能够对诸君有所启迪,开发出更加优美和友好的代码。

我的能力有限,若是文中出现什么谬误也请诸君不吝赐教,提前感谢。

《图解设计模式》一书的作者是结城浩先生,全书共计十个部分,预计会分为十篇博客进行介绍。书中的代码为java所写,虽然java并不是我比较擅长的代码,但是为了不引起错误贻笑大方,所以还是按照书中的java进行讨论。本博客介绍其中的第三部分:生成实例。因为每一章节中代码都占据了比较多的部分,为了更好地理解相关概念,建议阅读代码优先从Main.java的部分开始。

Chapter 5: Singleton模式–只有一个实例

在软件工程中,我们经常会希望在整个程序中确保某些实例有且只有一个。当然了,我们可以寄希望于我们在编程中只调用了一次new MyClass()以达到只生成一个实例的目的。显然这样的方法是不够保险的,我们对于这个实例有如下的要求

确保任何情况下都绝对只有一个实例

那么像这样的模式,我们可以称之为Singleton模式。这里因为这个模式的概念比较简单,我们就不使用例子而直接查看代码。

// Singleton.java
public class Singleton{
    private static Singleton singleton = new Singleton();
    private Singleton(){
        System.out.println("The Singleton is generated");
    }
    public static Singleton getInstance(){
        return singleton;
    }
}
// 这里的Singleton可以说是最简单的Singleton模型了,其中private的singleton属性和Singleton构造函数都是必须的,而getInstance可以根据实际的需要去进行改造。这里蕴含的这里很简单:Singleton类的构造函数是private的,那么从外部则无法调用此构造函数,即不能进行new Singleton()这样的调用,否则会出现编译错误。而在加载这个类的时候,static字段的singleton被初始化为Singleton类的实例,也是整个程序中有且只有的唯一的实例。

Static关键字:仅仅在该类被加载时进行一次。

// 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 and obj2 are same instance");
        }else{
            System.out.println("obj1 and obj2 are NOT same instance");
        }
        System.out.println("End");
    }
}

Main.java的输出如下所示

Start
The Singleton is generated
obj1 and obj2 are same instance
End

注意程序的输出,在输出“Start”之后就输出了“The Singleton is generated”,在第一次调用getInstance方法时,Singleton类被初始化,也是这个时候,static字段singleton被初始化,生成了唯一的一个实例。

当然,刚刚这一例子仅仅是Singleton模式在java下最简单的一个实现,事实上在多线程的情况下,或者在其他的语言的实现中,Singleton模式都可能会有一些差异,但是就像是在最开始强调的,这一系列的博客的目的并非是上手一门语言,而是去理解为什么要这么做、以及如果要这么做的话可以通过限定private之类的方式进行。

Chapter 6: Prototype模式–通过复制生成实例

生成实例?我们脑海里第一浮现的应该是new MyClass()这样的方式,然而在某些情况下,我们需要在不指定类名的前提下生成实例,一些比较常见的情况下被列举如下:

  1. 对象种类繁多,无法将其整合到一个类中
  2. 难以根据类来生成实例
  3. 解耦框架与生成的实例:我们希望要生成的实例和这里负责生成的框架是低耦合的

对于上述的若干种情况,我们可以通过Prototype模式来解决这个问题,根据实例来生成新的实例。在java语言中,我们可以使用clone来创建实例的副本,在这一章节我们就学习clone方法和Cloneable接口的使用方法。

例:我们希望设计一段程序,输入字符串,输出为方框中的字符串或者加了下划线的字符串。程序的结构如下所示

// Product.java:Product接口
package framework;
public interface Product extends Cloneable{
    public abstract void use(String s);
    public abstract Product createClone();
}
//这里Product继承了java.lang.Cloneable接口,实现了此接口的实例可以调用clone方法来自动复制实例
//use方法是“使用”的方法,具体怎么使用交给子类进行实现
// createClone方法是用于复制实例的方法
// Manager.java: Manager类
package framework;
import java.util.*;

public class Manager{
    private HashMap showcase = new Hashmap();
    //showcase属性保存了名字和实例之间的对应关系
    public void register(String name, Product proto){
        showcase.put(name, proto);
        //尽管我们还不知道proto是属于哪一个类的实例,但是我们确定一点,那就是每一个proto都实现了Product接口
        //在这一步将接收到的一对name和proto放进Hashmap中
    }
    public Product create(String protoname){
        Product p = (Product)showcase.get(protoname);
        return p.createClone;
    }
}
//这里Manager类使用Product接口来复制实例(Product接口是Manager类和其他的具体类之间的桥梁),但不管是Manager.java还是Product.java中都没有出现过关于MessageBox类和UnderlinePen类的名字,这就意味着这些组件之间的耦合关系是相当松弛的,我们可以独立的修改Product/Manager而不受MessageBox/UnderlinePen的影响。
// MessageBox.java: MessageBox类,实现了Product接口

import framework.*;

public class MessageBox implements Product{
    private char decochar;
    //使用decochar字符来组成MessageBox

    public MessageBox(char decochar){
        this.decochar = decochar;
    }

    public void use(String s){
        int length = s.getBytes().length;
        for(int i=0;i<length+4;i++){
            System.out.print(decochar);
        }
        System.out.println("");
        System.out.println(decochar+" "+s+" "+decochar);
        for(int i=0;i<length+4;i++){
            System.out.print(decochar);
        }
        System.out.println("");
    }
    //使用use方法来输出特定格式

    public Product createClone(){
        Product p = null;
        try{
            p = (Product)clone();
            //clone()是Java中定义的方法,用于复制自身。之所以可以调用clone(),是因为该类实现了java.lang.Cloneable接口(Product接口继承了Cloneable接口),如果没有实现这个接口会抛出CloneNotSupportedException异常
            //注意,java.lang.Cloneale接口只是告诉程序可以调用clone(),它自身并没有定义任何方法,只有类自己(或它的子类)能够调用java中的clone方法,当其他类要求复制实例时需要调用createClone这样的方阿飞,然后在内部调用clone方法
        }catch(CloneNotSupportedException e){
            e.printStackTrace();
        }
        return p;
    }
}
// UnderlinePen.java: UnderlinePen类
import framework.*;

public class UnderlinePen implements Product{
    private char ulchar;
    public UnderlineCharPen(char ulchar){
        this.ulchar = ulchar;
    }
    public void use(String s){
        int length = s.getBytes().length;
        System.out.println(s);
        for(int i=0;i<length;i++){
            System.out.print(ulchar);
        }
        System.out.println("");
    }
    public Product createClone(){
        Product p=null;
        try{
            p=(Product)clone();
        }catch(CloneNotSupportedException e){
            e.printStackTrace();
        }
        return p;
    }
}
// Main.java
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);
        //将prototype储存起来

        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");
    }
}

登场角色

  1. Prototype:由Product接口扮演,负责定义复制现有实例来生成新实例的方法
  2. ConcretePrototype:由MessageBox类和UnderlinePen类扮演,负责实现复制现有实例来生成新实例的方法
  3. Client:由Manager类扮演,负责使用复制现有实例来生成新实例的方法来生成新的实例

不难看出,Prototype模式的核心思想时非常朴素的:有一些实例通过类名声明起来很困难?那么我们只需要将这些实例,每一个实例都保存一个Prototype,等到需要声明他们的时候直接根据这些已经保存的Prototype就可以很方便的生成新的实例。

回顾本章开头,在上述示例中其实我们模拟了对象种类繁多以至于无法将他们整合到一个类的这种情况,在示例程序我们一共生成了3种prototype,当然在实际的工程应用中我们可以创造更多的prototype。而试想如果我们为了每一个prototype都去编写一个类,那么类的数量将会非常庞大。同时注意,我们使用了这样的设计模式,也带来了低耦合性的好处,在Manager类的create方法中,我们没有使用类名,取而代之的使用字符串为生成的实例命名,通过这样的方式,框架和类名之间的耦合关系就被解开了。

耦合性与类的使用: 在代码中使用类的名字时非常普遍的(这不是废话吗!),但是一旦在代码中出现要使用的类的名字,这段代码就无法与这个类分开,也就无法实现复用。当多个类必须紧密结合的时候,代码中出现这些类的名字是没有问题的,但是如果那些需要被独立出来作为组件复用的类的名字出现在代码中的时候,就可能引起麻烦。在每一个类的使用的时候,我们也不妨谨慎的去思考一下,是否存在更好的方法呢?

Chapter 7: Builder模式–组装复杂的实例

这一章节中要学习的是用于组装具有复杂结构实例的Builder模式

例:我们希望设计一段文档,使编写出的文档具有{一个标题+几个字符串+条目项目}的结构,我们在抽象的Builder类中声明抽象方法,然后Director类使用Builder类中的方法去编写一个具体的文档,而Builder类的子类(TextBuilder类/HTMLBuilder类)决定了用来编写文档的具体处理,也就是说,Director类使用TextBuilder类时可以编写纯文本文档,而使用HTMLBuilder类时可以编写HTML文档。

// Builder.java: Builder类是一个声明了编写文档的方法的抽象类,含有编写文档所需要的方法
public abstract class Builder{
    public abstract void makeTitle(String title);
    public abstract void makeString(String str);
    public abstract void makeItems(String[] items);
    public abstract void close();
}
// Director.java: Director类使用Builder类中的方法来编写文档
public class Director{
    private Builder builder;
    public Director(Builder builder){
        this.builder = builder;
    }
    public void construct(){
        builder.makeTitle("This is a title");
        builder.makeString("This is the string");
        builder.makeItems(new String[]{"Item1", "Item2"});
        builder.makeString("This is another string");
        builder.makeItems(new String[]{"Item3", "Item4"});
        builder.close();
    }
}
// 注意,在Director.java中,我们定义了一个复杂的流程(组装了一个复杂的结构),但是对于Builder来说,不管是TextBuilder还是HTMLBuilder,这这个流程都是相同的(定义在Director.java之中)
// TextBuilder.java
public class TextBuilder extends Builder{
    private StringBuffer buffer = new StringBuffer();
    public void makeTitle(String title){
        buffer.append("==================\n");
        buffer.append("+"+titile+"+\n");
        buffer.append("\n");
    }
    public void makeString(String str){
        buffer.append(str+"\n");
        buffer.append("\n");
    }
    public void makeItems(String[] items){
        for(int i=0;i<items.length;i++){
            buffer.append("->"+items[i]+"\n");
        }
        buffer.append("\n");
    }
    public void close(){
        buffer.append("==================\n");
    }
    public String getResult(){
        return buffer.toString();
    }
}
// HTMLBuilder.java
import java.io.*;

public class HTMLBuilder extends Builder{
    private String filename;
    private PrintWriter writer;
    public void makeTitle(String title){
        filename = title+".html";
        try{
            writer = new PrinterWriter(new FileWriter(filename));
        }catch(IOException e){
            e.printStackTrace();
        }
        writer.println("<html><head><title>"+title+"</title></head><body>");
        writer.println("<hl>"+title+"</hl>");
    }
    public void makeString(String str){
        writer.println("<p>"+str+"</p>");
    }
    public void makeItems(String[] items){
        writer.println("<ul>");
        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;
    }
}

注意这里,TextBuilder的getResult会返回编写好的文本的全部内容,而HTMLBuilder会返回编写好的html文档标题

// Main.java
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 result = htmlbuilder.getResult();
            System.out.println(result+"HTML file has been compiled");
        }else{
            usage();
            System.exit(0);
        }
    }
    public static void usage(){
        System.out.println("Usage: java Main plain   ---   compiled a text file");
        System.out.println("Usage: java Main html   ---   compiled a html file");
    }
}

登场角色

  1. Builder:由Builder类扮演此角色,声明了准备用于实例的抽象方法
  2. ConcreteBuilder:由TextBuilder类/HTMLBuilder类扮演此角色,定义了实例实际的方法
  3. Director:由Director类扮演此角色,负责使用Builder角色的接口来生成实例,而不依赖于ConcreteBuilder,同时为了达到这一目的,它只调用Builder角色中被定义的方法
  4. Client:由Main类扮演此角色,使用Builder模式

注意,在本例子中,Main类并不知道Builder类,只是调用Director类的construct()。而Director类知道Builder类,调用Builder类的方法来编写文档,却不知道真正使用的是TextBuilder类还是HTMLBuilder类。正是因为不知道所以才能够相互替换,而相互替换就可以增强组件的复用性,具有更高的价值。

Chapter 8: Abstract Factory模式—将关联零件组装成产品

众所周知,Factory是将零件组成产品的地方,而在这一章节我们要讨论的Abstract Factory则是将抽象零件组装成抽象产品的地方。对于抽象工厂来说,它不关心零件的具体实现,只关心接口,我们进使用该接口将零件组装成为产品。

在Template Method和Builder模式中,子类负责方法的具体实现,这一点在Abstract Factory模式中也是一样的。

例:我们希望设计一个程序,可以将带有层次关系的连接的集合制作成HTML文件。程序的结构如下所示

// Item.java: Item类
package factory
public abstract class Item{
    protected String caption;
    public Item(String caption){
        this.caption = caption;
    }
    public abstract String makeHTML();
}
// 是Link类和Tray类的父类
// Link.java: Link类
// 抽象的零件之一:表示一对链接名称和url,如caption="百度",url="www.baidu.com"
package factory;
public abstract class Link extends Item{
    protected String url;
    public Link(String caption, String url){
        super(caption);
        this.url = url;
    }
}
// 注意这里,因为Link类没有实现Item类中的抽象的makeHTML(),所以Link类依然是一个抽象类
// ListLink.java: ListLink类,必须实现父类中声明的makeHTML()
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";
    }
}
// Tray.java: Tray类
// 抽象的零件之二:是一个含有多个Link类和Tray类的容器,如实例化一个Tray(当然了抽象类不能实例化变量,这里仅仅是为了让解释更简明扼要)并使其caption="搜索引擎",那么这个Tray下可以含有若干个Tray(如"国内搜索引擎/国外搜索引擎"等),也可以含有若干个Link(如"百度/搜狗"等)
package factory
import java.util.ArrayList;

public abstract class Tray extends Item{
    protected ArrayList tray = new ArrayList();
    public Tray(String caption){
        super(caption);
    }
    public void add(Item item){
        tray.add(item);
    }
}
//ListTray.java: 如同ListLink一样,ListTray也必须提供一个makeHTML()的实现
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();
        buffer.append("<li>\n");
        buffer.append(caption+"\n");
        buffer.append("<ul>\n");

        Iterator it = tray.iterator();
        // 注意,tray是在父类Tray.java中定义的

        while(it.hasNext()){
            Item item = (Item)it.next();
            buffer.append(item.makeHTML());
        }

        buffer.append("</ul>\n");
        buffer.append("</li>\n");
        return buffer.toString();
    }
}

这里我们可以看到,ListTray.java这一部分的程序其实是很多个章节以来最能体现面向对象编程这一思想的程序。在while循环中,我们不关心每个Item的实例究竟是ListTray还是ListLink,我们只调用了item.makeHTML(),具体item进行了什么样的处理也只有实例才知道,这就是面向对象编程的优点。

// Page.java: 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;
    }
    public void add(Item item){
        content.add(item);
    }
    public void output(){
        try{
            String filename = title+".html";
            Writer writer = new FileWriter(filename);
            writer.write(this.makeHTML());
            writer.close();
            System.out.println(filename+"COMPILED FINISHED");
        }catch(IOException e){
            e.printStackTrace();
        }
    }
    public abstract String makeHTML();
}
// ListPage.java: 具体的产品
package listfactory;
import factory.*;
iport java.util.Iterator;

public class ListPage extends Page{
    public ListPage(String title, String author){
        super(title, author);
    }
    public String makeHTML(){
        StringBuffer buffer = new StringBuffer();
        buffer.append("<html><head><title>"+title+"</title></head>\n");
        buffer.append("<body>\n");
        buffer.append("<h1>"+title+"</h1>\n");
        buffer.append("<ul>\n");

        Iterator it = content.iterator();

        while(it.hasNext){
            Item item = (Item)it.next();
            buffer.append(item.makeHTML());
        }

        buffer.append("</ul>\n");
        buffer.append("<hr><address>"+author+"</hr></address>");
        buffer.append("</body></html>\n");

        return buffer.toString();
    }
}
// Factory.java
// 抽象的工厂
package factory

public abstract class Factory{
    public static Factory getFactory(String classname){
        Factory factory = null;
        try{
            factory = (Factory)Class.forName(classname).newInstance();
            // getFactory()通过调用Class类的forname方法来动态的读取类信息,接着使用newInstance()生成实例并返回给调用者
            // 我们使用这个程序的时候要使用 java Main listfactory.ListFactory的命令,listfactory.ListFactory会作为类的名称(也就是这里的classname)传递给getFactory()
        }catch(ClassNotFoundException e){
            System.err.println("CANNOT FIND CLASS: "+classname);
        }catch(Exception e){
            e.printStackTrace();
        }
        return factory;
    }

    public abstract Link createLink(String caption, String url);
    public abstract Tray createTray(String caption);
    public abstract Page createPage(String title, String author);
}
// ListFactory.java: ListFactory类,实现了Factory抽象类的createLink()/createTray()/createPage()
package listfactory;
import factory.*;

public class ListFactory extends Factory{
    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); 
    }
}
// Main.java
import factory.*;
public class Main{
    public static void main(String[] args){
        if(args.length!=1){
            System.out.println("UsageL 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 peopleNews = factory.createLink("人民日报", "http://www.people.com.cn/");
        Link peopleNews2 = factory.createLink("人民日报2", "http://www.people2.com.cn/");

        Link us_yahoo = factory.createLink("Yahoo_US", "http://www.yahoo.com");
        Link jp_yahoo = factory.createLink("Yahoo_JP", "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("NEWS");
        traynews.add(peopleNews);
        traynews.add(peopleNews2);

        Tray trayyahoo = factory.createTray("Yahoo!");
        trayyahoo.add(us_yahoo);
        trayyahoo.add(jp_yahoo);

        Tray traysearch = factory.createTray("SearchEngine");
        traysearch.add(trayyahoo);
        traysearch.add(excite);
        traysearch.add(google);

        Page page = factory.createPage("LinkPage", "John Doe");
        page.add(traynews);
        page.add(traysearch);
        page.output();
    }
}

扮演角色

  1. AbstractProduct:由Link类/Tray类/Page类扮演此角色,负责定义AbstractFactory角色所生产的抽象零件和产品的接口
  2. AbstractFactory:由Factory类扮演此角色,负责定义用于生产抽象产品的接口,如createLink()/createTray()/createPage()
  3. Client:由Main类扮演此角色,负责调用AbstractFactory角色和AbstractFactory角色的接口来进行工作,对于具体的Product和Factory一无所知
  4. ConcreteProduct:由listfactory包中的ListLink类/ListTray类/ListPage类扮演此角色,负责实现AbstractProduct角色的接口
  5. ConcreteFactory:由listfactory包中的ListFactory类扮演此角色,负责实现AbstractFactory角色的接口,也就是createLink()/createTray()/createPage()

要点分析,这里我们花费了巨大的力气来使用这个Abstract Factory模式,就是将Factory的概念抽象起来。在Abstract Factory中增加具体的工厂是非常容易的,只需要编写一个新的newlistfactory包,实现AbstractProduct角色和AbstractFactory的角色的接口即可。但是同时我们也需要注意一点,那就是如果我们想要在Abstract Factory中增加新的零件(如我们希望增加表示图像的Picture组件,那么我们在listfactory包中,我们需要在ListFactory中加入createPicture(),同时我们还需要在此包中增加一个ListPicture类。而同时我们也应该在factory包中增加一个Picture类表示新的零件)。