设计模式(策略,工厂,单例,享元,门面)+模板方法

发布于:2025-06-29 ⋅ 阅读:(20) ⋅ 点赞:(0)

前提

假设做一个需求,从文件中拿到数据并存在数据库中,文档有多种不同的类型,比如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即可.

模板方法

存在的问题

那么, 目前的代码就不存在问题了吗? 当然不是, 我们来看策略模式常见的两个问题

  1. 不同实现类中代码重复(靠模板方法解决)
  2. 如果想要根据传入参数动态使用某个策略类, 还是避免不了大量if else

第一个问题:

当我们要实现具体将某中文件数据导出到数据库时, 可以把大致过程划分为以下几步

  1. 检查参数中的filepath是否合法
    • 路径是否不为空
    • 文件是否存在
    • 文件类型是否和对应策略类类型一致
  2. 读取文件数据到一个Java对象中
    • 对数据进行处理,比如去除空格之类的,这里就是简单模拟一下
    • 注意, 有的文件读取后需要处理, 有的不需要,这里假设json文件需要做额外处理, 但是csv和excel文件不需要读取数据后做处理
  3. 保存到数据库中
    • 将处理后的数据转为数据表对应的实体类
    • 使用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()等方法, 所以后续拓展起来代码量也会更少, 更方便.

工厂模式

前面还剩下一个问题,就是根据传入的参数动态的调用。通过工厂+枚举类来实现。

工厂模式就是用来创建对象的,可以根据参数的不同返回不同的实例。

三种工厂模式的区别-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);
    }

    @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类,增加实现类,然后在枚举类中修改。

使用注解

使用注解实现解耦的流程大概如下

  1. 定义注解,并在相应的类上添加注解。
  2. 通过反射机制拿到添加了注解的类,放入工厂。

定义注解,并在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标准库中有很多应用。我们知道,包装类型如ByteInteger都是不变类,因此,反复创建同一个值相同的包装类型是没有必要的。以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);
    }

}


网站公告

今日签到

点亮在社区的每一天
去签到