为什么你应该了解装饰器模式-代码优化与重构的秘诀

举报
dancer 发表于 2024/03/30 06:54:44 2024/03/30
【摘要】 装饰器模式是一种设计模式,它允许在运行时向对象添加额外的职责,而无需修改其代码。这种模式提供了一种动态扩展对象功能的方法,同时保持了对象的单一职责原则。本文介绍了装饰器模式的基本概念、原理、优势、适用场景、实现方法、最佳实践和注意事项。通过装饰器模式,可以将多个行为组合成一个更复杂的行为,而无需使用继承或大量的接口实现。装饰器模式适用于需要对一个对象进行一系列的增强处理的情况,而这些增强处理可以以


💪🏻 制定明确可量化的目标,坚持默默的做事。


开篇

    在一个常常需要在不破坏封装的前提下扩展对象功能的编程世界,有一个模式悄无声息地成为了高级编程技术的隐形冠军。我们日复一日地享受着它带来的便利,却往往对其背后的复杂性视而不见。它是怎样织入我们代码的丝线的呢?这个编程世界里的‘变形金刚’,究竟隐藏着什么秘密?让我们一起揭开装饰器模式的神秘面纱。

二、装饰器模式概述

2.1 简介

    装饰器模式是一种设计模式,用于在不修改原始类代码的情况下,动态地给对象添加新的功能。它通过创建一个包装对象来包裹原有对象,并在包装对象中添加额外的职责。这样,通过在运行时动态地创建和附加这些包装对象,可以动态地扩展对象的功能。

2.2 适合场景

1. GUI设计:
    在图形用户界面(GUI)开发中,装饰器模式可以用于自定义组件的行为。例如,假设你有一个基本的文本框组件,你可以使用装饰器模式为其添加额外的功能,比如校验输入、实时自动完成等。通过装饰器模式,你可以在不改变原始组件代码的情况下,动态地为组件添加功能。
2. 数据流处理:
    在数据处理流程中,装饰器模式可以用于对数据流进行过滤或转换。例如,假设你有一组数据流(比如通过传感器采集的数据),你可以使用装饰器模式为数据流添加解密、压缩或加密等功能。通过装饰器模式,你可以在不修改原始数据流代码的情况下,增加新的数据处理能力。
3. 日志记录:
    在日志记录系统中,装饰器模式可以用于为不同的日志记录类添加额外的功能。例如,你可能有一个基本的日志记录器,可以将日志信息写入文件。你可以使用装饰器模式为该日志记录器添加时间戳或格式化日志信息等功能,而无需修改原始的日志记录器代码。
5. 身份验证:
    在身份验证过程中,装饰器模式可以用于为已有的身份验证系统添加额外的验证功能。例如,假设你有一个基本的用户身份验证系统,你可以使用装饰器模式为该系统添加双因素身份验证、IP白名单验证等功能。

三、深入装饰器模式的结构

3.1 核心组件解析

1. Component:
    这是被装饰的对象的基本接口或抽象类,定义了对象必须实现的操作。装饰器和具体的被装饰对象都应当实现这个接口。
2. ConcreteComponent:
    这是Component接口的一个具体实现类。它定义了原始对象的基本功能,也即我们打算添加新功能的对象。
3. Decorator:
    这是一个抽象类,它实现(或继承)Component接口并包含一个Component类型的实例变量。这个类通常会有一个构造器,用于接收一个Component对象,并为其提供一个额外的功能层。Decorator本身通常是抽象的,因为它的目的是定义一个与ConcreteComponent兼容的接口,并为子类实现共通的任务——保持对Component实例的引用。
4. ConcreteDecorator:
    这是Decorator子类的具体实现。每个ConcreteDecorator通常实现了在Component上添加的额外功能。它会调用其内部的Component实例的方法,并在调用前后添加新的行为。

  这个模式的核心在于允许功能的层层叠加,这是通过持有Component实例的引用,并将对其的调用委托给这个实例来实现的,从而不会影响其他对象。实际上,对象可以通过一个或多个ConcreteDecorator来增加任意数量的责任。

3.2 模式原理阐述

    装饰器模式(Decorator Pattern)是一种设计模式,属于结构型模式的一种,它允许向一个现有的对象添加新的功能,而不改变其结构。这种模式创建了一个装饰类,用来包装原有的类,并且在不改变原类文件的前提下为原类动态地添加功能。
  装饰器模式的原理可以用以下几个要点来概括:
1. 继承关系的替代:
    装饰器模式提供了一种灵活的替代扩展系统的方法,而不是通过继承增加子类的方式来扩展功能,因为继承是静态的,并且应用于整个类,而装饰器模式可以在运行时动态添加特定对象的功能。
2. 接口一致性:
    装饰器模式使用组件(Component)接口对客户端透明,这意味着客户端针对接口编程,而不是针对具体类编程。装饰器类实现或继承自同一个接口,确保装饰后的对象仍然遵循原有接口的约定。
3. 动态包装:
    在装饰器模式中,装饰类包裹着组件对象,从而在保持组件接口的同时,为组件对象添加了额外的行为(装饰)。因为多态性的原因,客户端可以自由地使用装饰后的对象。
