设计模式 03 生成实例

作者 柚爸

对于面向对象的语言, 如何创建对象一直是一大学问, 这一章就是各种和创建对象有关的模式.

  1. Singleton 单例模式
  2. 练习
  3. Prototype 原型模式
  4. 练习
  5. Builder 建造者模式
  6. 练习
  7. Abstract Factory 抽象工厂
  8. 练习

Singleton 单例模式

单例模式可不简单的是我自己控制在程序里只生成一个对象就可以了, 而是要确保在任何情况下都仅有一个实例, 不同的程序用到这个实例, 都要是同一个.

单例模式在Java中依赖与语言的特性, 即构造器函数是可以有访问权限的. 凡是想要实现单例模式的类, 都要将其构造器设置为private, 然后在类中创建一个对象. 对外暴露一个API, 每次总是返回同一个对象.

单例模式从代码量来说是最少的设计模式, 所以也易于理解, 在很多地方都可以见到.

public class SingleTon {

    private SingleTon() {
        System.out.println("创建了一个单例的实例");
    }

    private static SingleTon singleTon = new SingleTon();

    public static SingleTon getInstance() {
        return singleTon;
    }
}

由于是单例, 只有一个类, 也只有一个实例, 因此可以用静态域来存储单例对象, 也用静态方法暴露即可.

在使用这个单例的时候, 不能创建, 只能通过静态方法获取引用, 不管获取多少次, 都是同样一个对象:

public class Main {

    public static void main(String[] args) {
        System.out.println("Start.");
        SingleTon singleTon1 = SingleTon.getInstance();
        SingleTon singleTon2 = SingleTon.getInstance();
        SingleTon singleTon3 = SingleTon.getInstance();

        System.out.println(singleTon1);
        System.out.println(singleTon2);
        System.out.println(singleTon3);
        System.out.println(singleTon1==singleTon2);
        System.out.println(singleTon1==singleTon3);
        System.out.println(singleTon3==singleTon1);

    }
}

获取了三次单例对象, 然后打印, 可以看到打印出了同一个内存地址. 默认的比较==其实也是比较内存地址, 都是true. 这说明单例模式成功了.

这里实际上在类初始化的时候, 第一次访问静态方法, 才会去加载, 所以创建类的那句话会显示在Start.之后, 这里也顺便复习了Java加载类的知识点.

单例模式是一个相当基础也应用广泛的设计模式, 很多其他的设计模式如抽象工厂, 创建者和原型等都(可以)使用到单例模式.

练习

习题5-1 修改成单例:

public class TicketMaker {

    private int ticket = 1000;

    public int getNextTicketNumber() {
        return ticket++;
    }

    //添加如下三个让类采用单例模式的方法
    private TicketMaker() {}

    private static TicketMaker ticketMaker = new TicketMaker();

    private static TicketMaker getInstance() {
        return ticketMaker;
    }
}

无需改动原来的域和方法, 只需要将构造器设置成私有, 然后添加上静态域和获取单例的静态方法即可.

习题5-2 指定数量的实例

这个思路也很简单, 内部可以用一个容器放着指定数量的实例, 然后通过静态方法按照索引来返回指定编号的实例:

public class Triple {

    //单例所需的静态域, 存放长度为3的Triple数组
    private static Triple[] triples = new Triple[3];

    //静态块来初始化, 创建并向数组中填充三个对象
    static {
        for (int i = 0; i < 3; i++) {
            triples[i] = new Triple(i);
        }
    }

    //为了持有编号加上的域
    private int number;

    //私有的构造器
    private Triple(int i) {
        this.number = i;
    }

    //按照索引返回对象的静态方法
    private static Triple getInstance(int index) {
        if (index < 0 || index > 2) {
            throw new RuntimeException("索引错误");
        }
        return triples[index];
    }
}

可见想把任何类变成单例或者类似的有限个数的类, 只需要先将构造器私有化, 再添加上持有对象的静态域和获取对象的静态方法就可以了.

