实际项目运用之Decorator模式(装饰器模式)

1 概述

在项目中,经常因一些新增需求,导致同一业务的变更,如果所在类继承关系如下:Parent、Child、Grandparent,那么要在Child类上增强些功能怎么办?给Child类增加方法?那会对Grandparent产生什么影响?该如何去处理?看完本文,你会找到你的答案。

JavaIO中,像下面的嵌套语句很常见,为什么要怎样定义呢?理解装饰模式后,你会找到答案。

FilterInputStream filterInputStreasm = new BufferedInputStream(new FileInputStream(new File("/user/a")));

1.1案例

例如下面一个功能需求,4s店的汽车销售向客户推销自家品牌的产品,我们用代码实现,关系如下:

类图

具体代码: 汽车销售类

public abstract class CarSale {

    /**
     * 推销车的详情
     */
    public abstract void displayCarInfo();

    /**
     * 客户签订购买合同
     */
    public abstract void signContract(String customerName);

}

汽车参数详情

public class CarInfo extends CarSale {

    @Override
    public void displayCarInfo() {
        System.out.println("日本丰田GTR");
        System.out.println("百公里加速1秒");
        System.out.println("油耗偏高");
        System.out.println("后驱涡轮增压");
        System.out.println("内饰豪华");
        System.out.println("发动机噪音偏大");
        System.out.println("不支持电动座椅,后视镜加热");
    }

    @Override
    public void signContract(String customerName) {
        System.out.println("客户签约销售合同, 付款人:" + customerName);
    }
}

客户

public class Customer {

    public static void main(String[] args) {
        CarInfo carInfo = new CarInfo();
        //介绍车性能
        carInfo.displayCarInfo();
        //油耗太高??噪音大 推销我买?? 不存在的
       // carInfo.signContract();
    }

}

如果销售并没有很有针对性的去讲解,突出推销车型的优点,买家是不太会买账的,所以作为一个salesman我们需要修改一下我们的营销技巧,比如: 突出速度上的优势,突出后驱车这个特点, 修改后的图示关系如下: 类关系图

客户营销类

public class SalesTalk extends CarInfo {

    private void showSpeedRank() {
        System.out.println("这个车百公里加速快,外观吊炸天,装逼神器啊,大兄弟");
    }

    private void showWheelSys() {
        System.out.println("后驱车,让你漂移分分钟,就问对方怕不怕");
    }

    /**
     * 使用销售技巧,提高签约成功率
     */
    @Override
    public void displayCarInfo() {
        showSpeedRank();
        super.displayCarInfo();
        showWheelSys();
    }
}

客户2

public class Customer2 {

    public static void main(String[] args) {
        SalesTalk salesTalk = new SalesTalk();
        //销售一顿吹
        salesTalk.displayCarInfo();
        System.out.println("。。。。。");
        //艹,买买买
        salesTalk.signContract("不差钱哥");
    }

}

客户看到销售人员的激情推销,感觉有些心动,毕竟日本神车,最后还是忍不住宣传的力度,签订了销售合同。但是如果这时候,客户还是不买账,我们该怎么办,继续通过突出其他优势来营销么,这样会产生多少个子类,这样会导致类爆炸,类的数量激增,想想以后维护怎么办?并且在面向对象的设计中,如果超过两层继承,你就应该想想是不是出设计问题了,继承层次越多以后的维护成本越多,问题这么多,那怎么办?我们定义一批专门负责装饰的类,然后根据实际情况来决定是否需要进行装饰,类图稍做修正,如图所示 装饰修饰

增加一个抽象类和两个实现类,其中Decorator的作用是封装CarSale类

抽象类

public abstract class Decorator extends CarSale {

    /**
     * 销售技巧选择
     */
    private CarSale carSale;

    public Decorator(CarSale carSale) {
        this.carSale = carSale;
    }

    @Override
    public void displayCarInfo() {
        this.carSale.displayCarInfo();
    }

    @Override
    public void signContract(String customerName) {
        this.carSale.signContract(customerName);
    }
}