4. 多层装饰:
    装饰器模式允许多个装饰器按顺序装饰对象,从而实时增加新的行为或职责。装饰链中的每个装饰器可以向对象添加附加的状态或行为。
5. 增加灵活性:
    装饰器模式让用户可以选择需要的装饰器来组合功能,增强了系统的灵活性,同时代码维护和扩展也变得更加方便。

四、装饰器模式的超能力

    让我们通过一个简单的装饰器模式例子来阐释这个概念。假设我们在一个饮料店打工,需要为顾客提供几种不同类型的咖啡。起初,我们只提供简单的黑咖啡,但是顾客可以选择添加多种调料,如牛奶、糖、摩卡和奶泡。
  首先,我们建立一个饮料的抽象类,并让所有的咖啡和调料装饰器继承这个类:
// 抽象组件,定义饮料
public abstract class Beverage {
    String description = "Unknown Beverage";
    public String getDescription() {
        return description;
    }
    public abstract double cost();
}

  现在,我们添加我们的基础组件,黑咖啡:

// 具体组件,实现抽象组件
public class BlackCoffee extends Beverage {
    public BlackCoffee() {
        description = "Black Coffee";
    }
    public double cost() {
        return 1.00;
    }
}

  接下来,我们定义装饰器的抽象类,所有的调料装饰器都应该继承它:

// 装饰器抽象类,扩展自Beverage
public abstract class CondimentDecorator extends Beverage {
    public abstract String getDescription();
}

  然后我们为每种调料创建具体的装饰器实现类:

// 牛奶装饰器
public class Milk extends CondimentDecorator {
    Beverage beverage;

    public Milk(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", Milk";
    }

    public double cost() {
        return beverage.cost() + 0.20;
    }
}

// 糖装饰器
public class Sugar extends CondimentDecorator {
    Beverage beverage;

    public Sugar(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", Sugar";
    }

    public double cost() {
        return beverage.cost() + 0.10;
    }
}

// 摩卡装饰器
public class Mocha extends CondimentDecorator {
    Beverage beverage;

    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", Mocha";
    }

    public double cost() {
        return beverage.cost() + 0.50;
    }
}

// 奶泡装饰器
public class Whip extends CondimentDecorator {
    Beverage beverage;

    public Whip(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", Whip";
    }

    public double cost() {
        return beverage.cost() + 0.30;
    }
}

  最后,我们可以这样组合它们来创建不同的咖啡:

public class CoffeeShop {
    public static void main(String args[]) {
        // Order a black coffee without any condiments
        Beverage blackCoffee = new BlackCoffee();
        System.out.println(blackCoffee.getDescription() 
                           + " $" + blackCoffee.cost());
        
        // Add milk and sugar to the coffee
        Beverage coffeeWithMilkAndSugar = new BlackCoffee();
        coffeeWithMilkAndSugar = new Milk(coffeeWithMilkAndSugar);
        coffeeWithMilkAndSugar = new Sugar(coffeeWithMilkAndSugar);
        System.out.println(coffeeWithMilkAndSugar.getDescription() 
                           + " $" + coffeeWithMilkAndSugar.cost());
        
        // Make a fancy coffee by adding Milk, Mocha, and Whip to the coffee
        Beverage fancyCoffee = new BlackCoffee();
        fancyCoffee = new Milk(fancyCoffee);
        fancyCoffee = new Mocha(fancyCoffee);
        fancyCoffee = new Whip(fancyCoffee);
        System.out.println(fancyCoffee.getDescription() 
                           + " $" + fancyCoffee.cost());
    }
}

  在这个例子中,我们可以看到装饰器模式如何在不更改BlackCoffee类的情况下动态地为咖啡添加额外的调料。我们可以随意添加新的装饰器以实现新的功能,这展示了模式的灵活性。

五、装饰器模式的优缺点分析

5.1 优点

1. 增强扩展性:
    装饰器模式使得可以在不修改原始类代码的情况下通过添加装饰器在运行时扩展对象的行为。这使得遵循开闭原则(即软件实体应该对扩展开放,对修改关闭)成为可能,增加了类的灵活性和可扩展性。
2. 动态添加功能:
    与继承相比,装饰器模式能够动态地、非侵入式地添加或删除对象的责任。用户可以根据需要装饰对象,而不是在编译时决定其行为。
3. 功能组合:
    使用装饰器模式,多个装饰器可以组合起来使用,提供了一种非常灵活的方式来组合不同的功能。比如,在上面的咖啡例子中,你可以随意添加牛奶、糖、摩卡等,实现各种不同的咖啡组合。
4. 替代子类继承:
    由于使用子类继承来扩充功能可能会导致类爆炸现象(生成大量的子类),装饰器模式可以作为一种更优雅的解决方案,避免了类层次过多的缺点。
5. 保持类接口的纯净性:
    装饰器可以让我们把那些不适合放在类内部的功能,或者是那些会频繁更改的功能移出类外,这样可以保持原有的类结构清晰简单。
6. 细粒度的控制:
    装饰器模式提供了比继承更加细粒度的控制。每个装饰器可以实现更加精细的控制和管理。
