浅谈装饰模式

一、前言

hello大家好,本次打算简单聊一下装饰者模式,其实写有关设计模式的内容还是蛮有挑战性的,首先呢就是小永哥实力有限担心说不明白,其次设计模式是为了解决某些问题场景,在当前技术生态圈如此完善的情况下,能遇到使用设计模式的时候其实还是蛮少见的,而且我一直认为解决方案本身没有好坏之分,只有经过取舍适合当前的才是最好的。任何事物都是双刃剑,包括设计模式,在没有适合场景的情况下强行使用反而会造成过度设计,这样就不好了。

不过呢前几天小永哥确实遇到了一个小场景能用一下装饰者模式,不过作为演示呢我就不完整复刻那些复杂的代码了,咱来点简易的类比一下,尽量以少的代码能说明白。

二、业务场景

当时的业务场景是一个Excel导入功能,我们都知道导入时,需要读取逐行读取Excel数据,每行数据又要对应列的数据读取出来赋值给实体类对象,每一行就是一个实体类对象,最后将这些实体类对象设置到提前声明好的list集合中并调用批量保存完成导入,简易导入请看下述代码。

java 复制代码
package com.relation;

import com.alibaba.fastjson2.JSON;
import org.apache.poi.ss.usermodel.*;
import org.junit.jupiter.api.Test;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * @author huhy
 * @version 1.0
 * @Description:
 * @ClassName Date:2025/5/10 20:20
 */
public class ImportTest {
    @Test
    public void test() throws IOException {
        ArrayList<Map<String,Object>> mapList = new ArrayList<>();
        // 导入的源文件
        String path = "E:\\test\\demo.xlsx";
        // 工作薄对象
        Workbook workbook = WorkbookFactory.create(new FileInputStream(path));
        // 工作表 sheet
        Sheet sheet = workbook.getSheetAt(0);
        // 行
        int rows = sheet.getPhysicalNumberOfRows();
        DataFormatter dataFormatter = new DataFormatter();
        // 行的头信息,第一行,可以不处理
        for (int i = 1; i < rows; i++) {
            Row row = sheet.getRow(i);
            Map<String,Object> demoData = new HashMap<>();
            // 第一个单元格,因为是序号,可以不要
            // 从第二个单元格开始获取名称
            String name = dataFormatter.formatCellValue(row.getCell(1));
            demoData.put("name",name);
            // 第三个单元格获取编号
            String code = dataFormatter.formatCellValue(row.getCell(2));
            demoData.put("code",code);
            // 把组装好的 student对象,存入集合
            mapList.add(demoData);
        }
        System.out.println(JSON.toJSONString(mapList));
    }
}

以上就是一个简易的excel上传代码,运行测试类也能从excel中获取到数据,一个很正常和普通的小需求,想来以各位老铁的本事,这些都不叫事。下面有请我们T哥给我上点强度。

三、来自T哥的折磨

T哥:一般找我来都没啥好事,不过能给你填点堵我还是很乐意的。

小白:T哥你客气了,有事您吩咐,没有困难我制造困难也给你整明白儿的。

T哥:好了,废话少说,为了减少用户手输的错误率,你看能不能给表格内容去空格呀,防止用户不小心在前后多输空格然后又检查不出来。

小白:那太简单了,你就瞧好吧......

3.1、去空格需求的实现。

很简单嘛,给设置值的位置调用一下String自带的trim()方法不就好了,很轻松呀。

3.2、来自T哥的新需求。

T哥:这么快就实现了,不愧是专业的java开发人员,那个什么,客户要求除去空格之外,需要将每个数据都加上一个前缀AAA。这个能实现吧。

小白:好吧,也不是什么问题。

我们可以看到小白很快又搞定了,但是小白陷入了沉思。

小白:不对呀,为什么我每次都要改所有单元格设置的位置呢?我得想办法写个方法把这些集中起来,这样我只需要在第一次修改的时候改动位置稍微多一点,以后每次我修改集中起来的代码就好了。说干就干。

java 复制代码
package com.relation;

import com.alibaba.fastjson2.JSON;
import com.relation.common.utils.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.junit.jupiter.api.Test;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * @author huhy
 * @version 1.0
 * @Description:
 * @ClassName Date:2025/5/10 20:20
 */