练习5-3 纠错

public class FakeSingleton {

    private static FakeSingleton singleton = null;

    private FakeSingleton() {
        System.out.println("创建一个实例");
    }

    public static FakeSingleton getInstance() {
        if (singleton == null) {
            singleton = new FakeSingleton();
        }
        return singleton;
    }
}

这个例子为什么不是严格的单例, 是因为在调用getInstance()方法的时候, 如果是多线程程序, 很可能创建了对象之后还没有赋值就被打断, 由于非基本类型的赋值操作不是原子的.

于是可以出现线程1创建了一个new FakeSingleton(), 但是还没有赋值, 另外线程2也创建了一个new FakeSingleton()并且赋值, 然后使用这个单例的程序可能先获取了线程2创建的对象, 之后线程1赋值完之后, 使用单例的程序再去获取, 就获得了另外一个对象. 所以还是直接在类初始化的时候固定好静态变量比较好.

想要修改也很简单, 将getInstance()加上 synchronized 修饰即可, 或者严格按照单例模式, 在类初始化的时候就创建好 FakeSingleton() 对象.

Prototype 原型模式

原型模式, 我个人理解, 就是不使用 new 类名() 这种方式来生成实例, 而是根据现有的实例来生成实例.

乍一听好像有点奇怪, 不new 一个对象出来, 要如何生成新对象.

先看一下示例在回头过来谈谈理解, 这是一个先利用了模板设计模式的框架:

public interface Product extends Cloneable {

    void use(String s);

    Product createClone();
}

先是一个Product接口, 这个接口继承了Cloneable接口, 实现了该接口的类的对象可以调用.clone()方法来复制自己.

然后是Manager类, 这个类将使用Product接口类型来复制实例:

public class Manager {

    private HashMap<String, Product> showcase = new HashMap<>();

    public void register(String name, Product product) {
        showcase.put(name, product);
    }

    public Product create(String productName) {
        Product p = showcase.get(productName);
        return p.createClone();
    }
}

看了一下Manager类, 其主要作用是将一个对象注册到自己的容器里来, 如果外部有需要, 就用同名的产品复制出一个实例返回给外部, 而不是直接返回容器中的实例.

Manager和Product结合起来, 就是之前模板模式中提到的框架, 两个类互相有交互, 但没有使用任何具体实现类的类名.

从目前的框架来看, 实际上要实现的就是一个复制功能.来看看具体两个具体的Product实现类, 首先是MessageBox:

public class MessageBox implements Product {

    private char decochar;

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

    //没有什么实际意义的use, 仅仅为了实现
    @Override
    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();
    }


    @Override
    public Product createClone() {
        Product p = null;
        try{
            //clone()方法调用之后得到的是Object类型, 需要转型. 调用.clone()方法得到的是当前对象的复制
            p = (Product) clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return p;
    }
}

这里的关键是createClone()方法, 其中在自身上调用的 .clone() 方法要求类必须继承Cloneable接口, 我们已经在Product接口中继承了, 所以MessageBox类也是继承的.

如果不继承就调用, 会抛出CloneNotSupportedException, 所以要try-catch一下.

再来一个实现类 UnderlinePen:

public class UnderlinePen implements Product {

    private char decochar;

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

    //没有什么实际意义的use, 仅仅为了实现
    @Override
    public void use(String s) {
        int length = s.getBytes().length;
        System.out.println("\"" + s + "\"");
        System.out.print(" ");
        for (int i = 0; i < length; i++) {
            System.out.print(decochar);
        }
        System.out.println();
    }


    @Override
    public Product createClone() {
        Product p = null;
        try{
            //clone()方法调用之后得到的是Object类型, 需要转型. 调用.clone()方法得到的是当前对象的复制
            p = (Product) clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return p;
    }
}

这两个类只有.use()方法是不同的. 现在就可以通过Manager类, 来复制出对应的类了. 看一下测试代码:

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对象中
        manager.register("strong", upen);
        manager.register("warning", mbox);
        manager.register("slash", sbox);