7. 有利于解耦:
    装饰器模式有助于将类的核心职责和装饰功能进行分离,从而降低了系统的复杂度,并且有助于类的功能独立发展,减少了类之间的依赖关系。

5.2 缺点

1. 复杂性增加:
    使用装饰器模式可能会引入许多小类,因为每个装饰器通常对应一个小类。这有可能导致系统的结构变得复杂,特别是当装饰器过多或层次很深时。
2. 使用复杂性:
    如果使用过多的装饰器,那么客户端代码需要处理多个装饰层,这有可能使得对象的实例化过程变得复杂和难以理解。
3. 设计难度:
    正确并高效地使用装饰器模式需要对系统设计有深入理解。开发人员需要能够明确区分何时应该使用装饰器,何时应该使用简单的继承,以及何时选择其它设计模式。
4. 调试难度:
    对于装饰过的对象,调试可能更困难,因为装饰器会在调用栈中引入额外的层次。这意味着错误可能会在多个层次之间传播,而定位问题的根源可能更加耗时。
5. 接口不匹配:
    如果基础组件的接口随着时间发生变化,所有依赖于那个接口的装饰器都需要相应地进行更新,这可能会导致维护难度增加。
6. 过度使用:
    在不恰当的情况下强迫使用装饰器模式会使得设计过于复杂,简单问题可能仅仅需要更直接的方法。
7. 性能考虑:
    虽然大多数情况下这不是问题,但是在复杂的装饰器链中,每次调用都会通过所有的装饰器层,这可能会对性能产生不利影响。尤其是对于一些对性能要求高的系统,可能需要考虑额外的优化。

六、装饰器模式在现实世界中的应用

6.1 场景

    考虑这样一个实际应用:就是如何实现灵活的奖金计算。
    奖金计算是相对复杂的功能,尤其是对于业务部门的奖金计算方式,是非常复杂的, 除了业务功能复杂外,还有一个麻烦之处是计算方式还经常需要变动,因为业务部门要通过调整奖金的计算方式来激励士气。

  奖金计算方式:

    1. 首先是奖金分类,对于个人大致有个人当月业务奖金、个人累计奖金、个人业务增长奖金、及时回款奖金 、 限时成交加码奖金等;对于业务主管或者是业务经理,除了个人奖金外,还有团队累计奖金、团队业务增长奖金、团队盈利奖金等。

    2. 其次是计算奖金的金额,又有这么几个基数,销售额、销售毛利、实际回款、 业务成本、奖金基数等。

    3. 另外一个就是计算的公式,针对不同的人、不同的奖金类别、不同的计算奖金 的金额,计算的公式是不同的,即使是同一个公式,里面计算的比例参数也有 可能是不同的。
    上面的业务场景下计算奖金的方式有点复杂,这里主要是讲解设计模式,所以对上面的奖金的计算简化后如下:
1. 每个人当月业务奖金= 当月销售额×3%。
2. 每个人累计奖金= 总的回款额× 0.1%。
3. 团队奖金= 团队总销售额× 1%。

6.2 不用装饰模式实现

    一个人的奖金分成很多个部分。要实现奖金计算, 主要就是要按照各个奖金计算的规则,把这个人可以获取的每部分奖金计算出来,然后计算一个总和,这就是这个人可以得到的奖金。

  一个类中添加计算各种类型奖金的方法,实现代码如下:

/**
 * 计算奖金的对象
 *
 * @author danci_
 * @date 2024/2/4 12:22:03
 */
public class Prize {

    public static Map<String, Double> mapMonthSaleMoney = new HashMap<>();
    // 模拟数据,实际的工作中数据一般来源于数据库中
    static {
        mapMonthSaleMoney.put("张三", 100000D);
        mapMonthSaleMoney.put("李四", 200000D);
        mapMonthSaleMoney.put("王五", 300000D);
    }

    /**
     * 计算某人的奖金
     *
     * @param user 被计算奖金的人员
     * @return 某人的奖金
     */
    public double calcPrice(String user) {
        double prize = 0D;
        // 计算当月业务奖金,所有人都会计算
        prize = monthPrice(user);
        // 计算累计奖金
        prize += sumPrize(user);
        // 如果是经理,则添加团队奖金
        if (isManager(user)) {
            prize += groupPrice(user);
        }
        return prize;
    }

    private double monthPrice(String user) {
        // 计算当月业务奖金,按照人员去获取当月的业务额,然后乘以 3%
        double prize = mapMonthSaleMoney.get(user) * 0.03;
        System.out.println(user + " 当月的业务奖金为:" + prize);
        return prize;
    }

    /**
     * 计算某人的累计奖金
     * @param user 被计算奖金的人员
     * @return 某人的累计奖金
     */
    public double sumPrize(String user) {
        // 计算累计奖金,其实该按照人员去获取累计的业务额,然后再乘以 0。1%
        // 简单演示一下,假定大家的累计业务额都是 1000000 元
        double prize = 1000000 * 0.001;
        System.out.println(user + " 累计奖金为:" + prize);
        return prize;
    }

    private boolean isManager(String user) {
        // 这里假定王五为经理(实际的场景应该是从数据中获取用户的角色信息,判断是否为经理)
        return "王五".equals(user);
    }

