【设计模式实践笔记】第四节:原型模式

161 阅读4分钟

原型模式

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

我们在开发时经常会遇到对象很复杂,依赖各种外部资源,导致实例化很慢,那么这时候就可以考虑使用原型模式来提升创建对象的效率。

Java 语言中,原型模式是被天然支持的,被拷贝的对象只需要声明实现 Cloneable 接口,然后实现 Object 提供的 clone 方法,这样我们就能完成一个普通对象的拷贝。

使用原型模式生成网页代码

假设老板需要我们提供一个在线生成PDF文件的功能,我们的思路是通过 HTML 进行排版和渲染,然后通过网页转PDF工具生成PDF文件,这么做可以充分利用网页的样式优势。

那么首先我们需要一个网页的模板文件,模板里可以预先写好各种样式,然后往里面填值,生成我们想要的网页。

从文件中读取模板的代码如下:

public class TemplateHelper {
    private static final String TPL_PATH = "/template.html";

    // 通过模板生成html字符串
    public static String getHtml(Map<String, String> param) throws IOException {
        HtmlTemplate template = getTemplate();
        template.setParams(param);
        return template.getHtml();
    }

    // 获取模板
    private static HtmlTemplate getTemplate() throws IOException {
        String filePath = TemplateHelper.class.getResource(TPL_PATH).getFile();
        BufferedReader reader = new BufferedReader(new FileReader(filePath));
        StringBuilder stringBuilder = new StringBuilder();
        char[] buffer = new char[10];
        while (reader.read(buffer) != -1) {
            stringBuilder.append(new String(buffer));
            buffer = new char[10];
        }
        reader.close();
        String templateSource = stringBuilder.toString();
        return new HtmlTemplate(templateSource);
    }
}

模板类:

// 模板类
public class HtmlTemplate {

    // 源码
    private String source;
    // 模板参数
    private Map<String, String> params = new HashMap<>();
    // 渲染后的结果缓存
    private String html;
    // 插值
    private Set<String> slotSet = new HashSet<>();

    public HtmlTemplate(String source) {
        this.source = source;
        init();
    }

    private void init() {
        if (slotSet.isEmpty()) {
            // 插值匹配
            String SLOT_PATTERN = "\\{\\{[a-zA-z0-9]+}}";
            Pattern r = Pattern.compile(SLOT_PATTERN);
            Matcher m = r.matcher(source);

            while (m.find()) {
                slotSet.add(m.group());
            }
        }
    }

    private void render() {
        html = source;
        for (String slot : slotSet) {
            html = html.replace(slot, params.getOrDefault(slot.replace("{{", "").replace("}}", ""), slot));
        }
    }

    public String getHtml() {
        if (html == null) {
            render();
        }
        return html;
    }

		// 省略 getter、setter
}

模板文件和具体代码参考源码。

测试类:

public class TemplateHelperTest {

    @Test
    public void getHtml() throws IOException {
        Map<String, String> params = new HashMap<>();
        params.put("hello", "你好");
        params.put("world", "Java实践笔记");
        String content = TemplateHelper.getHtml(params);
        System.out.println(content);
    }
}

输出结果:

<!DOCTYPE HTML><html><head><title>原型模式</title></head><body>快来说你好Java实践笔记!</body></html>

这样我们提供 HTML 的核心方法就结束了,接下来可以使用其他工具直接把 HTML 代码转成PDF文件。细心的小伙伴可能会发现,这种实现方式每次都要有一个模板对象,而创建模板对象涉及到文件IO、解析插值这些比较耗时的操作,不是一个高效的做法,很容易遇到性能问题。

那么我们能不能优化一下呢?

答案肯定是可以,优化的思路很多,比如选择使用缓存,预先把模板读到内存里,可以节省IO部分的时间,还可以使用原型模式,省去每次创建对象前的IO操作和创建对象后的初始化过程,当然还有其他的优化思路,这里不多展开。

下面我们使用原型模式来重构获取模板的逻辑。

重构之后的模板类:

// 模板类【实现Cloneable接口】
public class HtmlTemplate implements Cloneable {

    // 源码
    private String source;
    // 模板参数
    private Map<String, String> params = new HashMap<>();
    // 渲染后的结果缓存
    private String html;
    // 插值
    private Set<String> slotSet = new HashSet<>();

    public HtmlTemplate(String source) {
        this.source = source;

        init();
    }

  	// 构造器【使用私有构造器复制关键变量】
    private HtmlTemplate(String source, Set<String> slotSet) {
        this.source = source;
        this.slotSet = slotSet;
    }

    private void init() {
        if (slotSet.isEmpty()) {
            // 插值匹配
            String SLOT_PATTERN = "\\{\\{[a-zA-z0-9]+}}";
            Pattern r = Pattern.compile(SLOT_PATTERN);
            Matcher m = r.matcher(source);

            while (m.find()) {
                slotSet.add(m.group());
            }
        }
    }

    private void render() {
        html = source;
        for (String slot : slotSet) {
            html = html.replace(slot, params.getOrDefault(slot.replace("{{", "").replace("}}", ""), slot));
        }
    }

    public String getHtml() {
        if (html == null) {
            render();
        }
        return html;
    }

  	// 省略 getter、setter
  
  	// 【实现克隆方法】
    @Override
    public HtmlTemplate clone() {
        return new HtmlTemplate(this.source, this.slotSet);
    }
}

模板类经过改造已经支持对象拷贝,接下来我们要创建一个原型对象。

public class TemplateHelper {
    private static final String TPL_PATH = "/template.html";

    // 原型对象
    private static HtmlTemplate htmlTemplateProtoType;

    static {
        try {
            init();
        } catch (IOException e) {
            System.out.println("初始化模板原型异常!");
        }
    }

    // 初始化原型对象
    private synchronized static void init() throws IOException {
        if (htmlTemplateProtoType != null) {
            return;
        }
        String filePath = TemplateHelper.class.getResource(TPL_PATH).getFile();
        BufferedReader reader = new BufferedReader(new FileReader(filePath));
        StringBuilder content = new StringBuilder();
        String str;
        while ((str = reader.readLine()) != null) {
            content.append(str);
        }
        reader.close();
        String templateSource = content.toString();
        htmlTemplateProtoType = new HtmlTemplate(templateSource);
    }

    // 通过模板生成html字符串
    public static String getHtml(Map<String, String> param) throws IOException {
        HtmlTemplate template = getTemplate();

        template.setParams(param);
        return template.getHtml();
    }

    // 获取模板
    private static HtmlTemplate getTemplate() throws IOException {
        if (htmlTemplateProtoType == null) {
            init();
        }

        // 使用原型创建新对象
        return htmlTemplateProtoType.clone();
    }
}

相比对重构前的代码,只改变了创建模板对象的方式,将模板原型对象放到了静态块里进行初始化,正常情况只需要初始化一次,后续所有的模板对象创建都是以原型为母本进行的复制。

为了保持文章简单,文中对模板的解析和渲染都做了删减,争取用更易懂的方式呈现出来,生产代码请勿直接使用。

小结

原型模式在实践中的例子没有其他的设计模式那么多,也更容易通过其他手段进行替代,但是这种思想仍然值得我们学习和借鉴。

实践出真知,让我们一起加油吧!

关注「Java实践笔记」公众号获取全部源代码!

版权声明:本文为公众号「Java实践笔记」的原创文章,转载请联系作者,附上原文链接及本声明。 公众号合集链接