设计模式(策略,工厂,单例,享元,门面)+模板方法
文章目录
- 前提
- 策略模式
- 思想
- 实现
- 如何拓展
- 模板方法
- 存在的问题
- 思想
- 实现
- 如何拓展
- 工厂模式
- 实现
- 问题及解决(解耦)
- 配置文件方式
- 使用注解
- 单例模式
- 实现方式
- 1,懒汉式(线程不安全)
- 2,懒汉式(线程安全)
- 3,饿汉式
- 4,双重校验锁机制(面)
- 5,静态内部类
- 6,枚举
- 体现
- 享元模式
- 门面模式
前提
假设做一个需求,从文件中拿到数据并存在数据库中,文档有多种不同的类型,比如json,excel,csv等等。在做这个去求得在过程中,如何让代码变得优雅,可读性高,耦合度低,易扩展。
策略模式
为解决上述问题,首先想到的是下面的代码
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文件类型, 又要修改代码, 拓展性差.
- 代码核心代码都写到一个方法中, 一些逻辑无法复用, 而且会导致方法代码巨多, 可读性差, 后续也不好维护.
思想
策略模式是多态最好的体现, 也是解决这种标签类的最好的方式之一.
策略模式的定义为: 在策略模式定义了一系列策略类,并将每个具体实现封装在独立的类中,使得它们可以互相替换。通过使用策略模式,可以在运行时根据需要选择不同的算法,而不需要修改调用端代码。是一种用来解决很多if else的方式.
实现
在本需求中, 需要写一个顶层的策略接口FileExport, 新增 export2Db抽象方法.
然后根据不同类型的导出方式, 编写CsvExport, ExcelExport, JsonExport三个策略类实现FileExport接口.
这里给出类图和具体代码.
public interface FileExport {void export2Db(String filepath);
}
public class CsvExport implements FileExport{@Overridepublic void export2Db(String filepath) {// 读取csv文件, 将数据保存到数据库中, 此处省略具体代码}
}
public class ExcelExport implements FileExport {@Overridepublic void export2Db(String filepath) {// 读取excel文件, 将数据保存到数据库中, 此处省略具体代码}
}
public class JsonExport implements FileExport{@Overridepublic 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
第一个问题:
当我们要实现具体将某中文件数据导出到数据库时, 可以把大致过程划分为以下几步
- 检查参数中的filepath是否合法
- 路径是否不为空
- 文件是否存在
- 文件类型是否和对应策略类类型一致
- 读取文件数据到一个Java对象中
- 对数据进行处理,比如去除空格之类的,这里就是简单模拟一下
- 注意, 有的文件读取后需要处理, 有的不需要,这里假设json文件需要做额外处理, 但是csv和excel文件不需要读取数据后做处理
- 保存到数据库中
- 将处理后的数据转为数据表对应的实体类
- 使用mybatis/jpa/jdbc等orm工具保存到数据库中
通过上述的过程我们发现,每个策略类的具体实现经历的大体步骤/框架都相同,只有少部分的代码/逻辑不同,如果每个类都自己写自己的具体实现,就会导致大量的重复代码。
第二个问题:
什么是动态使用策略类?简而言之, 就是根据传入的参数, 或者根据某些情况来决定使用哪个策略类来处理.
现在只能传入FileExport类型的参数,如果我要传入String类型的filePath或者其他标识文件类型的参数,就又会导致因判断属于哪个FileExport而产生if-else,代码如下
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);}}
}
思想
接下来, 我们用模板方法模式来解决第一个问题, 也就是不同实现类中的代码重复问题。
模板方法模式会在抽象类或者接口中定义一个算法的整体流程, 该流程中会调用不同的方法. 这些方法的具体实现交给不同的子类完成. 也就是说它适合整体流程固定, 具体细节不同的场景.
实现
定义一个抽象类来当模板类
- 具体方法void check(String filepath): 检查filepath是否合法
- 具体方法 void fileDataExport2Db(FileData fileData): 导出数据到数据库
- 实现void export2Db(String filepath): 调用以上四个抽象方法来完成文件导出到数据库
- 抽象方法needProcessData():是否需要进行数据处理
- 抽象方法 FileData readFile(String filepath): 来读取文件数据
- 抽象方法 FileData processData(FileData fileData): 来处理数据
public interface FileExport {void export2Db(String filepath);
}
public abstract class AbstractFileExport implements FileExport {@Overridepublic 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";@Overrideprotected String getFileType() {return FILE_TYPE;}@Overrideprotected boolean needProcessData() {return false;}protected FileData readFile(String filepath) {System.out.println("以json方式读取filepath中的文件");System.out.println("将读取后的结果转为通用的FileData类型");return new FileData();}
}
大量重复代码和流程都被抽取到父类中了. 策略模式中出现的代码重复问题就解决了.
如何拓展
和之前类似, 如果后续需求变更, 需要拓展其他文件格式导出到数据库, 比如yml文件导出到数据库. 那么我们新增YmlExport类, 继承AbstractFileExport即可.
由于AbstractFileExport规定了统一流程, 且提供了 check(), fileDataExport2Db()等方法, 所以后续拓展起来代码量也会更少, 更方便.
工厂模式
前面还剩下一个问题,就是根据传入的参数动态的调用。通过工厂+枚举类来实现。
工厂模式就是用来创建对象的,可以根据参数的不同返回不同的实例。
三种工厂模式的区别-CSDN博客
这里使用简单工厂模式
实现
枚举类
@Getter
@AllArgsConstructor
@ToString
public enum FileType {JSON("json"),CSV("csv");private final String type;private static final Map<String, FileType> VALUE_MAP = Arrays.stream(values()).collect(Collectors.toMap(FileType::getType,Function.identity(),(existing, replacement)->replacement));public static FileType stringParseObject(String fileType) {if(!VALUE_MAP.containsKey(fileType)){throw new IllegalArgumentException("不支持的文件类型");}return VALUE_MAP.get(fileType);}
}
工厂类
public class FileExportFactory {private static final Map<FileType, FileExport> CACHE = new HashMap<>();static {CACHE.put(FileType.JSON, new JsonExport());CACHE.put(FileType.CSV, new CsvExport());}public static FileExport getFileExport(FileType fileType) {if (!CACHE.containsKey(fileType)) {throw new IllegalArgumentException("找不到对应类型:" + fileType.getType());}return CACHE.get(fileType);}public static FileExport getFileExport(String type) {FileType fileType = FileType.from(type);return getFileExport(fileType);}
}
问题及解决(解耦)
可以发现,这种情况下如果要增加新的新的文件类型,那么就需要更改FileExportFactory工厂类的代码,违反了OOP原则中的开闭原则(当应用需求发生改变的时候,我们尽量不要修改源代码,可以对其进行扩展,扩展的功能块不会影响到原来的功能块)。
解决方法
spring的解决方法有两种
- @Component/@Bean,使用注解方式,动态添加新的文件类型
- spring.factories,使用kv键值对,配置了需要自动装配类的全类名
配置文件方式
在resource文件夹下的yml配置文件中定义需要用到的全类名,然后读取出来。也可以通过反射拿到所有实现FileExport接口的类,然后筛选拿到需要用到的类。
这里是在枚举类中定义好相应的全类名,这样在工厂类中可以直接拿到。理由:实现类很少,操作简便。
枚举类
@Getter
@AllArgsConstructor
@ToString
public enum FileType {JSON("json", "com.luxiya.design.JsonExport"),CSV("csv","com.luxiya.design.CsvExport");private final String type;private final String className;private static final Map<String, FileType> VALUE_MAP = Arrays.stream(values()).collect(Collectors.toMap(FileType::getType,Function.identity(),(existing, replacement)->replacement));public static FileType stringParseObject(String fileType) {if(!VALUE_MAP.containsKey(fileType)){throw new IllegalArgumentException("不支持的文件类型");}return VALUE_MAP.get(fileType);}@SneakyThrowspublic FileExport classNameParseObject() {Class<?> clazz = Class.forName(this.getClassName());return (FileExport) clazz.newInstance();}
}
工厂类
public class FileExportFactory {private static final Map<FileType, FileExport> Cache;static {Cache = Arrays.stream(FileType.values()).map(fileType -> new Pair<>(fileType, fileType.classNameParseObject())).collect(Collectors.toMap(Pair::getKey,Pair::getValue,(existing, replacement)-> replacement));}public static FileExport getFileExport(FileType fileType) {if (!Cache.containsKey(fileType)) {throw new IllegalArgumentException("不支持的文件类型");}return Cache.get(fileType);}public static FileExport getFileExport(String fileType) {FileType fileTypeNew = FileType.stringParseObject(fileType);System.out.println(fileTypeNew);return getFileExport(fileTypeNew);}
}
这样如果新增YmlExport类,增加实现类,然后在枚举类中修改。
使用注解
使用注解实现解耦的流程大概如下
- 定义注解,并在相应的类上添加注解。
- 通过反射机制拿到添加了注解的类,放入工厂。
定义注解,并在JsonExport和CsvExport类上添加该注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FileExportComponent {
}
工厂类拿到所需类
public class FileExportFactory {private static final Map<FileType, FileExport> Cache;static {Set<Class<?>> classes = ClassUtil.scanPackage("com.luxiya.design", FileExport.class::isAssignableFrom);Cache = classes.stream().filter(ClassUtil::isNormalClass).filter(clazz -> AnnotationUtil.hasAnnotation(clazz, FileExportComponent.class)).map(ReflectUtil::newInstance).map(fileExport -> (FileExport) fileExport).collect(Collectors.toMap(FileExport::getSupportType,Function.identity(),(existing, replacement) -> replacement));}public static FileExport getFileExport(FileType fileType) {if (!Cache.containsKey(fileType)) {throw new IllegalArgumentException("不支持的文件类型");}return Cache.get(fileType);}public static FileExport getFileExport(String fileType) {FileType fileTypeNew = FileType.stringParseObject(fileType);System.out.println(fileTypeNew);return getFileExport(fileTypeNew);}
}
单例模式
保证一个类只有一个实例,并提供一个全局访问他的访问点,避免一个全局使用的类频繁的创建与销毁。
实现方式
1,懒汉式(线程不安全)
**是否 Lazy 初始化:**是
**是否多线程安全:**否
**实现难度:**易
**描述:**这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }
}
2,懒汉式(线程安全)
**是否 Lazy 初始化:**是
**是否多线程安全:**是
**实现难度:**易
**描述:**这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
- 优点:第一次调用才初始化,避免内存浪费。
- 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
- getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }
}
3,饿汉式
**是否 Lazy 初始化:**否
**是否多线程安全:**是
**实现难度:**易
**描述:**这种方式比较常用,但容易产生垃圾对象。
- 优点:没有加锁,执行效率会提高。
- 缺点:类加载时就初始化,浪费内存。
public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; }
}
4,双重校验锁机制(面)
**是否 Lazy 初始化:**是
**是否多线程安全:**是
**实现难度:**较复杂
**描述:**这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。
private static volatile Singleton singleton;private Singleton(){}public static Singleton getInstance(){if(singleton == null){synchronized (Singleton.class){if(singleton == null){singleton = new Singleton();}}}return singleton;}
5,静态内部类
**是否 Lazy 初始化:**是
**是否多线程安全:**是
利用 ClassLoader 的特性:
- 类的静态变量在第一次加载类时初始化,JVM 保证这一过程是线程安全的。
- 静态内部类(如
SingletonHolder
)不会随外部类(Singleton
)的加载而加载,只有在被显式调用时才加载。
public class Singleton {private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}private Singleton() {}public static Singleton getInstance() {return SingletonHolder.INSTANCE;}
}
6,枚举
**是否 Lazy 初始化:**否
**是否多线程安全:**是
**实现难度:**易
**描述:**这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
public enum Singleton { INSTANCE; public void whateverMethod() { }
}
体现
上述需求中,其实FileFactory工厂类的Map存储了所有FileExport的实现类,所用代码中用到的都是Map中的实现类,就是单例模式。
且用到的是枚举创建的对象,而且不会被反射和反序列化破坏。
享元模式
通过共享对象来减少系统对象的数量,本质就是缓存对象,降低内存消耗。
享元(Flyweight)的核心思想很简单:如果一个对象实例一经创建就不可变,那么反复创建相同的实例就没有必要,直接向调用方返回一个共享的实例就行,这样即节省内存,又可以减少创建对象的过程,提高运行速度。
享元模式在Java标准库中有很多应用。我们知道,包装类型如Byte
、Integer
都是不变类,因此,反复创建同一个值相同的包装类型是没有必要的。以Integer
为例,如果我们通过Integer.valueOf()
这个静态工厂方法创建Integer
实例,当传入的int
范围在-128
~+127
之间时,会直接返回缓存的Integer
实例:
// 享元模式
public class Main {public static void main(String[] args) throws InterruptedException {Integer n1 = Integer.valueOf(100);Integer n2 = Integer.valueOf(100);System.out.println(n1 == n2); // true}
}
对于Byte
来说,因为它一共只有256个状态,所以,通过Byte.valueOf()
创建的Byte
实例,全部都是缓存对象。
因此,享元模式就是通过工厂方法创建对象,在工厂方法内部,很可能返回缓存的实例,而不是新创建实例,从而实现不可变实例的复用。
其实FileFactory工厂类的Map就是共享对象,运用到了享元模式。
门面模式
一文搞懂设计模式—门面模式-CSDN博客
门面模式(Facade Pattern)也叫做外观模式,是一种结构型设计模式。它提供一个统一的接口,封装了一个或多个子系统的复杂功能,并向客户端提供一个简单的调用方式。通过引入门面,客户端无需直接与子系统交互,而只需要通过门面来与子系统进行通信。
角色 | 职责 |
---|---|
门面(Facade) | 提供统一接口,封装子系统的功能调用,隐藏内部细节。 |
子系统(Subsystem) | 实现具体功能的多个模块或类,不直接对外暴露,由门面协调调用。 |
客户端(Client) | 通过门面对象间接调用子系统功能,无需依赖具体子系统类。 |
简单门面类
public class FileExportClient {public static void exportToDb(String filePath){String type = FileTypeUtil.getTypeByPath(filePath);FileExport fileExport = FileExportFactory.getFileExport(type);fileExport.export(filePath);}public static void exportToDb(File file){String filePath = file.getAbsolutePath();exportToDb(filePath);}}