项目亮点之设计模式六合一 (1)
本篇文章,是项目亮点系列的开山之作。项目亮点系列,会从多种设计模式配合使用,到系统优化,再到线上问题处理,如OOM、内存泄漏、慢SQL排查优化等进行讲解。如果有兴趣记得收藏关注。当然了,本人水平也有限,难免有些疏漏之处,还望大家批评指正。
引言
背景
可能一些同学觉得自己的简历平平无奇,乏善可陈. 没关系,设计模式六合一, 教你如何将六种设计模式丝滑融入业务. 看完这篇文章你就能给简历新增一条:使用xx和xx设计模式实现xxx需求,降低耦合度,提高代码灵活度和可复用性。
可能一些同学想要提升技术或者只是单纯想学习设计模式,设计模式六合一, 教你如何将六种设计模式丝滑融入业务,看完这篇文章也能让你对设计模式有更深入的理解。
本文将会从一个真实的需求出发,使用6种常见设计模式取长补短,环环相扣来解决问题,会切实的让你体会到设计模式的妙用。绝非网上其他文章那种为了设计而设计,生搬硬套,粗制滥造。
问题
提起设计模式, 很多人的感觉就是虚无缥缈, 空中楼阁. 感觉只有一些框架底层, 或者牛人高手才会用到, 不知道自己平时做的crud业务该如何使用设计模式.
究其原因, 是因为网上设计模式的教程虽多, 但大都脱离实际. 比如, 假设有个菜品类, 有个厨师类, 使用装饰器模式来做出不同的菜. 又或者有个计算机类, 有个CPU类, 使用建造者模式构建一个计算机.
但是实际业务代码中, 哪有厨师类, 哪有计算机类?
用我非常敬佩的一位程序员大佬的话说就是, 设计模式教程已然是白骨满地, 脱离业务, 脱离具体应用场景, 谈何设计.
而且, 当前Java业务开发, 都是在Springboot环境中, 我们获取对象都是从Spring的ioc容器中拿, 很少自己new对象了. 而网上的设计模式教程, 一次new几个甚至十几个对象, 试问在Spring环境中, 你会这样new对象吗?
所以在Spring环境中, 如何将现有的设计模式融合进去呢?
文章简介
这篇文章会从一个具体的业务出发, 来讲如何将常见的6种设计模式丝滑地融入业务中. 但是由于这是一个设计模式教程, 并不是讲业务需求的教程, 所以将偏业务的内容用伪代码或者注释代替, 让大家专注于设计模式的学习.
在看文章中, 大家一定会产生很多疑惑. 比如, 这段代码用这个设计模式是否合适? 这里为什么要这么写, 不能那么写? 为什么不用Spring的工厂, 要自己建工厂等等. 不必着急, 认真看完, 在文章的最后几节, 针对于学习过程中遇到的具体问题, 会做一个详细的解答.
在本文中, 会将如下六个设计模式融合在一起. 这几个设计模式结合使用, 能够扬长避短, 相辅相成.
- 策略模式
- 模板方法模式
- 工厂模式
- 单例模式
- 享元模式
- 门面模式/外观模式
注意, 本文章注重于将设计模式融入业务中, 不会讲解这些设计模式的基本思想, 如果完全不了解设计模式, 建议了解基本思想后再看.
需求分析
需求
产品: 现在系统要开发一个新功能, 从不同的文件中读取数据到写入到某张数据表中. 比如, 有个csv / json / excel文件, 里面存储了用户相关的信息, 读取这些文件信息, 保存到数据库中. 目前, 文件类型主要有3种, csv文件, json文件, excel文件.
码农: 这***产品, 怎么文件类型还有好几种, 这得写多少个if else呀. 家人们, 谁懂啊, 无语了.
拿到这个需求, 一些人的想法是这样的
public class XXX { public void export2Db(String filepath) { String type = getFileType(filepath); if ("csv".equals(type)) { // 读取csv文件, 将数据保存到数据库中, 此处省略500行代码 } else if ("json".equals(type)) { // 读取json文件, 将数据保存到数据库中, 此处省略500行代码 } else if ("excel".equals(type)) { // 读取excel文件, 将数据保存到数据库中, 此处省略500行代码 } else { throw new IllegalArgumentException("不支持该类型: " + type); } } }
存在的问题
对于这种代码, 我们都能看到一些问题
- type使用String类型的魔法值, 没用枚举.
- 有几个type就if判断几次, 假设新增txt文件类型, 又要修改代码, 拓展性差.
- 代码核心代码都写到一个方法中, 一些逻辑无法复用, 而且会导致方法代码巨多, 可读性差, 后续也不好维护.
- 此处省略n行吐槽
标签类
其实, 我们无论是学习还是写需求, 要学会举一反三. 用计算机行话来说, 要学会抽象. 从一个问题, 抽象出一类问题.
这种需求的本质其实是根据不同类型(csv, json, excel)来做不同操作(csv->db, json->db, excel->db), 但是操作大致流程相同.
这种需求往往很多人会写一个带有标签字段(type)的类来处理, 然后if else判断, 对不同type使用不同操作.
这类根据不同类型来做不同操作, 但是操作大致流程相同的需求, 被称为标签类问题.
当你把这个需求抽象为标签类问题后, 你就会发现, 业务中这种需求比比皆是. 如
- 多种类第三方支付问题
- 对不同身份的人的处理问题
- 不同数据库的sql生成问题.
在Effective Java这本书中的23节, 类层次结构优于标签类, 其实有介绍过这个问题.
以下为Effective Java原文
在重构这本书中, 以子类取代类型码这一节, 也说过这类问题.
以下为重构这本书的原文
无论是Effective Java, 还是重构, 都推荐以子类的方式来取代标签类. 其实这里就是多态, 或者说就是策略模式的思想.
那么, 接下来, 让我们来领略设计模式的魅力.
策略模式
思想
策略模式是多态最好的体现, 也是解决这种标签类的最好的方式之一.
策略模式的定义为: 在策略模式定义了一系列策略类,并将每个具体实现封装在独立的类中,使得它们可以互相替换。通过使用策略模式,可以在运行时根据需要选择不同的算法,而不需要修改调用端代码。是一种用来解决很多if else的方式.
看到这儿, 可能很多人依旧一脸懵逼. 没关系, 就当这句话在放屁. 我们看明白下面的具体代码如何写即可.
实现
在本需求中, 需要写一个顶层的策略接口FileExport, 新增 export2Db
抽象方法.
然后根据不同类型的导出方式, 编写CsvExport, ExcelExport, JsonExport三个策略类实现FileExport接口.
这里给出类图和具体代码.
public interface FileExport { void export2Db(String filepath); }
public class CsvExport implements FileExport{ @Override public void export2Db(String filepath) { // 读取csv文件, 将数据保存到数据库中, 此处省略具体代码 } }
public class ExcelExport implements FileExport { @Override public void export2Db(String filepath) { // 读取excel文件, 将数据保存到数据库中, 此处省略具体代码 } }
public class JsonExport implements FileExport{ @Override public void export2Db(String filepath) { // 读取json文件, 将数据保存到数据库中, 此处省略具体代码 } }
有其他类依赖于我们的策略类, 那么就可以这样使用, 需要哪个直接传入对应的FileExport对象即可.
class XXX { // 注意这里参数类型声明为FileExport接口, 这就意味着可以传入任意的FileExport实现类 public static void fileExport2Db(FileExport fileExport, String filepath) { fileExport.export2Db(filepath); } public static void main(String[] args) { FileExport excelExport = new ExcelExport(); fileExport2Db(excelExport, "文件路径"); } }
有人说, 感觉你这和网上的设计模式教程也没啥区别呀. 别着急, 慢慢往下看.
如何拓展
使用策略模式后, 如果后续需求变更, 需要拓展其他文件格式导出到数据库, 比如yml文件导出到数据库. 那么我们新增YmlExport类, 实现FileExport即可.
存在的问题
那么, 目前的代码就不存在问题了吗? 当然不是, 我们来看策略模式常见的两个问题
- 不同实现类中代码重复
- 如果想要根据传入参数动态使用某个策略类, 还是避免不了大量if else
问题1: 不同实现类中代码重复
当我们要实现具体将某中文件数据导出到数据库时, 可以把大致过程划分为以下几步
- 检查参数中的filepath是否合法
- 读取文件数据到一个Java对象中
- 对数据进行处理
- 保存到数据库中
那么我们修改每一个策略类 (FileData是一个保存文件数据的类, 仅做示例, 不做实现)
- 新增抽象方法
void check(String filepath)
: 检查filepath是否合法 - 新增抽象方法
FileData readFile(String filepath)
: 来读取文件数据 - 新增抽象方法
FileData processData(FileData fileData)
: 来处理数据 - 新增抽象方法
void fileDataExport2Db(FileData fileData)
: 导出数据到数据库 void export2Db(String filepath)
: 调用以上四个抽象方法来完成文件导出到数据库
以下为类图和具体实现
public class FileData { // 这是一个保存文件数据的类, 仅做示例 }
public interface FileExport { void export2Db(String filepath); }
public class JsonExport implements FileExport { @Override public void export2Db(String filepath) { check(filepath); FileData fileData = readFile(filepath); fileDataExport2Db(fileData); } private void check(String filepath) { // 检查filepath是否为空 if (StrUtil.isBlank(filepath)) { throw new IllegalArgumentException("filepath为空"); } // 检查filepath是否存在, 是否为文件 File file = new File(filepath); if (!file.exists() || !file.isFile()) { throw new IllegalArgumentException("filepath不存在或者不是文件"); } // 检查文件类型是否为Json类型 (用了hutool的FileTypeUtil工具) String type = FileTypeUtil.getType(file); if (!Objects.equals("json", type)) { throw new IllegalArgumentException("文件类型异常: " + type); } } private FileData readFile(String filepath) { System.out.println("以json方式读取filepath中的文件"); System.out.println("将读取后的结果转为通用的FileData类型"); return new FileData(); } private void fileDataExport2Db(FileData fileData) { System.out.println("将处理后的数据转为数据表对应的实体类"); System.out.println("使用mybatis/jpa/jdbc等orm工具保存到数据库中"); } }
public class CsvExport implements FileExport { @Override public void export2Db(String filepath) { check(filepath); FileData fileData = readFile(filepath); FileData fileDataProcessed = processData(fileData); fileDataExport2Db(fileDataProcessed); } private void check(String filepath) { // 检查filepath是否为空 if (StrUtil.isBlank(filepath)) { throw new IllegalArgumentException("filepath为空"); } // 检查filepath是否存在, 是否为文件 File file = new File(filepath); if (!file.exists() || !file.isFile()) { throw new IllegalArgumentException("filepath不存在或者不是文件"); } // 检查文件类型是否为csv类型 (用了hutool的FileTypeUtil工具) String type = FileTypeUtil.getType(file); if (!Objects.equals("csv", type)) { throw new IllegalArgumentException("文件类型异常: " + type); } } private FileData readFile(String filepath) { System.out.println("以csv方式读取filepath中的文件"); System.out.println("将读取后的结果转为通用的FileData类型"); return new FileData(); } private FileData processData(FileData fileData) { System.out.println("对fileData进行处理"); return fileData; } private void fileDataExport2Db(FileData fileData) { System.out.println("将处理后的数据转为数据表对应的实体类"); System.out.println("使用mybatis/jpa/jdbc等orm工具保存到数据库中"); } }
public class ExcelExport implements FileExport { @Override public void export2Db(String filepath) { check(filepath); FileData fileData = readFile(filepath); FileData fileDataProcessed = processData(fileData); fileDataExport2Db(fileDataProcessed); } private void check(String filepath) { // 检查filepath是否为空 if (StrUtil.isBlank(filepath)) { throw new IllegalArgumentException("filepath为空"); } // 检查filepath是否存在, 是否为文件 File file = new File(filepath); if (!file.exists() || !file.isFile()) { throw new IllegalArgumentException("filepath不存在或者不是文件"); } // 检查文件类型是否为Excel类型 (用了hutool的FileTypeUtil工具) String type = FileTypeUtil.getType(file); if (!Objects.equals("xlsx", type)) { throw new IllegalArgumentException("文件类型异常: " + type); } } private FileData readFile(String filepath) { System.out.println("以Excel方式读取filepath中的文件"); System.out.println("将读取后的结果转为通用的FileData类型"); return new FileData(); } private FileData processData(FileData fileData) { System.out.println("对fileData进行处理"); return fileData; } private void fileDataExport2Db(FileData fileData) { System.out.println("将处理后的数据转为数据表对应的实体类"); System.out.println("使用mybatis/jpa/jdbc等orm工具保存到数据库中"); } }
代码写到这里, 想必大家也看出来问题了. 策略模式虽好, 但是实现时往往会产生大量重复代码, 比如上文中重复的校验, 重复的读文件等.
那么该如何解决这个问题呢?
由于我将整体流程已经划分好了, 所以有设计模式基础的人可能一下子就想到了模板方法模式.
算法的整体流程固定, 但是某些实现类的细节不同, 这不就是模板方法模式的思想吗.
这也解释了, 为什么有些同学觉得策略模式和模板方法模式有些许相似, 只是模板方法模式多了个模板来控制整体流程. 其实所谓模板方法模式就可以理解为是对策略模式抽取了统一的算法流程所得.
可能某些对模板方法模式不太理解的同学还不太能理解上面这段话. 没关系, 接着看下去, 后面将模板方法模式的章节会给你答案.
问题2: 动态使用策略类产生大量if else
对于第二个问题, 某些同学可能会产生疑问, 什么是动态使用策略类呢?
简而言之, 就是根据传入的参数, 或者根据某些情况来决定使用哪个策略类来处理.
对于我们当前的需求来说, 就是根据文件类型, 动态选择对应的策略类来进行处理.
- 如果filepath为
/a/b/c/test.json
, 可以自动new JsonExport
来进行处理 - 如果filepath为
/a/b/c/test.csv
, 可以自动new CsvExport
来进行处理
这样岂不是用起来更方便. 那么代码就变成了这样
public class XXX { public void import2Db(String filepath) { String fileType = getFileType(filepath); FileExport fileExport; if ("csv".equals(fileType)) { fileExport = new CsvExport(); fileExport.export2Db(filepath); } else if ("json".equals(fileType)) { fileExport = new JsonExport(); fileExport.export2Db(filepath); } else if ("excel".equals(fileType)) { fileExport = new ExcelExport(); fileExport.export2Db(filepath); } else { throw new IllegalArgumentException("不支持该类型: " + fileType); } } }
看到这儿, 某些同学心里在想, 说好的策略模式就可以不写if else呢? 果然, 童话里都是骗人的.
不要着急, 后续会采用枚举+工厂模式来解决这个问题. 肯定会帮你写出优雅的代码.
模板方法模式
接下来, 我们用模板方法模式来解决第一个问题, 也就是不同实现类中的代码重复问题
思想
模板方法模式会在抽象类或者接口中定义一个算法的整体流程, 该流程中会调用不同的方法. 这些方法的具体实现交给不同的子类完成. 也就是说它适合整体流程固定, 具体细节不同的场景.
看到这儿, 可能很多人依旧一脸懵逼. 没关系, 就当这句话在放屁. 我们看明白下面的具体代码如何写即可.
实现
这里的代码基于策略模式中问题1的代码
- 首先新增抽象父类AbstractFileExport
JsonExport / CsvExport / ExcelExport
具体实现类, 继承AbstractFileExport, 重写以上部门方法.
以下为类图和代码实现
public interface FileExport { void export2Db(String filepath); }
public abstract class AbstractFileExport implements FileExport { @Override public void export2Db(String filepath) { check(filepath); FileData fileData = readFile(filepath); // 钩子函数, 子类决定是否需要对数据进行处理 if (needProcessData()) { fileData = processData(fileData); } fileDataExport2Db(fileData); } protected void check(String filepath) { // 检查filepath是否为空 if (StrUtil.isBlank(filepath)) { throw new IllegalArgumentException("filepath为空"); } // 检查filepath是否存在, 是否为文件 File file = new File(filepath); if (!file.exists() || !file.isFile()) { throw new IllegalArgumentException("filepath不存在或者不是文件"); } // 检查文件类型是否为子类可以处理的类型 (用了hutool的FileTypeUtil工具) String type = FileTypeUtil.getType(file); if (!Objects.equals(getFileType(), type)) { throw new IllegalArgumentException("文件类型异常: " + type); } } /** * 数据类型转换并保存到数据库, 这是通用操作, 所以写在父类中 */ protected void fileDataExport2Db(FileData fileData) { System.out.println("将处理后的数据转为数据表对应的实体类"); System.out.println("使用mybatis/jpa/jdbc等orm工具保存到数据库中"); } /** * 如果子类要处理数据, needProcessData()返回true, 并重新该方法 */ protected FileData processData(FileData fileData) { throw new UnsupportedOperationException(); } /** * 获取子类能处理的文件类型, check()方法会用到 */ protected abstract String getFileType(); /** * 钩子函数, 让子类决定是否需要处理数据 */ protected abstract boolean needProcessData(); protected abstract FileData readFile(String filepath); }
public class JsonExport extends AbstractFileExport { private static final String FILE_TYPE = "json"; @Override protected String getFileType() { return FILE_TYPE; } @Override protected boolean needProcessData() { return false; } protected FileData readFile(String filepath) { System.out.println("以json方式读取filepath中的文件"); System.out.println("将读取后的结果转为通用的FileData类型"); return new FileData(); } }
public class CsvExport extends AbstractFileExport { private static final String FILE_TYPE = "csv"; protected FileData readFile(String filepath) { System.out.println("以csv方式读取filepath中的文件"); System.out.println("将读取后的结果转为通用的FileData类型"); return new FileData(); } @Override protected String getFileType() { return FILE_TYPE; } @Override protected boolean needProcessData() { return true; } protected FileData processData(FileData fileData) { System.out.println("对fileData进行处理"); return fileData; } }
public class ExcelExport extends AbstractFileExport { private static final String FILE_TYPE = "xlsx"; protected FileData readFile(String filepath) { System.out.println("以Excel方式读取filepath中的文件"); System.out.println("将读取后的结果转为通用的FileData类型"); return new FileData(); } protected FileData processData(FileData fileData) { System.out.println("对fileData进行处理"); return fileData; } @Override protected String getFileType() { return FILE_TYPE; } @Override protected boolean needProcessData() { return true; } }
看完代码后大家就会发现, 大量重复代码和流程都被抽取到父类中了. 策略模式中出现的代码重复问题就解决了.
大家也能理解我所说的模板方法模式可以理解为是对策略模式抽取了统一的算法流程所得.
如何拓展
和之前类似, 如果后续需求变更, 需要拓展其他文件格式导出到数据库, 比如yml文件导出到数据库. 那么我们新增YmlExport类, 继承AbstractFileExport即可.
由于AbstractFileExport规定了统一流程, 且提供了 check()
, fileDataExport2Db()
等方法, 所以后续拓展起来代码量也会更少, 更方便.
问题
可以看到, 这里使用策略类抽取整体流程为模板方法的方式, 虽然解决了代码重复问题.
但是之前提到的动态使用策略类产生大量if else的依旧没有解决. 后续将会使用枚举+工厂模式来彻底解决这个问题.
简历or面试
看到这里恭喜你已经掌握了使用策略模式来实现一个需求,以及策略模式抽取通用流程来转化为模板方法模式。
写简历
你可以直接将本需求合理地融合到你的项目中,简历直接新增一条:使用策略模式+模板方法模式实现将不同类型文件导入到数据库的功能,降低耦合度,提升代码的灵活性和可复用性。
当然了,你也可以使用今天所学设计模式来解决你公司项目或者你个人的问题,然后简历直接新增一条:使用xx设计模式实现xxx需求,降低耦合度,提升代码的灵活性和可复用性。
如果你有耐心看完后面的设计模式,那你简历上就可以写:使用策略+模板方法+工厂+享原+单例+门面模式来实现xx功能,降低耦合度,大大提高代码灵活度和可复用性。
你也许觉得实现一个需求,使用这么多设计模式是不是过度设计或者大材小用。不用担心,每一个设计模式的使用,都是为了解决当前存在的问题,不同设计模式取长补短,环环相扣,绝非过度滥用。
准备面试
同样的,面试官看到你的简历也有可能产生同样的疑问。那么你就可以大大方方的给面试官解释,我用当前这个的设计模式是为了解决某个具体需求,或者解决某个设计模式带来的问题,各个设计模式是如何环环相扣,取长补短的。
相信只要你能够讲明白,面试官也会对你刮目相看!
面试官有可能会问的问题?或者你可以主动给面试官聊如下问题。
为什么要用策略模式?
- 因为不同的文件导入数据库的方式是不同的,所以使用不同的策略类来做。
- 每个策略类都实现策略接口,可互相替换,且每个类各司其职,符合单一职责原则。
为什么后面过渡到了模板方法模式?
- 不同策略类虽然具体细节不同,但是整体流程相同,都要做校验,读文件,处理数据,最后写入数据库。
- 如果所有策略类都做一遍这个流程,那么重复代码很多。
- 使用模板方法的思想抽取整体流程到抽象父类中,就可以规定整体流程,实现代码复用。
- 具体策略类只负责处理方法细节即可。
策略模式和模板方法模式是什么关系?
- 个人认为模板方法模式可以理解为是对策略模式抽取了统一的算法流程所得.
- 父类的模板方法负责规定整体流程,子类策略类负责实现具体方法细节。
策略模式带来的if else问题怎么解决?
- 可以使用枚举+工厂模式解决,后续文章会讲到,敬请期待。
我们都说组合大于继承,为什么在模板方法这里还用了这么多继承?
- 虽然说目前的设计推荐多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。
- 继承改写成组合意味着要做更细粒度的类的拆分,要定义更多的类和接口。类和接口的增多也就或多或少地增加代码的复杂程度和维护成本,所以具体使用继承还是组合还得看项目整体设计情况。
- 当继承层级比较浅,比如只继承一两层时,或者继承关系比较稳定,不会轻易改变时,使用继承是比较方便的。反之,则组合较好。
- 当然了关于继承和组合的优劣这个问题比较复杂,需要更多的例子和篇幅才能聊清楚,这里暂不展开。不理解的同学,只需要知道组合优于继承,但是并非一味摒弃继承即可。
最后提醒一下各位读者,面试切记不要死记硬背,而是要真正理解每个地方为什么要这样用,为什么要这样处理,这样处理有什么优点,同时又带来了什么问题,新的问题又是怎么被解决的。
总结
本文从一个具体的需求出发, 分享了策略模式, 模板方法模式的具体实现. 但是还是留下了一些问题.
后续会用 工厂模式+单例模式+享元模式+门面模式/外观模式 来解决完善.
如果有兴趣记得点赞收藏关注。
#设计模式##后端开发##简历中的项目经历要怎么写##简历修改[话题]##简历技巧[话题]#从一个真实的需求出发,使用几种常见设计模式取长补短,环环相扣来解决问题,会切实的让你体会到设计模式的妙用。绝非网上其他文章那种为了设计而设计,导致生搬硬套,粗制滥造。