public class ImportTest {
    @Test
    public void test() throws IOException {
        // 将Excel文件导入到数据库中
        ArrayList<Map<String,Object>> mapList = new ArrayList<>();
        // 导入的源文件
        String path = "E:\\test\\demo.xlsx";
        // 工作薄对象
        Workbook workbook = WorkbookFactory.create(new FileInputStream(path));
        // 工作表 sheet
        Sheet sheet = workbook.getSheetAt(0);
        // 行
        int rows = sheet.getPhysicalNumberOfRows();
        DataFormatter dataFormatter = new DataFormatter();
        // 行的头信息,第一行,可以不处理
        for (int i = 1; i < rows; i++) {
            Row row = sheet.getRow(i);
            Map<String,Object> demoData = new HashMap<>();
            // 第一个单元格,因为是序号,可以不要
            // 从第二个单元格开始获取名称
            String name = dataFormatter.formatCellValue(row.getCell(1));
            demoData.put("name",getStr(name));
            // 第三个单元格获取编号
            String code = dataFormatter.formatCellValue(row.getCell(2));
            demoData.put("code",getStr(code));
            // 把组装好的 student对象,存入集合
            mapList.add(demoData);
        }
        System.out.println(JSON.toJSONString(mapList));
    }

    private String getStr(String data){
        if(StringUtils.isEmpty(data)){
            return data;
        }
        return "AAA"+data.trim();
    }
}

小白抽取了一个方法,专门用来处理表格内容,好像比之前要好很多了。

T哥:你进步了,已经开始能自查并完善自己的代码了......

小白:当然了,每次都改那么多地方,我累,能少改干嘛要多改呢?

T哥:那既然你这么说,那你抽取getStr方法之后,不还是得在第一次修改的时候,逐个找到每个获取表格的位置去替换为使用getStr方法吗?

小白:还好吧,只有两处而已,有必要再折腾吗?

T哥:没错,现在确实是只有名称和编码两个属性,那万一有几十个属性呢?你也要一个个去修改吗?不是你自己说能少写一行代码就不多写一行吗?

小白:那好吧,我再想想办法,毕竟我是一个面向对象工程师......

3.3、面向对象方式改造
java 复制代码
package com.relation;

import com.relation.common.utils.StringUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;

/**
 * @author huhy
 * @version 1.0
 * @Description:
 * @ClassName Date:2025/5/10 21:47
 */
public class DataFormatterExpand extends DataFormatter {

    @Override
    public String formatCellValue(Cell cell) {
        String cellValue = super.formatCellValue(cell);
        if(StringUtils.isEmpty(cellValue)){
            return cellValue;
        }
        return "AAA"+cellValue.trim();
    }
}
java 复制代码
package com.relation;

import com.alibaba.fastjson2.JSON;
import com.relation.common.utils.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.junit.jupiter.api.Test;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * @author huhy
 * @version 1.0
 * @Description:
 * @ClassName Date:2025/5/10 20:20
 */
public class ImportTest {
    @Test
    public void test() throws IOException {
        // 将Excel文件导入到数据库中
        ArrayList<Map<String,Object>> mapList = new ArrayList<>();
        // 导入的源文件
        String path = "E:\\test\\demo.xlsx";
        // 工作薄对象
        Workbook workbook = WorkbookFactory.create(new FileInputStream(path));
        // 工作表 sheet
        Sheet sheet = workbook.getSheetAt(0);
        // 行
        int rows = sheet.getPhysicalNumberOfRows();
        DataFormatterExpand dataFormatter = new DataFormatterExpand();
        // 行的头信息,第一行,可以不处理
        for (int i = 1; i < rows; i++) {
            Row row = sheet.getRow(i);
            Map<String,Object> demoData = new HashMap<>();
            // 第一个单元格,因为是序号,可以不要
            // 从第二个单元格开始获取名称
            String name = dataFormatter.formatCellValue(row.getCell(1));
            demoData.put("name",name);
            // 第三个单元格获取编号
            String code = dataFormatter.formatCellValue(row.getCell(2));
            demoData.put("code",name);
            // 把组装好的 student对象,存入集合
            mapList.add(demoData);
        }
        System.out.println(JSON.toJSONString(mapList));
    }
}

我们可以看到,小白扩展了一个类并继承了DataFormatter并重写了formatCellValue方法,先调用父类的formatCellValue,然后再对获取的值进行统一扩展完成任务。

T哥:很好,一个非常标准的OO设计。

小白:当然了,我可是一个专业的OO程序员。

T哥:其实做到这一步已经设计的很不错了,代码改动也少,变化的代码也抽取出去了,那还有可以改进的地方吗?

小白:我不是很明白,现在不是很完美了吗?为什么还要改进?

T哥:如果针对此功能需求到此为止,那你的设计可以说完美,但是如果后续还有变化呢?你还能优雅保持你的扩展性吗?

小白:我想是可以的。

T哥:好的,那咱们继续,

3.4、T哥的BUFF叠加

T哥:来新需求了,我们需要再加上BBB后缀。

小白:这简单,我继续修改一下。

T哥:新需求来了,要求把AAA前缀去掉......

小白:T哥,你怕不是玩死我吧?

T哥:你说的什么话,不是你让我来给你提需求吗?

小白:我*******

T哥:我*******

