一、前言
在学习设计模式的时候,大家可能只会去关心每一种设计模式的单独使用,而忘记了设计模式的本质,设计模式是从多种混杂的设计中拆离出来供大家学习理解的,每种设计模式其实都是有多多少少的关联,我们要结合将多种设计模式结合灵活使用,才能擦除美妙的火花,本文将结合自己的实战经历带大家理解多种设计模式的结合使用。
二、什么是设计模式 ?
从这段话可以看出,设计模式是问题的解决方法,而不是具体的实现,这句话很明显的告诉了我们,设计模式是没有语言的边界的,任何语言都可以运用。设计模式是解决一类问题的方法,所以大家在了解了问题的本质后,再去选择设计模式才是最佳的。
三、为什么要使用设计模式 ?
大家可能对设计模式是只闻其名,不见其人。可能在日常的编码中也不会去考虑使用设计模式,就像算法一样,在日常的编码中也不怎么去考虑算法,但是某一天你遇到了一个问题,别人说用这个算法可以很好的解决,用那个设计模式可以轻松化解的时候,你的心情又是怎样的呢?这并不代表这我们为了虚荣心去学习,而是当别人写出了这种代码,别人提出了这种观点,你能 get 到,而不是一脸迷惑的样子,当遇到了真正使用设计模式去更好的解决问题时你能做出最优方案,其实设计模式也是对面向对象语言的灵活运用,面向对象和设计模式是相辅相成的。
四、怎么使用设计模式 ?
在这里我将使用一个 Email 解析,翻译的案例,来综合使用一下,策略模式,模板模式,工厂模式,建造者模式。
在这里由于为了文件的安全性,我需要把段落进行拆分翻译,所以只能通过 手动创建 文档节点的方式进行。
1、执行流程
首先,介绍一下执行流程:
→ 读取 Email 文件到系统中去 。
→ 使用解析 Email 的库进行 Email 的解析,并且将同一个 Email 中的文件存放到对应的文件目录中 。
→ 将所有的 Email 文件中包含的内容读取到内存中,并且进行语句的拆分,以便分开翻译,保证数据的安全 。
→ 将翻译好的语句进行构建文档节点 。
→ 最终将翻译的结果输出到指定目录。
在这个流程中,我主要使用了三种设计模式(策略模式用来处理不同翻译平台的对接,模板模式主要用来进行翻译流程化控制,简单工厂模式用来进行翻译策略和模板的管理)
2、策略模式
我们需要定义一个策略模式的接口,将其他语言翻译成中文。
public interface TranslateStrategy {
/**
* 翻译
* @param source 其他语言
* @return 返回中文
*/
String translate(String source) throws IOException;
}
结合简单工厂模式,对接口进行实现并使用工厂进行管理。
public class TranslateStrategyFactory {
public static final int BAIDU = 1;
public static final int FUXIN = 2;
private static Map<Integer, TranslateStrategy> strategies = new HashMap<>(6);
private static String APP_ID;
private static String SECURITY_KEY;
static {
strategies.put(BAIDU, baiDu());
strategies.put(FUXIN, fuXin());
}
public static TranslateStrategy getStrategy() {
return strategies.get(TYPE);
}
public static TranslateStrategy baiDu() {
return source -> {
TransApi transApi = new TransApi(APP_ID, SECURITY_KEY);
// 发送请求
String transResult = transApi.getTransResult(source, "auto", "zh");
// 解析结果
StringBuilder stringBuilder = new StringBuilder();
// 解析返回的字符串
JSONObject object = (JSONObject) JSON.parse(transResult);
Object transStringResult = object.get("trans_result");
JSONArray transArray = (JSONArray) transStringResult;
// 将输出的结果进行拼装
transArray.forEach(result -> {
JSONObject toResult = (JSONObject) result;
stringBuilder.append(toResult.get("dst"));
});
System.out.println("百度翻译结果:" + stringBuilder);
return stringBuilder.toString();
};
}
public static TranslateStrategy fuXin() {
return source -> {
// 构建jsonString
Map<String, String> paramMap = new RequestParamBuilder()
.add("content", source)
.build();
FuXinResult fuXinResult = OkHttpClientUtil.doPostHttpRequest("http://api.pdf365.cn/translate/text", paramMap);
System.out.println("福昕翻译结果:" + fuXinResult.data.result);
return fuXinResult.data.result;
};
}
/**
* 构建 request 请求参数
*/
static class RequestParamBuilder {
Map<String, String> jsonMap = new HashMap<>();
public RequestParamBuilder() {
jsonMap.put("from", "auto");
jsonMap.put("to", "zh");
jsonMap.put("token", "cKS7YdMeu1NcDO2A1xfIX24b19lzsio0");
}
public RequestParamBuilder add(String key, String value) {
jsonMap.put(key, value);
return this;
}
public Map<String, String> build() {
return jsonMap;
}
}
}
在这里我使用读取配置文件的方式进行选择,这样可以保证用户只需要进行翻译类型的配置,无需进行代码的修改。
/**
* 读取配置文件
*/
private static void readBaiduConfig() {
String projectPath = System.getProperty("user.dir");
File file = new File(projectPath + File.separator + "app.txt");
try {
if (StringUtil.isEmpty(APP_ID) || StringUtil.isEmpty(SECURITY_KEY)) {
try (FileInputStream fileInputStream = new FileInputStream(file); BufferedReader ris = new BufferedReader(new InputStreamReader(fileInputStream))) {
// 读取百度配置
String config = ris.readLine();
List<String> list = StringUtil.splitStr(config, ":", String::new);
APP_ID = list.get(0);
SECURITY_KEY = list.get(1);
// 翻译类型
String typeLine = ris.readLine();
String[] split = typeLine.split(":");
String s = split[1];
Integer type = Integer.valueOf(s.trim());
// 配置类型
TYPE = type;
}
}
} catch (Exception e) {
} finally {
// 兜底操作
APP_ID = "20220713001271424";
SECURITY_KEY = "fPkw5j2Wt6KBnMJieGyq";
}
}
对于请求参数的处理,我也使用简单的建造者模式进行了封装。
/**
* 构建 request 请求参数
*/
static class RequestParamBuilder {
Map<String, String> jsonMap = new HashMap<>();
public RequestParamBuilder() {
jsonMap.put("from", "auto");
jsonMap.put("to", "zh");
jsonMap.put("token", "cKS7YdMeu1NcDO2A1xfIX24b19lzsio0");
}
public RequestParamBuilder add(String key, String value) {
jsonMap.put(key, value);
return this;
}
public Map<String, String> build() {
return jsonMap;
}
}
3、模板模式
在这里我使用 模板模式 对整个流程进行封装,抽取通用的方法,让子类实现父类抽象的方法。
public abstract class FileTranslateToFolderTemplate {
protected File file;
public void setFile(File file) {
this.file = file;
}
public FileTranslateToFolderTemplate(File file) {
this.file = file;
}
public FileTranslateToFolderTemplate() {
}
/**
* 执行
*/
public void execute() {
try {
Attachment attachment = parserFile();
translateAttachment(attachment);
attachmentToSameFolder(attachment);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
/**
* 翻译附件
*
* @param attachment
*/
protected void translateAttachment(Attachment attachment) throws Exception {
// 翻译 word 文档
List<AttachmentParagraph> paragraphs = attachment.getParagraphs();
for (AttachmentParagraph paragraph : paragraphs) {
paragraph.setTranslationContext(translateStringMethod(paragraph.getOriginalContext()));
}
}
/**
* 将文件写入到相同的目录
*
* @param attachment
*/
protected void attachmentToSameFolder(Attachment attachment) throws Exception {
File outPutFile = getOutPutFile();
writeByWay(outPutFile, attachment);
}
/**
* 根据word和pdf不同的方式写
*
* @param outPutFile
* @param attachment
*/
protected abstract void writeByWay(File outPutFile, Attachment attachment) throws Exception;
/**
* 解析文件
*
* @return
*/
protected Attachment parserFile() throws FileNotFoundException {
FileInputStream fileInputStream = new FileInputStream(file);
return parserFile(fileInputStream);
}
protected abstract Attachment parserFile(FileInputStream fileInputStream);
/**
* 获取输出到的文件
*
* @return
*/
protected File getOutPutFile() throws FileNotFoundException {
File parent = new File(this.file.getParent());
File outToFile = new File(parent, "【译文】" + file.getName());
return outToFile;
}
}
可以看到 execute() 方法,定义了统一的执行的流程,该执行流程是固定的,大家只需要实现抽象方法,对解析的方法进行编写即可,这样比如我想添加一个新的文件类型的解析,只需要按照这个规范进行即可,只需要实现 parserFile() 和 writeByWay() ,无需重复编写大量的代码,使代码的复用性大大提高。
对 PDF 解析的实现
public class PdfTranslateToFolder extends FileTranslateToFolderTemplate {
public PdfTranslateToFolder() {
}
public PdfTranslateToFolder(File file) {
super(file);
}
@Override
protected void writeByWay(File outPutFile, Attachment attachment) throws Exception {
attachment.setName(outPutFile.getName());
PDFDocumentBuilder builder = new PDFDocumentBuilder();
builder.setAttachment(attachment);
builder.setFatherFile(new File(outPutFile.getParent()));
builder.build();
}
@Override
protected Attachment parserFile(FileInputStream fileInputStream) {
return ParserPDFUtils.parser(fileInputStream);
}
}
对 Word 解析的实现
public class WordDocxTranslateToFolder extends FileTranslateToFolderTemplate {
public WordDocxTranslateToFolder() {
}
public WordDocxTranslateToFolder(File file) {
super(file);
}
@Override
protected void writeByWay(File outPutFile, Attachment attachment) throws IOException {
SimpleShowMailOriginalBuilder builder = new SimpleShowMailOriginalBuilder();
builder.add(SimpleShowMailOriginalBuilder.ContextTitle.ORIGINAL, attachment.getParagraphs());
try (FileOutputStream fileOutputStream = new FileOutputStream(outPutFile);
XWPFDocument document = builder.build()) {
document.write(fileOutputStream);
}
}
@Override
protected Attachment parserFile(FileInputStream fileInputStream) {
return ParserWordUtils.parserDocx(fileInputStream);
}
}
五、滥用的后果
在进行处理创建节点的模块时,我确实是为了使用设计模式而使用设计模式,出现了编写时确实思路清晰,但是在修改的时候造成了,本来的一处修改,现在变成了多处修改,这样就增加了代码维护的成本。
从代码的文件目录接口可以看出,我把每一个节点都抽取出为一个策略,并且使用建造者模式进行构建,可后来修改的时候发现,维护成本实在是太高了。更何况节点的变动是非常大的。
我也希望通过这个案例,向大家说明,不是说任何情况设计模式都是可取的,要根据适当的场景去选择。
六、总结
- 设计模式在日常的开发中并不是必须的,当时设计模式能够提升代码的维护性,他也是一种通用的规范,能够帮助到大家在团队里面进行沟通。
- 设计模式的学习并被一日之功,需要大家慢慢消化理解,一次一次的尝试,直到找到最适合的解决方案。
- 同时我也推荐大家去阅读《设计模式之禅》和《重学设计模式》