    public double groupPrice(String user) {
        // 计算当月团队业务奖金,先计算出团队总的业务额,然后再乘以1%
        double group = 0D;
        for (double d : mapMonthSaleMoney.values()) {
            group += d;
        }
        System.out.println(user + " 当月团队业务奖金为:" + group * 0.01);
        return group * 0.01;
    }
}

  客户端代码如下:

/**
 * 计算奖金类 <br/>
 *
 * @author danci_
 * @date 2024/2/4 12:36:28
 */
public class Client {
    public static void main(String[] args) {
        Prize p = new Prize();

        // 计算张三的奖金
        double prize1 = p.calcPrice("张三");
        System.out.println("张三奖金为:" + prize1);
        // 计算李四的奖金
        double price2 = p.calcPrice("李四");
        System.out.println("李四奖金为:" + price2);
        // 计算王五的奖金
        double price3 = p.calcPrice("王五");
        System.out.println("王五奖金为:" + price3);

    }
}

  测试运行结果如下:

张三 当月的业务奖金为:3000.0
张三 累计奖金为:1000.0
张三奖金为:4000.0
李四 当月的业务奖金为:6000.0
李四 累计奖金为:1000.0
李四奖金为:7000.0
王五 当月的业务奖金为:9000.0
王五 累计奖金为:1000.0
王五 当月团队业务奖金为:6000.0
王五奖金为:16000.0

6.3 问题和痛点

  问题

    最痛苦的是,这些奖金的计算方式经常发生变动,几乎是每个季度都会有小调整,每年都有大调整。

    比如,根据业务需要添加一个计算奖金的规则“环比增长奖金”:就是本月的销售额比上个月有增加,而且要达到一定的比例,当然增长比例越高,奖金比例越大。则需要实现这个功能,并且添加到计算奖金类中去。

    又比如,过段时间业务奖励的策略发生了变化,不再需要这个奖金了,或者是另外换了一个新的奖金方式了,那么软件就需要把这个功能从软件中去掉,然后再实现新的功能。

    还有,在运行期间,不同人员参与的奖金计算方式是不同的。如业务经理,除了参与个人计算部分外,还要参加团队奖金的计算,这就意味着需要在运动期间动态地来组合要计算的部分,也就是一堆的判断 if-else操作。

  总结出如下问题:
    1. 计算逻辑复杂。
    2. 要有足够灵活性,可以方便地增加或者减少功能。
    2. 要能动态地组合计算方式,不同的人参与的计算不同。

  痛点

    在这种奖金计算方式不断变化的情况下,如何设计和实现奖金的计算方式,使得在计算方式变化的情况下软件能足够灵活,能够很快进行相应的调整和修改, 否则就不能满足实际业务的需要。

    一种方案是通过继承赤扩展功能;另一种方案是到计算奖金的对象里面添加或删除新的功能,并在计算奖金的时候,调用新的功能或是不调用某些去掉的功能,这种方案会亚严重违反了“开—闭原则”。

    现在要解决的问题是,在有一个计算奖金的对象的情况下,现在需要能够灵活地给它增加或减少功能,还需要能够动态的组合功能,每个功能就相当于在计算奖金的某个部分。
    换句话说就是,如何才能够透明地给一个对象增加功能,并不实现功能的动态组合。

6.4 使用装饰模式实现

  解决上述问题的一个合理的方案是使用装饰模式。

6.4.1 装饰模式定义

    动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式比生成子类更加灵活。

6.4.2 解决思路

    所谓透明地给一个对象增加功能,换句话说就是要给一个对象增加功能,但是不能让这个对象知道,也就是不能去发动这个对象。而实现了给一个对象透明地增加功能,自然就实现了功能的动态组合。(如原来对象有计算当月业务奖金功能,现在透明地给它增加计算累计奖金功能,则相当于动态组合了“当月业务奖金”和“累计奖金”功能了)

  解决方案选择:
    1. 继承的方式:对于减少和个性功能,继承是非常不灵活的复用方式。
    2. 对象组合方式:对象组合方式允许装饰器类的组合更加灵活和动态。每个装饰器可以独立存在,然后通过组合的方式动态地添加到被装饰的对象上,从而实现不同的功能组合。这种灵活性允许开发人员根据需求自由组合各种装饰器,而不需要创建大量的静态类继承结构。

6.4.3 装饰器模式的结构和说明

image.png

  · Component:组件对象的接口,可以给这些对象动态地添加职责。
  · ConcreteComponent:具体的组件对象,实现组件对象接口,通常就是被装饰器装饰的原始对象,也就是可以给这个对象添加职责。
  · Decorator:所有装饰器的抽象父类,需要定义一个与组件接口一致的接口,并持有一个Component 对象,其实就是持有一个被装饰的对象。
  · ConcreteDecorator:实际的装饰器对象,实现具体要向被装饰对象添加的功能。
注意:Decorator 这个被装饰的对象不一定是最原始的那个对象了,也可能是被其他装饰器装饰过后的对象,反正都是实现的同一个接口,也就是同一类型。

6.4.4 示例代码

  组件抽象类

/**
 * 组件对象的抽象类,可以给这些对象动态地添加职责 <br/>
 *
 * @author danci_
 * @date 2024/2/4 16:28:52
 */
