原型模式
本文已参与「新人创作礼」活动,一起开启掘金创作之路。
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
我们在开发时经常会遇到对象很复杂,依赖各种外部资源,导致实例化很慢,那么这时候就可以考虑使用原型模式来提升创建对象的效率。
在 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实践笔记」的原创文章,转载请联系作者,附上原文链接及本声明。 公众号合集链接