        //根据strong名称创建对象, 得到的是什么呢, 是upen吗?
        Product p1 = manager.create("strong");
        System.out.println(p1);
        System.out.println(upen);
        System.out.println(p1 == upen);
        //行为一样吗?
        p1.use("saner");
        upen.use("saner");

        Product p2 = manager.create("warning");
        p2.use("owl");

        Product p3 = manager.create("slash");
        p2.use("sixtuan");
    }
}

通过测试, 可以发现我们创建了一个制造不同实例的框架. 利用了Java 的clone机制, 我们在Manager中注册好了所有需要生成的对象之后, 通过create()加上指定的参数, 就创建出来了不同的对象.

在这里我们把Product类型叫做原型, MessageBox, UnderlinePen等原型的实现类, 叫做具体原型, 负责复制现有实例然后返回新实例.

Manager类被称为Client – 使用者, 程序的其他部分, 通过使用者来获取新的实例并使用. 这里每调用一次manager.create()并传入一个标识具体原型的参数, 就会根据具体原型创建一个新类型返回. 这个类型能够在Product接口之下正常工作.

看到这里终于有点明白原型模式了, 与通过一个类直接创建对象不同, 原型模式是通过了一个中介(Manager)来有选择的在多个实现了同一个接口的具体原型中间做选择, 去创建新的实例.

前边工厂模式是一种具体类型创建一个具体工厂. 而这里针对不同的具体原型, 一个代理就可以根据参数来创建不同的具体对象. 由于框架规定好了操作, 因此具体原型对框架的执行完全没有影响.

确实有意思啊设计模式.

这里额外要注意的是利用了Java 的 .clone() 特性, .clone()是在Object里定义的方法, 但只有使用了Cloneable标记接口的对象才能调用. Cloneable就和序列化接口一样都是一种标记接口.

而且clone只是浅复制, 会复制对象的域中的值和引用, 对于对象持有的对象, 不会再进行复制, 如果要深复制, 需要自行编写针对某个对象的.clone()方法, 不过要记得先要调用父类的super.clone()方法.

关于浅复制可以简单的测试一下, 给MessageBox类加上一个数组引用和打印数组的方法:

public class MessageBox implements Product {

    ......

    private List<Integer> list = new ArrayList<>();

    public void showList() {
        System.out.println(list);
    }

    ......
}

然后进行测试:

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对象中
        manager.register("strong", upen);
        manager.register("warning", mbox);
        manager.register("slash", sbox);

        //测试浅复制, 都复制自同一个具体原型
        Product messagebox1 = manager.create("warning");
        Product messagebox2 = manager.create("warning");

        //给具体原型put一个10
        mbox.put(10);
        //打印出来发现所有克隆出来的对象的数组里都有了10
        mbox.showList();
        ((MessageBox) messagebox1).showList();
        ((MessageBox) messagebox2).showList();

        //给一个对象再put一个20
        mbox.put(20);
        //再打印, 发现所有对象的数组也都变成了[10, 20]
        mbox.showList();
        ((MessageBox) messagebox1).showList();
        ((MessageBox) messagebox2).showList();
    }
}

另外我这里也细细体会了一下书里说的, 就是摆脱类名的束缚, 使用字符串或者其他标记来生成具体对象, 确实可以有效的提高解耦程度. 可以无需知道类名就可以进行创建对象, 极大的提高了灵活程度.

以后如果是一批实现了同一个接口的对象, 具体实现不同, 都可以考虑使用这种原型模式来批量生产, 而不是每次都一个一个创建实例.

练习

练习6-1 让两个类共用的方法

我想了一下, 站在今天的角度来说, 可能有如下方案:

  1. 编写一个父类具备该方法, 继承即可.
  2. 在接口中编写默认方法

不过我尝试在接口中编写默认方法如下:

public interface Product extends Cloneable {

