假设做一个需求,从文件中拿到数据并存在数据库中,文档有多种不同的类型,比如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);
}
}
}
这里可以看到有很多问题,比如
策略模式是多态最好的体现, 也是解决这种标签类的最好的方式之一.
策略模式的定义为: 在策略模式定义了一系列策略类,并将每个具体实现封装在独立的类中,使得它们可以互相替换。通过使用策略模式,可以在运行时根据需要选择不同的算法,而不需要修改调用端代码。是一种用来解决很多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即可.
那么, 目前的代码就不存在问题了吗? 当然不是, 我们来看策略模式常见的两个问题
第一个问题:
当我们要实现具体将某中文件数据导出到数据库时, 可以把大致过程划分为以下几步
通过上述的过程我们发现,每个策略类的具体实现经历的大体步骤/框架都相同,只有少部分的代码/逻辑不同,如果每个类都自己写自己的具体实现,就会导致大量的重复代码。
第二个问题:
什么是动态使用策略类?简而言之, 就是根据传入的参数, 或者根据某些情况来决定使用哪个策略类来处理.
现在只能传入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);
}
}
}
接下来, 我们用模板方法模式来解决第一个问题, 也就是不同实现类中的代码重复问题。
模板方法模式会在抽象类或者接口中定义一个算法的整体流程, 该流程中会调用不同的方法. 这些方法的具体实现交给不同的子类完成. 也就是说它适合整体流程固定, 具体细节不同的场景.
定义一个抽象类来当模板类
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()等方法, 所以后续拓展起来代码量也会更少, 更方便.
前面还剩下一个问题,就是根据传入的参数动态的调用。通过工厂+枚举类来实现。
工厂模式就是用来创建对象的,可以根据参数的不同返回不同的实例。
三种工厂模式的区别-CSDN博客
这里使用简单工厂模式
枚举类
@Getter
@AllArgsConstructor
@ToString
public enum FileType {
JSON("json"),
CSV("csv");
private final String type;
private static final Map 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的解决方法有两种
在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 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 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 Cache;
static {
Set> 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);
}
}
保证一个类只有一个实例,并提供一个全局访问他的访问点,避免一个全局使用的类频繁的创建与销毁。
**是否 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;
}
}
**是否 Lazy 初始化:**是
**是否多线程安全:**是
**实现难度:**易
**描述:**这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
**是否 Lazy 初始化:**否
**是否多线程安全:**是
**实现难度:**易
**描述:**这种方式比较常用,但容易产生垃圾对象。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
**是否 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;
}
**是否 Lazy 初始化:**是
**是否多线程安全:**是
利用 ClassLoader 的特性:
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;
}
}
**是否 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);
}
}