然后T哥和小白打起来了.........

小永哥:小白你先放手,这事儿哥帮你办了,你先消消气。

3.5、小永哥的改造
java 复制代码
package com.relation.expand;

import com.relation.common.utils.StringUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;

/**
 * @author huhy
 * @version 1.0
 * @Description:
 * @ClassName Date:2025/5/10 21:47
 */
public class DataFormatterTrimExpand extends DataFormatter {

    private DataFormatter dataFormatter;

    public void setDataFormatter(DataFormatter dataFormatter) {
        this.dataFormatter = dataFormatter;
    }

    @Override
    public String formatCellValue(Cell cell) {
        String cellValue = dataFormatter.formatCellValue(cell);
        if(StringUtils.isEmpty(cellValue)){
            return cellValue;
        }
        return cellValue.trim();
    }
}
java 复制代码
package com.relation.expand;

import com.relation.common.utils.StringUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;

/**
 * @author huhy
 * @version 1.0
 * @Description:
 * @ClassName Date:2025/5/10 21:47
 */
public class DataFormatterPrefixExpand extends DataFormatter {

    private DataFormatter dataFormatter;

    public void setDataFormatter(DataFormatter dataFormatter) {
        this.dataFormatter = dataFormatter;
    }

    @Override
    public String formatCellValue(Cell cell) {
        String cellValue = dataFormatter.formatCellValue(cell);
        if(StringUtils.isEmpty(cellValue)){
            return cellValue;
        }
        return "AAA"+cellValue;
    }
}
java 复制代码
package com.relation.expand;

import com.relation.common.utils.StringUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;

/**
 * @author huhy
 * @version 1.0
 * @Description:
 * @ClassName Date:2025/5/10 21:47
 */
public class DataFormatterSuffixExpand extends DataFormatter {

    private DataFormatter dataFormatter;

    public void setDataFormatter(DataFormatter dataFormatter) {
        this.dataFormatter = dataFormatter;
    }

    @Override
    public String formatCellValue(Cell cell) {
        String cellValue = dataFormatter.formatCellValue(cell);
        if(StringUtils.isEmpty(cellValue)){
            return cellValue;
        }
        return cellValue+"BBB";
    }
}
java 复制代码
package com.relation.expand;

import org.apache.poi.ss.usermodel.DataFormatter;

/**
 * @author huhy
 * @version 1.0
 * @Description:
 * @ClassName Date:2025/5/10 22:27
 */
public class DataFormatterFactory {

    public static DataFormatter createDataFormatter(){
        DataFormatter dataFormatter = new DataFormatter();
        //创建去空格扩展类
        DataFormatterTrimExpand dataFormatterTrimExpand = new DataFormatterTrimExpand();
        //创建加前缀扩展类
        DataFormatterPrefixExpand dataFormatterPrefixExpand = new DataFormatterPrefixExpand();
        //创建加后缀扩展类
        DataFormatterSuffixExpand dataFormatterSuffixExpand = new DataFormatterSuffixExpand();
        //按需求进行组合
        //将原生DataFormatter设置到去空格扩展类来达到去空格功能
        dataFormatterTrimExpand.setDataFormatter(dataFormatter);
        //将去空格扩展类设置到加前缀扩展类,来给去空格后的值加前缀
        dataFormatterPrefixExpand.setDataFormatter(dataFormatterTrimExpand);
        //将加前缀扩展类设置到加后缀扩展类,给加完前缀值在加上后缀
        dataFormatterSuffixExpand.setDataFormatter(dataFormatterPrefixExpand);
        return dataFormatterSuffixExpand;
    }
}
java 复制代码
package com.relation;

import com.alibaba.fastjson2.JSON;
import com.relation.expand.DataFormatterFactory;
import com.relation.expand.DataFormatterTrimExpand;
import org.apache.poi.ss.usermodel.*;
import org.junit.jupiter.api.Test;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * @author huhy
 * @version 1.0
 * @Description:
 * @ClassName Date:2025/5/10 20:20
 */
public class ImportTest {
    @Test
    public void test() throws IOException {
        // 将Excel文件导入到数据库中
        ArrayList<Map<String,Object>> mapList = new ArrayList<>();
        // 导入的源文件
        String path = "E:\\test\\demo.xlsx";
        // 工作薄对象
        Workbook workbook = WorkbookFactory.create(new FileInputStream(path));
        // 工作表 sheet
        Sheet sheet = workbook.getSheetAt(0);
        // 行
        int rows = sheet.getPhysicalNumberOfRows();
        DataFormatter dataFormatter = DataFormatterFactory.createDataFormatter();
        // 行的头信息,第一行,可以不处理
        for (int i = 1; i < rows; i++) {
            Row row = sheet.getRow(i);
            Map<String,Object> demoData = new HashMap<>();
            // 第一个单元格,因为是序号,可以不要
            // 从第二个单元格开始获取名称
            String name = dataFormatter.formatCellValue(row.getCell(1));
            demoData.put("name",name);
            // 第三个单元格获取编号
            String code = dataFormatter.formatCellValue(row.getCell(2));
            demoData.put("code",code);
            // 把组装好的 student对象,存入集合
            mapList.add(demoData);
        }
        System.out.println(JSON.toJSONString(mapList));
    }
}