public abstract class Component {

    /**
     * 示例方法
     */
    public abstract void operation();
}

  组件实现类

/**
 * 具体实现组件对象接口的对象 <br/>
 *
 * @author danci_
 * @date 2024/2/4 16:29:40
 */
public class ConcreteComponent extends Component {

    @Override
    public void operation() {
        // 相应的功能逻辑
    }
}

  装饰器对象

/**
 * 装饰器抽象类,维持一个指向组件对象的接口对象,并定义一个与组件接口一致的接口 <br/>
 *
 * @author danci_
 * @date 2024/2/4 16:30:31
 */
public abstract class Decorator extends Component {

    protected Component component;

    /**
     * 构造方法,传入组件对象
     * @param component 组件对象
     */
    public Decorator(Component component) {
        this.component = component;
    }

    @Override
    public void operation() {
        // 转发请求给组件对象,可以在转发前后执行 一些附加动作
        component.operation();
    }
}

  具体的装饰器实现类A,示意添加状态

/**
 * 装饰器的具体实现对象,向组件对象添加状态 <br/>
 *
 * @author danci_
 * @date 2024/2/4 16:31:48
 */
@Data
public class ConcreteDecoratorA extends Decorator {

    /**
     * 构造方法,传入组件对象
     *
     * @param component 组件对象
     */
    public ConcreteDecoratorA(Component component) {
        super(component);
    }

    private String addeState;

    public void operation() {
        // 调用父类的方法,可以在调用前后执行一些附加动作
        // 在这里进行处理的时候,可以使用添加的状态
        super.operation();
    }

}

  具体的装饰器实现类B,示意添加职责

/**
 * 装饰器的具体实现对象,向组件对象添加职责 <br/>
 *
 * @author danci_
 * @date 2024/2/4 16:34:31
 */
public class ConcreteDecoratorB extends Decorator {

    /**
     * 构造方法,传入组件对象
     *
     * @param component 组件对象
     */
    public ConcreteDecoratorB(Component component) {
        super(component);
    }

    public void addedBehavior() {
        // 需要添加的职责实现
    }

    public void operation() {
        // 调用父类的方法,可以在调用前后执行一些附加动作
        super.operation();
        addedBehavior();
    }
}

6.4.5 用装饰器模式重写示例

  大致的实现步骤

· 需要定义一个组件对象的接口,在这个接口中定义计算奖金的业务方法,因为外部就是使用这个接口来操作装饰模式构成的对象结构中的对象。
· 需要添加一个基本的实现组件接口的对象,可以让它返回奖金为 0 就可以了。
· 把各个计算奖金的规则当作装饰器对象,需要为它们定义一个统一的抽象的装饰器对象,方便约束各个具体的装饰器的接口。
· 把各个计算奖金的规则实现成为具体的装饰器对象。

  实现代码
  组件抽象类

/**
 * 计算奖金的组件抽象类 <br/>
 *
 * @author danci_
 * @date 2024/2/4 16:28:52
 */
public abstract class Component {

    /**
     * 计算某人的奖金
     * @param user 被计算奖金的人员
     */
    public abstract double calcPrize(String user);
}

  组件实现类

/**
 * 基本的实现计算奖金的类,也是被装饰器装饰的对象 <br/>
 *
 * @author danci_
 * @date 2024/2/4 16:29:40
 */
public class ConcreteComponent extends Component {

    @Override
    public double calcPrize(String user) {
        // 默认的实现,没有奖金
        return 0;
    }
}

  装饰器抽象类
  在进 一步定义装饰器之前,先定义出各个装饰器公共的父类,在这里定义所有装饰器对象需要实现的方法。这个父类应该实现组件的接口,这样才能保证装饰后的对象仍然可以继续被装饰。

/**
 * 装饰器的接口,需要和被装饰的对象实现同样的接口 <br/>
 *
 * @author danci_
 * @date 2024/2/4 16:30:31
 */
public abstract class Decorator extends Component {


    public static Map<String, Double> mapMonthSaleMoney = new HashMap<>();
    // 模拟数据,实际的工作中数据一般来源于数据库中
    static {
        mapMonthSaleMoney.put("张三", 15000D);
        mapMonthSaleMoney.put("李四", 22000D);
        mapMonthSaleMoney.put("王五", 37000D);
    }

    /**
     * 持有被装饰的组件对象
     */
    protected Component component;

    /**
     * 通过构造方法传入被装饰的对象
     * @param component 组件对象
     */
    public Decorator(Component component) {
        this.component = component;
    }

    @Override
    public double calcPrize(String user) {
        // 转调组件对象的方法
        return component.calcPrize(user);
    }
}

  装饰器具体的实现类
  用一个具体的装饰器对象,来实现一条计算奖金的规则。现在有三条计算奖金的规则,那就对应有三个装饰器对象来实现。下面依次来看看它们的实现。
  装饰器具体的实现类-当月业务奖金

/**
 * 装饰器对象,计算当月业务奖金 <br/>
 *
 * @author danci_
 * @date 2024/2/4 16:50:39
 */