在构造器中定义需要装饰的类,然后调用displayCarInfo()进行修饰,有点类似代理模式

速度排名

public class SpeedRankDecorator extends Decorator {

    public SpeedRankDecorator(CarSale carSale) {
        super(carSale);
    }

    //突出速度特点
    private void showSpeedRank() {
        System.out.println("这个车百公里加速快,外观吊炸天,装逼神器啊,大兄弟");
    }

    //宣传
    @Override
    public void displayCarInfo() {
        showSpeedRank();
        super.displayCarInfo();
    }
}

驱动情况

public class WheelDeployDecorator extends Decorator {

    public WheelDeployDecorator(CarSale carSale) {
        super(carSale);
    }

    //突出后驱车的优势De
    private void showWheelSys() {
        System.out.println("后驱车,让你漂移分分钟,就问对方怕不怕");
    }

    @Override
    public void displayCarInfo() {
        showWheelSys();
        super.displayCarInfo();
    }
}

客户3

public class Customer3 {

    public static void main(String[] args) {
        //需要被装饰的类
        CarInfo carInfo = new CarInfo();
        System.out.println("-----------速度排名--------------");
        Decorator decorator = new SpeedRankDecorator(carInfo);
        decorator.displayCarInfo();

        System.out.println("---------汽车驱动-----------");
        decorator = new WheelDeployDecorator(carInfo);
        decorator.displayCarInfo();
        //听的很舒服那就买
        decorator.signContract("土豪");

    }

}

如上所示,通过装饰器,我们可以很轻松的包装我们想要的对象

2 装饰模式(Decorator Pattern)

概念

装饰器模式 允许向一个现有的对象添加新的功能,同时又不改变其结构。装饰者可以在所委托被装饰者的行为之前或之后加上自己的行为,以达到特定的目的。

2.1 组成

模式类图

装饰器模式由组件和装饰者组成。

抽象组件(Component):需要装饰的抽象对象。 具体组件(ConcreteComponent):是我们需要装饰的对象 抽象装饰类(Decorator):内含指向抽象组件的引用及装饰者共有的方法。 具体装饰类(ConcreteDecorator):被装饰的对象。

2.2 装饰模式的简化

如果只有一个ConcreteComponent类,那么可以考虑去掉抽象的Component类(接口),把Decorator作为一个ConcreteComponent子类。如下图所示: 装饰模式

如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,而可以把Decorator和ConcreteDecorator的责任合并成一个类。

透明性的要求 装饰模式对客户端的透明性要求程序不要声明一个ConcreteComponent类型的变量,而应当声明一个Component类型的变量。

用上面汽车推销的例子来说有:

        CarInfo carInfo = new CarInfo();
        System.out.println("-----------速度排名--------------");
        Decorator decorator = new SpeedRankDecorator(carInfo);
        decorator.displayCarInfo();

        System.out.println("---------汽车驱动-----------");
        decorator = new WheelDeployDecorator(carInfo);
        decorator.displayCarInfo();
        //听的很舒服那就买
        decorator.signContract("土豪");

而不是下面的做法:

 WheelDeployDecorator decorator = new WheelDeployDecorator(carInfo);

2.3 装饰者模式的优缺点

优点  可以动态的扩展功能;  装饰者和被装饰者解耦,互相不关联。

缺点:  多层装饰比较复杂。

装饰者模式的适用场景:  扩展一个类的功能;  动态增加和撤销功能。

注意:装饰者模式可以替代繁杂的继承,但其内部实现使用的也是继承。也就是说,装饰者模式将繁杂的继承转化成了其内部的简单的继承。

3 装饰模式IO流中的运用

装饰模式在Java语言中的最著名的应用莫过于Java I/O标准库的设计了。

由于Java I/O库需要很多性能的各种组合,如果这些性能都是用继承的方法实现的,那么每一种组合都需要一个类,这样就会造成大量性能重复的类出现。而如果采用装饰模式,那么类的数目就会大大减少,性能的重复也可以减至最少。因此装饰模式是Java I/O库的基本模式。