    void use(String s);

    default Product createClone() {
        Product p = null;
        try {
            //clone()方法调用之后得到的是Object类型, 需要转型. 调用.clone()方法得到的是当前对象的复制
            p = (Product) clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return p;
    }
}

是无法通过编译的, 这是因为接口不继承Object, 因此找不到clone()方法. 看来还是只能采取继承的方式才行.

练习 6-2 java.lang.Object 类实现了 java.lang.Cloneable 接口了吗?

这个问题有点意思, Object中首先实现了clone()方法, 如果clone()方法会检查是不是具备Cloneable接口从而抛出异常. 所以Object肯定没有实现Cloneable接口, 因为其他类都继承自Object, 如果实现的话, 永远也不会抛出异常了.

Builder 建造者模式

建造者模式用于创建一个比较复杂的实例, 并不能简单的new一下就得到需要的实例, 而是需要针对一个实例调用各种建造方法, 全部完毕之后, 再调用一个方法, 就可以获得建造成功的实例.

建造者模式也通常用于配置文件的创立, 其本质就是不断通过API改变对象的状态, 最后创建完毕.

建造者模式的核心就是一个建造者接口, 然后还有一个使用这个建造者接口对象的监工对象. 需要创建的时候, 通过监工对象去调用就可以了. 本质上也不需要监工对象, 直接使用建造者也可以.

建造者的接口如下:

public interface Builder {

    void makeTitle(String title);

    void makeString(String string);

    void makeItems(String[] items);

    void close();
}

这个接口意图在创建一篇带有标题, 正文和项目的文章, 可以是文本(字符串格式), 也可以是HTML格式, 就根据文本和HTML来编写接口的实现类:

public class TextBuilder implements Builder {

    private StringBuffer result = new StringBuffer();

    @Override
    public void makeTitle(String title) {
        result.append("===========================================\n").append("[").append(title).append("]\n").append("\n");
    }

    @Override
    public void makeString(String string) {
        result.append("*").append(string).append("\n").append("\n");
    }

    @Override
    public void makeItems(String[] items) {
        for (String item : items) {
            result.append(" . ").append(item).append("\n");
        }
    }

    @Override
    public void close() {
        result.append("===========================================\n");
    }

    public String getResult() {
        return result.toString();
    }
}

这里很有意思的是, 作者原来的代码是反复调用StringBuffer, 但现在新的Java里StringBuffer就是Builder模式, 只不过返回自身对象, 所以可以链式调用.

通过阅读代码可以知道, 作者这里对于调用方法的顺序还是有要求的, 否则文章会乱掉, 其实如果开发一个成熟的类的话, 还需要加上各种标记来标志建造的阶段等等. 这里因为是一个例子, 就简化了.

创建HTML文件的代码如下:

public class HTMLBuilder implements Builder {

    private String filename;

    private PrintWriter writer;

    @Override
    public void makeTitle(String title) {
        filename = title + ".html";
        try {
            writer = new PrintWriter(filename);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        writer.println("<html><head><title>" + title + "</title></head><body>");
        writer.println("<h1>" + title + "</h1>");
    }

    @Override
    public void makeString(String string) {
        writer.println("<p>" + string + "</p>");
    }

    @Override
    public void makeItems(String[] items) {
        writer.println("<ol>");
        for (String item : items) {
            writer.println("<li>" + item + "</li>");
        }
        writer.println("</ol>");
    }

    @Override
    public void close() {
        writer.println("</body></html>");
        writer.close();
    }

    public String getFilename() {
        return filename;
    }
}

代码都很简单, 作者就是为了演示. 然后是使用建造者的Director类:

public class Director {

    private Builder builder;

    public Director(Builder builder) {
        this.builder = builder;
    }

    public void construct(String title, String string, String[] items) {
        builder.makeTitle(title);
        builder.makeString(string);
        builder.makeItems(items);
        builder.close();
    }
}

这样我们可以解耦, 给Director传入具体的建造者对象就可以进行创建, 在Main类中进行测试:

public class Main {