public class MonthPrizeDecorator extends Decorator {
    /**
     * 通过构造方法传入被装饰的对象
     *
     * @param component 组件对象
     */
    public MonthPrizeDecorator(Component component) {
        super(component);
    }

    @Override
    public double calcPrize(String user) {
    	// 先获取前面运算出来的资金
        double money = super.calcPrize(user);
        // 然后计算当月业务资金,按人员去获取,然后再乘以3%
        double prize = mapMonthSaleMoney.get(user);
        System.out.println(user + " 当月资金为:" + prize * 0.03);
        return money + prize * 0.03;
    }
}

  装饰器具体的实现类-累计资金

/**
 * 装饰器对象,计算累计奖金 <br/>
 *
 * @author danci_
 * @date 2024/2/4 16:53:06
 */
public class SumPrizeDecorator extends Decorator {
    /**
     * 通过构造方法传入被装饰的对象
     *
     * @param component 组件对象
     */
    public SumPrizeDecorator(Component component) {
        super(component);
    }

    public double calcPrize(String user) {
    	// 先获取前面运算出来的资金
        double money = super.calcPrize(user);
        // 然后计算累计资金,按人员获取累计的业务额,然后再乘以 0.1%
        // 假设大家的累计业务额都是150000
        double prize = 150000 * 0.001;
        System.out.println(user + " 累计奖金为:" + prize);
        return money + prize;
    }
}

  装饰器具体的实现类-当月团队业务资金

/**
 * 装饰器对象,计算当月团队业务奖金 <br/>
 *
 * @author danci_
 * @date 2024/2/4 16:54:33
 */
public class GroupPrizeDecorator extends Decorator {
    /**
     * 通过构造方法传入被装饰的对象
     *
     * @param component 组件对象
     */
    public GroupPrizeDecorator(Component component) {
        super(component);
    }

    @Override
    public double calcPrize(String user) {
   	 	// 先获取前面运算出来的资金
        double money = super.calcPrize(user);
        // 然后计算当月团队业务资金,先计算出团队总业务额,然后再乘以 1%
        // 这里假定张三、李四和王五 为一个团队
        double group = 0D;
        for (double d : mapMonthSaleMoney.values()) {
            group += d;
        }
        double prize = group * 0.01;
        System.out.println(user + " 当月团队业务奖金为:" + prize);
        return money + prize;
    }
}

  客户端
  1. 首先需要创建被装饰的对象
  2. 然后创建需要的装饰对象
  3. 接下来是组合装饰对象,然后对前面的对象进行装饰

/**
 * 计算奖金类 <br/>
 *
 * @author danci_
 * @date 2024/2/4 12:36:28
 */
public class Client {
    public static void main(String[] args) {
        // 先创建计算基本奖金的类,这也是被装饰的对象
        Component c1 = new ConcreteComponent();
        // 然后对计算的基本奖金进行装饰,这里要组合各个装饰
        // 说明,各个装饰者之间最好是不要有先后顺序的限制
        // 也就是先装饰谁和后装饰谁都应该是一样的

        // 先组合普通业务人员的奖金计划
        Decorator d1 = new MonthPrizeDecorator(c1);
        Decorator d2 = new SumPrizeDecorator(d1);
        // 这里只需使用最后组合好的对象调用业务方法即可,会依次调用回去
        double prize1 = d2.calcPrize("张三");
        System.out.println("------------张三应得奖金为:" + prize1);
        double prize2 = d2.calcPrize("李四");
        System.out.println("------------李四应得奖金为:" + prize2);

        System.out.println("----------------");
        // 业务经理添加团队奖金
        Decorator d3 = new GroupPrizeDecorator(d2);
        double prize4 = d3.calcPrize("王五");
        System.out.println("------王经应得奖金为:" + prize4);
    }
}

  运行结果如下

张三 当月资金为:450.0
张三 累计奖金为:150.0
------------张三应得奖金为:600.0
李四 当月资金为:660.0
李四 累计奖金为:150.0
------------李四应得奖金为:810.0
----------------
王五 当月资金为:1110.0
王五 累计奖金为:150.0
王五 当月团队业务奖金为:740.0
------王经应得奖金为:2000.0

  当测试运行的时候会按照装饰器的组合顺序,依次调用相应的装饰器来执行业务功能,是一个递归的调用方法。以业务经理“王五”为例,王五的资金计算过程如下图
image.png

==注:这个图很好地揭示了装饰模式的组合和调用过程,请仔细体会一下==

6.4.6 用装饰器模式重写示例-结构图

image.png

6.5 装饰器模式讲解

6.5.1 装饰器的功能

    装饰模式能够实现动态地为对象添加功能,是从一 个对象外部来给对象增加功能, 相当于是改变了对象的外观。当装饰过后,从外部使用系统的角度看,就不再是使用原 始的那个对象了,而是使用被一系列的装饰器装饰过后的对象。

    这样就能够灵活地改变一个对象的功能,只要动态组合的装饰器发生了改变,那么最终所得到的对象的功能也就发生了改变。
    变相地还得到了另外一个好处,那就是装饰器功能的复用,可以给一个对象多次增加同一个装饰器,也可以用同一个装饰器装饰不同的对象。