JavaI/O库的对象结构图如下,由于Java I/O的对象众多,因此只画出InputStream的部分。 inputstream

根据上图可以看出:

●抽象构件(Component)角色:由InputStream扮演。这是一个抽象类,为各种子类型提供统一的接口。

●具体构件(ConcreteComponent)角色:由ByteArrayInputStream、FileInputStream、PipedInputStream等类扮演。它们实现了抽象构件角色所规定的接口。

●抽象装饰(Decorator)角色:由FilterInputStream扮演。它实现了InputStream所规定的接口。

●具体装饰(ConcreteDecorator)角色:由几个类扮演,有BufferedInputStream、DataInputStream、HttpInputStream等

public class IOTest {

    public static void main(String[] args) {
        //抽象装饰器                              具体装饰器                      被装饰对象
        FilterInputStream filterInputStream = new BufferedInputStream(new ByteArrayInputStream("input-test".getBytes()));
        byte[] bs = new byte[0];
        try {
            bs = new byte[filterInputStream.available()];
            filterInputStream.read(bs);
        } catch (IOException e) {
            e.printStackTrace();
        }
        String content = new String(bs);
        System.out.println(content);
    }

}

像其他的OutputStream Read Writer 就列举了其抽象装饰器分别是 FilterOutputStream FilterReader FilterWriter

4 实际项目中的运用

4.1 需求

在金融项目数据加密是非常常见的,比如前端将加密后的字符串作为参数传给我们, 我们在接口中解密,再比如我们后端返回给前端的结果也是一串加密后的字符串,这是防止爬虫的一种安全手段,那么我们是否需要在每个接口在去进行加密解密?是否有一套机制去控制我想要加密的接口,同学们应该会想到只需在一个请求到达Controller之前能够截获其请求,并且根据其具体情况对 HttpServletRequest 中的参数进行过滤或者修改。然后再放回到该HttpServletRequest 中呢?

流只能读取一次,读取前端传过来的参数具体参数,如果是post请求,那么getInputStream()可以解析到详细的参数

在正式代码之前,我还是先简单介绍下ServletRequest、HttpServletRequest、ServletRequestWrapper以及HttpServletRequestWrapper这几个接口或者类之间的层次关系,并说明“继承HttpServletRequestWrapper类以实现在Filter中修改HttpServletRequest的参数”这种方式的原理是什么

它们之间的层次关系是这样的: tomcat-ServletRequest 关系图

如上图所示,在过滤器中:

//我们会进入这个方法
doFilter(ServletRequest request, ServletResponse response, FilterChain chain)

//通过ServletRequest进来的,之后我们会强制造型如下:
 HttpServletRequest req=(HttpServletRequest) request;
''''''
//最后调用
chain.doFilter(req, res);

如果需要对前端加密过来的参数进行解密,那么我们需要对HttpServletRequest进行处理,而上面图关系毫无疑问是一个装饰模式:

ServletRequest 抽象组件 HttpServletRequest 抽象组件的一个子类 ServletRequestWrapper 一个基本的装饰类,这里是非抽象的 HttpServletRequestWrapper 一个扩展装饰类 兼具体的装饰者

4.2 具体实现方案

既然是修改getInputStream()方法的内容,我们可以在HttpServletRequestWrapper 做具体的装饰器

下面具体的代码:

public class ParamsWrapperDecorator extends HttpServletRequestWrapper {

    private static final Logger logger = LoggerFactory.getLogger(ParamsWrapperDecorator.class);

    private HttpServletRequest request;