我们可以看到,经过小永哥的改造,前缀和后缀都加上了,但是好像代码量多了很多。

小白:永哥,你这是什么名堂,为啥你改造完以后代码量反而更多了呢?

小永哥:你说的对,初期确实代码量比你的OO设计要多了不少,待我给你好好介绍一下。

3.6、代码解析

1)、如下图所示,我们为每一种扩展都创建了对应的实现类。

2)、我们改造了扩展类,我们加了一个DataFormatter类型的成员变量,我们不再调用父类的方法,而是调用成员变量的方法。

3)、这样做的好处是,成员变量可以是原生的类,也可是我们的扩展类,这样的话,我们就可以按照需求灵活对扩展类进行组合。

4)、我们创建了一个简易的工厂DataFormatterFactory来生成DataFormatter,将我们组合的逻辑封装在工厂类的createDataFormatter方法中,从此刻开始,我们已经将所有变化的东西,从我们的主业务逻辑中抽取了出来。

5)、经过一系列的改造,我们的代码可扩展性和可维护性已经大大的提高了,需求变化时,如果我们已经有对应的扩展类,则直接进行组合即可,如果没有对应的扩展类,我们可以创建新的扩展类,并进行新的组合。

T哥:小永哥有两下子哈,小白你能总结一下你小永哥这么做符合哪些原则吗?

小白:首先,小永哥对每一种扩展创建对应的实现类,满足了单一职责。对于新的扩展,创建新的实现类,而不对业务代码进行修改,符合开闭原则,但好像对工厂类有改动,好像又没有很完美的符合开闭原则。

T哥:你说的很对,开闭原则是对修改关闭,对新增开放以大大减少对已有稳定代码的改动,以减少需求变化对系统的影响,但是要完全做到开闭原则很难,我们在开发过程中或多或少都会涉及到对原有代码的改动,这个是不可避免的,小永哥可以说将代码改动控制在很小范围之内了,如果出现问题,排查范围也会小很多。

小永哥:不好意思,我插一句哈,在扩展类的改造中,我使用了组合模式,利用组合模式松耦合的特性来提升系统的弹性,小白这一点你要牢记,有时候有一个比是一个要更好,说的更直白一点,就是组合模式要比继承的灵活性更强。

小白:永哥,我记住了。

3.7、总结

经过了一系列的改造,我们已经将代码改造好了,特别是最后一次改造,运用的就是经典的装饰模式。通过自由组合的方式对原始的类进行层层装饰,从而达到我们的目的。

四、结语

又耗费了不少脑细胞,写了这么多希望可以通过这个简单的案例将装饰模式能聊清楚,装饰模式虽然很经典,但是在小永哥的心中,威力最大的还是策略模式,特别是策略模式+模版方法模式,威力巨大,能灵活多变的应对繁琐的业务变化,小永哥好好构思一下业务场景,争取下把和大家聊聊这两种设计模式以及这两种设计模式的组合体,本次就到这了,谢谢大家,晚安。

相关推荐
天上掉下来个程小白1 分钟前
缓存套餐-01.Spring Cache入门案例
java·redis·spring·缓存·springboot·springcache
深色風信子14 分钟前
Eclipse 插件开发 6 右键菜单
java·ide·eclipse·右键菜单
网安INF17 分钟前
Apache Shiro 1.2.4 反序列化漏洞(CVE-2016-4437)
java·网络安全·apache
it-搬运工36 分钟前
远程调用负载均衡LoadBalancer
java·微服务·负载均衡
努力努力再努力wz41 分钟前
【Linux实践系列】:进程间通信:万字详解共享内存实现通信
java·linux·c语言·开发语言·c++
-曾牛1 小时前
Azure OpenAI 聊天功能全解析:Java 开发者指南
java·开发语言·人工智能·spring·flask·azure·大模型应用
zhojiew2 小时前
service mesh的定制化与性能考量
java·云原生·service_mesh
cdut_suye2 小时前
【Linux系统】从零开始构建简易 Shell:从输入处理到命令执行的深度剖析
java·linux·服务器·数据结构·c++·人工智能·python
-qOVOp-2 小时前
zst-2001 历年真题 设计模式
java·算法·设计模式
张狂年少2 小时前
【十五】Mybatis动态SQL实现原理
java·sql·mybatis