6.5.2 对象组合

    在面向对象的设计中,有一条基本的规则就是“尽量使用对象组合,而不是对象继承” 来扩展和复用功能。装饰模式的思考起点就是这个规则。

  对象组合

    对象组合是指通过将一个对象实例作为另一个对象的成员变量,将它们组合在一起形成一个更大的对象的过程。对象组合允许一个对象包含另一个对象,从而形成更复杂的对象结构。

    对象组合是一种关系,它体现了对象之间的包含关系和整体与部分之间的关系。通过对象组合,可以构建更复杂、更具有层次结构的对象,以适应不同的业务需求和设计目标。

    举个例子,假设我们有一个汽车类(Car)和一个引擎类(Engine)。我们可以通过对象组合的方式,将引擎作为汽车的成员变量,将引擎对象组合到汽车对象中。这样,汽车对象就可以通过引擎对象来驱动和操作。通过对象组合,我们可以形成一个由汽车和引擎组成的复合对象,从而实现汽车的功能。
public class Engine {
    public void run() {
        System.out.println("执行引擎的功能.");
    }
}
public class Car {
    private Engine engine = new Engine();
    public void run() {
        // 转调引擎的功能
        // 在转调前后可以做一些功能处理,对于Engine对象来说是透明的。
        engine.run();
    }
    public void oth() {
        System.out.println("其它功能。");
    }
}

  用对象组合来实现简单了,也更灵活了。

     · 首先可以有选择地复用功能,不是所有Engine的功能都会被复用,在 Car 中少调用几个 Engine 定义的功能就可以了;
     · 其次在转调前后,可以实现一些功能处理,而且对于Engine 对象是透明的,也就是 Engine对象并不知道在run方法处理的时候被追加了功能;
     · 还有一个额外的好处,就是可以组合拥有多个对象的功能,即Car 中还可以添加其它对象成为成员变量,同Engine 相似添加相应的功能。

  何时创建被组合对象的实例

     · 一种方案是在属性 上直接定义并创建需要组合的对象实例。
     · 另外一种方案是在属性上定义一个变量,来表示持有被组合对象的实例,具体实例从外部传入,也可以通过IoC/DI 容器来注入。

6.5.3 装饰器

  扮演着的重要角色和作用

      1. 添加额外功能:装饰器类的主要作用之一是添加额外的功能到被装饰对象上。通过装饰器类,可以在不修改现有对象结构的情况下,动态地向对象添加新的功能或修改现有功能。这种能力使得装饰器模式非常适用于需要灵活地扩展对象功能的场景。

      2. 透明的包装:装饰器类通过包装原始对象,以透明的方式向对象添加新的功能。外部代码可以使用装饰后的对象,而无需关心装饰器类的存在。这样,装饰器模式实现了对原始对象的透明包装,使得功能的添加对客户端来说是透明的。

      3. 符合开闭原则:装饰器类使得可以在不修改现有代码的情况下,通过添加新的装饰器类来扩展对象的功能。这符合开闭原则,即软件实体应该对扩展开放,对修改关闭。

      4. 组合方式:装饰器类采用对象组合的方式,通过持有对被装饰对象的引用,将装饰器类像堆积木一样组合在一起,从而形成一条功能链,实现一系列的功能嵌套和组合。

  装饰器与组合类的关系

    在装饰器模式中,装饰器类和组件类之间存在一种紧耦合的关系。装饰器类实际上是对组件类的包装或扩展,通过持有组件类的引用来实现对组件的操作。
    装饰器类和组件类之间的关系可以描述为:
     1. 继承或实现同一接口:装饰器类通常继承自组件类(也即实现了组件接口),或者与组件类实现了相同的接口。这样做是为了确保装饰器类能够与组件类拥有相同的外部接口,从而实现对组件的透明包装。

     2. 持有组件类的引用:装饰器类持有一个组件类的对象实例作为自己的成员变量。这个成员变量通常被称为"wrapped",代表被装饰的组件对象。装饰器类在需要调用组件类的方法时,会通过该成员变量来访问和操作组件类。

     3. 递归调用:装饰器类中的方法通常会先调用被装饰的组件对象的对应方法,然后再在此基础上添加额外的功能。这种方式形成了装饰器类与组件类之间的递归调用关系,通过递归调用,装饰器可以将多个装饰器层层包装在一起,以实现功能的叠加和组合。

七、从装饰器模式看设计原则

7.1 开闭原则

    一个软件实体应当对扩展开放,对修改关闭。这一原则最早由BertrandMeyer [MEYER88]提出,英文原文是:Software entities should be open for extension, but closed for modification.

    这个原则说的是,在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展。换言之,应当可以在不必修改源代码的情况下增强这个模块的行为。

7.2 合成复用原则

    合成/聚合复用原则 (Composite/Aggregate Reuse Principle,或 CARP)经常又叫做合成复用原则(Composite Reuse Principle或CRP)。合成/聚合复归原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象迪过向这些对象的委派达到复用已有功能的日的。

    这个设计原则有只 - 个更简短的表述:要尽量使用合成/聚合,尽量不要使用继承。
    合成复用原则(Composite Reuse Principle, CRP)鼓励使用对象的组合(合成)有助于提供新的功能,而不是通过继承来获得功能。这种原则可以避免因继承带来的紧密耦合问题,保持系统中各个类的独立性,从而提高了系统的可复用性和可维护性。