    public static void main(String[] args) {
        String title = "设计模式";
        String string = "23种设计模式";
        String[] items = new String[]{"适配器模式", "建造者模式", "工厂模式", "迭代器模式", "原型模式", "单例模式", "模板模式"};

        HTMLBuilder htmlBuilder = new HTMLBuilder();
        new Director(htmlBuilder).construct(title, string, items);
        System.out.println(htmlBuilder.getFilename());

        TextBuilder textBuilder = new TextBuilder();
        new Director(textBuilder).construct(title, string, items);
        System.out.println(textBuilder.getResult());
    }
}

可以看到监工模式其实没什么用, 只是套一层壳, 直接也可以使用建造者对象.

练习

其实第一题已经做掉了, 作者原书的Builder是抽象类, 我给直接修改成了接口. 因为遵循纯粹的抽象类, 不如修改成接口. 模板模式的抽象类才有设计模式上的价值.

习题 7-2 就是我之前提出的思考, 也就是建造者调用方法的顺序. 代码也很简单, 加一个标志位就可以了:

import java.io.FileNotFoundException;
import java.io.PrintWriter;

public class HTMLBuilderTitleFirst implements Builder {

    private boolean titled = false;

    private String filename;

    private PrintWriter writer;

    @Override
    public void makeTitle(String title) {

        //添加判断
        if (titled) {
            System.out.println("无法重复创建标题");
            return;
        }

        filename = title + ".html";
        try {
            writer = new PrintWriter(filename);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        writer.println("<html><head><title>" + title + "</title></head><body>");
        writer.println("<h1>" + title + "</h1>");

        //成功写入标题之后设置标志位
        titled = true;
    }

    @Override
    public void makeString(String string) {

        //判断标志位的代码
        if (!titled) {
            System.out.println("必须先创建标题");
            return;
        }

        writer.println("<p>" + string + "</p>");
    }

    @Override
    public void makeItems(String[] items) {

        //判断标志位的代码
        if (!titled) {
            System.out.println("必须先创建标题");
            return;
        }

        writer.println("<ol>");
        for (String item : items) {
            writer.println("<li>" + item + "</li>");
        }
        writer.println("</ol>");
    }

    @Override
    public void close() {

        //判断标志位的代码
        if (!titled) {
            System.out.println("必须先创建标题");
            return;
        }

        writer.println("</body></html>");
        writer.close();
    }

    public String getFilename() {
        return filename;
    }
}

习题 7-3 就略过了, 就是纯粹的再新编写一个类.

习题 7-4 , 如果连续用String拼接, 每个字符串都会创建一个对象, 用StringBuffer的话效率会高很多.

Abstract Factory 抽象工厂

之前学习了工厂模式, 就是将模板模式用在创建实例上, 有一个Factory抽象类和一个Product接口, 用不同的Factory实现类, 就可以制造出不同的, 但是都符合Product接口规范的产品.

抽象工厂在这个基础上更进一步, 用于组装复杂的对象. 这些对象未必简简单单实现一个接口, 而可能还由其他对象或者说零件来组成.

抽象工厂模式就是有抽象的工厂, 还有抽象的零件, 然后也像框架一样搭建好生产过程, 最后靠实现类来完成组装.

作者用了一个把带有层次关系的链接制作成HTML文件的例子, 先来看一批抽象类:

//这是一个抽象类, 当做具体链接和链接列表的父类, 其中规定了构造器传入一个标题, 而makeHTML()方法交给子类实现
public abstract class Item {

    protected String caption;

    public Item(String caption) {
        this.caption = caption;
    }

    public abstract String makeHTML();
}
//link类表示一个链接, 增加了一个url域, 但是没有实现makeHTML(), 也是一个抽象类
public abstract class Link extends Item {

    protected String url;

