文章目录
前提
假设做一个需求,从文件中拿到数据并存在数据库中,文档有多种不同的类型,比如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{
@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
第一个问题:
当我们要实现具体将某中文件数据导出到数据库时, 可以把大致过程划分为以下几步
- 检查参数中的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 {
@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();
}
}
大量重复代码和流程都被抽取到父类中了. 策略模式中出现的代码重复问题就解决了.
如何拓展
和之前类似, 如果后续需求变更, 需要拓展其他文件格式导出到数据库, 比如yml文件导出到数据库. 那么我们新增YmlExport类, 继承AbstractFileExport即可.
由于AbstractFileExport规定了统一流程, 且提供了 check(), fileDataExport2Db()等方法, 所以后续拓展起来代码量也会更少, 更方便.
工厂模式
前面还剩下一个问题,就是根据传入的参数动态的调用。通过工厂+枚举类来实现。
工厂模式就是用来创建对象的,可以根据参数的不同返回不同的实例。
这里使用简单工厂模式
实现
枚举类
@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);
}
@SneakyThrows
public 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就是共享对象,运用到了享元模式。
门面模式
门面模式(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);
}
}