7.3 装饰器模式中的开闭原则

  实践

1. 不修改现有代码的情况下扩展功能:
    装饰器模式允许在不改变原始类的源代码和不增加新的子类的情况下,动态的扩展对象的功能。通过将每个要添加的功能封装在一个装饰器类中,可以组合使用多个装饰器来增强对象的功能。
2. 运行时选择功能:
    与静态继承相比,装饰器模式通过组合的方式在运行时动态地为对象添加额外的职责,这样就可以根据需要来增加或修改对象的行为,实现开闭原则中提到的对扩展开放。
3. 维护对象接口不变:
    装饰器具备与继承的对象相同的接口,这意味着从客户端的角度看,装饰的对象与原始对象遵循相同的接口(即它们可以是透明的),这保证了原始对象的接口不受影响,同时也可以提供新的功能。
4. 松耦合设计:
    装饰器模式促进了良好的设计原则,比如单一职责原则和开闭原则,装饰器类通常专注于一个具体的功能,并且可以在不影响其他装饰器或对象的情况下单独更改。

  步骤

1. 确定共同的接口:
    设计一个一致的接口或抽象类,用于原始对象和装饰器统一实现。这个接口规定了所有对象共有的方法。
2. 应用封装:
    创建具体的装饰器类,每个类封装了在原有对象基础上增加的功能。装饰器类实现共同接口,并持有一个指向该接口的引用。
3. 动态组合:
    在需要添加功能或者改变对象行为的时候,客户端可以灵活选取并组合多个装饰器来装饰对象,无需修改已有类的内部代码。

7.4 装饰器模式中的合成复用原则

  实践

1. 对象组合而非继承:
    在装饰器模式中,装饰器对象包装原始对象,通过在其接口调用链前添加额外的行为来实现功能扩展。这种方式避免使用继承,而是利用组合将原始对象的参考(reference)传递给装饰器对象。
2. 维护类的独立性:
    每个装饰器类都是独立的,它实现了特定的功能,这样的设计使各个类职责明确,并且促进了代码的复用。一个装饰器可以装饰不同的组件或者其他装饰器而不影响它们自身的行为。
3. 易于添加新的装饰器:
    由于使用了组合,所以在不修改现有代码的情况下,可以非常容易地添加新的装饰器来增强系统的功能,而这不会对现有的组件或装饰器产生副作用。

  步骤

1. 定义通用接口:你的组件和装饰器都应该遵循相同的接口,保证它们在功能上是可替换的。
2. 维使用组合关系:装饰器中包含一个组件的引用,通过这种方式,装饰器可以在调用原组件的功能的基础上添加新的行为。
3. 易独立扩展:由于装饰器的存在,你可以单独扩展组件的行为而无需修改组件自身的代码,这符合合成复用原则的核心思想。
4. 多装饰器组合:可以根据需求将多个装饰器按顺序应用到组件上,各个装饰器之间相互独立,这样的灵活性正是组合优于继承的表现。

八、结语

    装饰器模式在软件设计中扮演着重要的角色,特别是当你希望以动态、透明的方式为对象增加职责时,而不想通过重写或修改现有的代码。装饰器模式优雅地体现了设计原则,尤其是开闭原则和合成复用原则,从而促进了软件结构的灵活性与可扩展性。
    开闭原则强调对拓展开放、对修改关闭,装饰器模式通过允许在不改变现有对象的代码的基础上进行功能拓展来达成这一目标。装饰器能够给对象动态添加职责,而保持接口不变。这样,当需要添加新功能时,只需要增加新的装饰器类,无需修改现有的类。这种方法不仅保持了现有类结构的完整性,而且也使系统易于拓展和维护。
    合成复用原则鼓励使用组合关系而不是继承关系来达到软件复用目的。装饰器模式通过将一个组件嵌入到另一个对象中(装饰器中),来实现运行时的动态组合。这种方法避免了继承的缺陷(如紧密耦合和继承层次的膨胀),同时保持了类的独立和拓展性。
    回顾之前的内容,我们提出了如何通过装饰器模式体现和实践开闭原则和合成复用原则的问题。在装饰器模式中,每一个装饰器都遵循相同的接口并增加特定的行为,它们可以灵活地与目标对象组合,从而在不修改已有对象代码的情况下增加新的功能,这恰好符合了开闭原则。
    同时,装饰器模式使用组合而非继承来扩展对象的功能,这样的设计不仅遵循了合成复用原则,而且还增加了代码的可复用性和可维护性。装饰器模式允许我们将不同的功能封装到不同的装饰器中,然后通过将装饰器堆叠来增强原始对象,提供了一种高度灵活和动态的方式来扩展对象功能。
    综上所述,装饰器模式是设计模式中的一个强大的工具,有助于开发者设计易于扩展和维护的应用程序。通过遵守开闭原则和合成复用原则,装饰器模式使得在增加新功能的同时不必更改现有代码成为可能,这对于实现松耦合和高内聚的设计目标至关重要。
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。