    public Link(String caption, String url) {
        super(caption);
        this.url = url;
    }

}
//trayl类要理解一下, 这个类继承Item, 其中又定义了一个Item容器. 这里要注意一下泛型是Item, 因为Link表示单个链接, 而Tray可以表示多个链接组成的列表. 由于列表中又可以包含单个链接或者列表, 所以Tray同时可以装入为Item类的Link和Tray类.
public abstract class Tray extends Item {
    protected ArrayList<Item> trays = new ArrayList<>();

    public Tray(String caption) {
        super(caption);
    }

    public void add(Item item) {
        trays.add(item);
    }
}

到了这里基本上明白了, 最终要组装的产品 – Page类, 其实是由多层次的Tray和Link类的实现类构成的. Tray和Link都是零件, Item是对这些零件的更高一层抽象. Page类是对使用这些零件组装的产品的抽象:

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;

public abstract class Page {

    protected String title;

    protected String author;

    protected List<Item> 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 PrintWriter(filename);
            writer.write(this.makeHTML());
            writer.close();
            System.out.println("编写完成");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public abstract String makeHTML();
}

可以看到, Page类中有一个内容列表, 其实就是先获得这个页面构成的所有要素, 然后也有一个抽象方法 makeHTML(), 交给子类来实现. 这本身也是模板模式.

零件和产品的抽象类都有了, 之后是Factory类:

public abstract class Factory {

    public static Factory getFactory(String classname){
        Factory factory = null;

        try {
            factory = (Factory) Class.forName(classname).newInstance();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException 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);

}

抽象的Factory采用了反射的方式, 可以通过类名返回一个任意的Factory具体对象. 而创建Link, Tray和组装Page的方法也是抽象的, 交给具体工厂来实现.

通过这几个抽象类, 我们可以发现已经像一个框架一样搭建好了组装Page产品的过程:

  1. 1 获取具体的工厂对象
  2. 2 根据内容创建Link 和 Tray对象, 如果有多个层次, 需要一层一层创建
  3. 3 根据内容创建Page对象

这里仅仅是过程, 具体每个对象的实际行为, 也就是makeHTML()方法, 就交给各个子类具体实现了. 虽然还没有实现, 但是已经可以看出来端倪了, 就是一个嵌套的列表.

下边是实体类, 对应我们的具体产品, 即生产出来的具体产品是HTML列表元素.

//ListLink是将标题和url创建为单个列表项目的具体实现类
public class ListLink extends Link {

    public ListLink(String caption, String url) {
        super(caption, url);
    }

    //Link类是创建单独的链接
    @Override
    public String makeHTML() {
        return "<li><a href=\"" + url + "\">" + caption + "</a></li>";
    }
}
public class ListTray extends Tray {
    public ListTray(String caption) {
        super(caption);
    }