    public ParamsWrapperDecorator(HttpServletRequest request) {
        super(request);
        this.request = request;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException, RoborderException {
        String path = request.getRequestURI();
        //if (new Random().ints(0, 4).boxed().findFirst().orElse(-1) == 1) {
            LoggerUtil.showInfoLogDetails(logger, "IP:{}, 请求地址:{}",Optional.ofNullable(MDC.get("ip")).orElse("unknown"),path);
       // }
        final MethodRule  rule = MethodRule.getEnum(path);
        //处理传入传入参数是否加密
        ServletInputStream is = handleWithParamsDecrypt(rule);
        return is == null ? request.getInputStream() : is;

    }

    private ServletInputStream handleWithParamsDecrypt(final MethodRule rule) throws IOException, RoborderException {
        if (Optional.ofNullable(rule).isPresent() && rule.getParamsDecrypt()) {
           // 大致业务逻辑代码
            BufferedReader reader = request.getReader();
            String encryptData = String.join("", reader.lines().collect(Collectors.toList()));
            reader.close();
           .......................................
            String result;
            EncapDataDTO dto = JSONObject.parseObject(encryptData, EncapDataDTO.class);
            try {
                result = Optional.of(Des3Util.decode(dto.getEncryptData(), secretKey)).orElse("");
                LoggerUtil.showInfoLogDetails(logger, "真实传入参数:{}, 传入参数encaptData:{}, 密钥secretKey:{}", result, encryptData, secretKey);
            } catch (Exception e) {
                LoggerUtil.showErrorDetails(logger, "参数解密失败,加密的数据为:{}", dto.getEncryptData());
                throw new RoborderException(MemberErrorCode.DATA_TRANSFORM_ERROR);
            }
            ByteArrayInputStream is = new ByteArrayInputStream(result.getBytes(Charset.forName("utf8")));

            return new ServletInputStream() {

                @Override
                public boolean isFinished() {
                    return false;
                }
                @Override
                public boolean isReady() {
                    return false;
                }
                @Override
                public void setReadListener(ReadListener listener) {
                }
                @Override
                public int read() throws IOException {
                    return is.read();
                }
            };
        }
        return null;
    }

}

定义接口规则枚举类

public enum MethodRule {

    start("start", true, true, EncryptType.BASE64, 0),
    Member_registerUser(RequestUrl.Member + RequestUrl.Member_registerUser, false, false, EncryptType.NULL, 0),
    end("end", true, true, EncryptType.RSA, 0);

    private final String url;

    /**
     * 参数是否需要解密
     */
    private final Boolean paramsDecrypt;

    /**
     * 返回结果是否需要加密
     */
    private final Boolean resultEncrypt;

    /**
     * 加密方式
     */
    private final EncryptType signType;

    /**
     * 返回数据是否需要压缩 0 不压缩, 1 压缩
     */
    private final Integer dataCompress;


    MethodRule(String url, Boolean paramsDecrypt, Boolean resultEncrypt, EncryptType signType, Integer dataCompress) {
        this.url = url;
        this.paramsDecrypt = paramsDecrypt;
        this.resultEncrypt = resultEncrypt;
        this.signType = signType;
        this.dataCompress = dataCompress;
    }

    public static MethodRule getEnum(final String url) {
        return Arrays.stream(MethodRule.values()).filter(method -> url.startsWith(method.URL()))
                .findFirst().orElse(null);
    }

    public String URL() {
        return url;
    }

    public Boolean getParamsDecrypt() {
        return paramsDecrypt;
    }

    public Boolean getResultEncrypt() {
        return resultEncrypt;
    }

    public EncryptType getSignType() {
        return signType;
    }

    public Integer getDataCompress() {
        return dataCompress;
    }

}

controller层

    @ApiOperation(value = "用户注册", notes = "userDTO 可选参数:推荐人userToken  推荐码referralCode,其他为必选参数 ", response = ResultVO.class)
    @RequestMapping(value = RequestUrl.Member_registerUser, method = RequestMethod.POST)
    public ResultObject<ResultVO> registerUser(){
}

最后是dofliter中调用

 HttpServletRequestWrapper params = new ParamsWrapperDecorator(req);
 ResultWrapperDecorator result = new ResultWrapperDecorator(res);
 chain.doFilter(params, result);

这里参数加密解密的介绍,后台返回数据加密 也是一个道理,使用装饰器模式可以很好的解决这个问题

文章目录
-----------------------
最新评论

[评论][COMMENTS]