    //Tray的makeHTML()要注意, 内部的ArrayList中可能存放有List或者Tray对象, List对象我们已经编写完毕了.
    //Tray内部有Tray对象的话, 遍历然后调用makeHTML()方法, 这样即使嵌套, 也可以正确的自动递归来实现.
    //这里除了抽象工厂的方法之外, 可以看到, 对于嵌套的内容, 如果最深层都是同样的零件, 通过把最深层的零件和外壳零件声明为同一个类型, 然后编写方法, 可以自然而然的实现递归, 而不用人工去递归. 感觉这个思想比面向对象还重要.
    @Override
    public String makeHTML() {
        StringBuilder result = new StringBuilder();

        //先写入标题部分
        result.append("<li>\n")
                .append(caption)
                .append("\n")

                .append("<ul>\n");

        for (Item tray : this.trays) {
            result.append(tray.makeHTML());
        }

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

看设计模式竟然发现了面向对象的思想可以自然而然的解决递归, 功力又要涨了. 然后是具体产品类:

public class ListPage extends Page {

    public ListPage(String title, String author) {
        super(title, author);
    }

    //这个方法比较简单,添加标题信息, 然后就递归将其中所有的Item对象的HTML代码放进一个ul列表, 最后生成HTML文件.
    @Override
    public String makeHTML() {
        StringBuilder result = new StringBuilder();

        result.append("<html><head><title>" + title + "</title></head>\n")
                .append("<body>\n")
                .append("<h1>" + title + "</h1>")
                .append("<ul>");

        for (Item item : content) {
            result.append(item.makeHTML());
        }

        result.append("</ul>").append("<hr><address>" + author + "</address>")
        .append("</body></html>");
        return result.toString();
    }
}

ListPage也没有什么花头, 其实就是外层的一个最大的容器, 底部依然都是落到Link项目上, 所以就再套一层. 话说这个声明成同一个类型然后使用递归的思想真的学到了.

最后是ListFactory类:

public class ListFactory extends Factory {

    @Override
    public Link createLink(String caption, String url) {
        return new ListLink(caption, url);
    }

    @Override
    public Tray createTray(String caption) {
        return new ListTray(caption);
    }

    @Override
    public Page createPage(String title, String author) {
        return new ListPage(title, author);
    }
}

这个类现在回头看看就很清晰了, 先组装List, 再组装Tray, 再组装二者的嵌套, 最后组装Page对象.

现在就可以用Main来实际组装一下List产品了:

public class Main {

    public static void main(String[] args) throws IOException {
        //反射获取类对象
        Factory listFactory = Factory.getFactory("designpatterns.abstractfactory.listfactory.ListFactory");

        //开始组装内容
        Link link1=  listFactory.createLink("柚子小站", "http://conyli.cc");
        Link link2=  listFactory.createLink("柚子小站2", "http://conyli.cc");
        Link link3=  listFactory.createLink("柚子小站3", "http://conyli.cc");
        Link link4=  listFactory.createLink("柚子小站4", "http://conyli.cc");

        Tray tray1 = listFactory.createTray("柚子小站列表1");
        tray1.add(link2);
        tray1.add(link3);

        Page page = listFactory.createPage("test", "minko");
        page.add(link1);
        page.add(tray1);
        page.add(link4);

        String re = page.makeHTML();
        System.out.println(re);

        Writer out = new PrintWriter("test.html");
        out.write(re);
        out.close();
    }
}

抽象工厂终于搞明白了, 这里的例子还同时学习到了利用接口完成递归的好处. 这里是产生了列表, 如果是产生其他对象, 只需要再编写一套工厂,零件和产品的实现类即可.

其实这里就是普通的工厂模式和模板模式, 再加上了零件的设定, 用来组装更加复杂的类, 对于复杂的类确实棒.

这时候还可以回忆一下创建实例的方法:

  1. 直接使用new关键字和单例模式: 耦合高, 需要具体类名出现在源代码中
  2. 工厂模式, 包括抽象工厂: 只需要知道工厂和产品抽象类的类名
  3. 原型模式: 只需要知道标示了原型的字符串和原型的基类
  4. 反射模式: 需要知道表示类名的字符串

在大型开发中一般都是规定好的接口, 所以第二种情况用的比较多. 后边三种情况都比第一种要解耦.

练习

习题 8-1 如果将其修改为private, 继承的类虽然也隐含继承了该域, 但无法访问, 必须通过父类的方法才可以访问. 好处是更加私密, 坏处就是要会给继承带来不便.

习题 8-2 这个其实就是改几行代码, 不再具体写了

习题 8-3 由于父类没有无参构造器, 而且编译器提供的默认构造器会先默认调用父类的无参构造器, 因此不能依靠默认构造器, 必须显式指定构造器, 而且父类构造器必须先调用.

习题 8-4 这个我思考了一下, 在这个例子里, Page最终也是由Link类构成的, 理论上是可以将其也声明为Item对象来获得统一的处理方法. 实际上也确实都有同名的方法public abstract String makeHTML(), 但这样做会让Tray对象可以添加Page对象, 但Page对象是成品, 从语义上还是要与其他零件区分开来.