每天认识一个设计模式-装饰器模式:给对象「动态换装」的能力

一、前言

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。之前,我们已经一起探索了工厂、单例、组合、代理和适配器这些经典的设计模式。

今天,我们把目光聚焦到装饰器模式上。简单来说,装饰器模式就像给一个对象披上一件件 “功能披风”,在不改变对象原本结构的情况下,能够在运行时动态地为它添加新功能。比如说,给一部基础款手机的功能对象在运行的时候添加拍照功能模块,根本不需要对基础代码大动干戈。这种强大的灵活性和扩展性,正是装饰器模式的核心魅力所在。它不仅能优化代码结构,还能大大提升代码的可维护性和可复用性。接下来,就让我们一起深入研究它的原理和实际应用吧。

二、装饰器模式深度剖析 

装饰器模式(Decorator Pattern)作为一种结构型设计模式,其主要解决在子类数量急剧膨胀的情况下,避免通过继承引入静态特征的问题

比如我们在开发一个电商系统时,其中商品有基础的展示功能。但随着业务发展,不同类型商品需要不同的额外功能,比如限时折扣、赠品提示等。如果使用继承,每增加一种新功能,就需要创建一个新的子类,当功能种类繁多时,子类数量会急剧膨胀,代码变得难以维护。​

而装饰器模式就能很好地解决这个问题。它允许在运行时以动态方式对商品对象的功能进行添加或修改。此模式能够在不改变商品对象原有结构的前提下,动态地为商品增添新的功能。其实现机制是将商品对象封装于一个装饰器对象之中,以此达成对商品功能的扩展。比如某个商品在促销期间,就动态添加限时折扣装饰器;促销结束,移除该装饰器即可。相较于继承,装饰器模式展现出更高的灵活性,原因在于它能够在运行时依据实际需求添加或移除装饰器,而继承则是在编译阶段便已确定类的结构。

为了更直观地理解装饰器模式,我们以咖啡店的咖啡为例。

假设咖啡店提供基础的咖啡,顾客可以根据自己的口味选择添加不同的调料,如牛奶、糖、巧克力等。每添加一种调料,咖啡的口味和价格都会发生变化。

在这个场景中,基础咖啡就是被装饰的对象,而各种调料就是装饰器,它们为基础咖啡添加了额外的功能和特性。

装饰器模式主要包含以下几个角色:​

  • 抽象构件(Component):定义一个抽象接口,它是具体构件和抽象装饰器的共同父类,声明了具体构件中实现的业务方法,为客户端提供统一的操作接口。例如,上述咖啡例子中的咖啡抽象类,它定义了获取咖啡描述和价格的方法。​
  • 具体构件(ConcreteComponent):实现抽象构件接口的具体对象,是被装饰的原始对象,提供了最基本的功能实现。如基础咖啡类,它实现了咖啡抽象类的方法,提供了基础的咖啡功能。​
  • 抽象装饰器(Decorator):抽象装饰器也是抽象构件的子类,它持有一个指向抽象构件对象的引用,通过该引用可以调用被装饰对象的方法,并在其前后添加额外的功能。抽象装饰器通常定义了一个构造函数,用于接收被装饰的对象。例如,咖啡装饰器抽象类,它实现了咖啡抽象类的接口,并持有一个咖啡对象的引用。​
  • 具体装饰器(ConcreteDecorator):具体装饰器是抽象装饰器的子类,负责向被装饰对象添加具体的额外功能。它通过调用父类的构造函数,传入被装饰的对象,并在自身的方法中实现具体的装饰逻辑。如牛奶装饰器、糖装饰器等,它们分别为咖啡添加了牛奶和糖的功能。

用 UML 类图表示如下:

根据这个实现逻辑我们可以简单在代码中实现上面谈到的咖啡的例子:

// 抽象构件:咖啡
interface Coffee {
    String getDescription();
    double getPrice();
}

// 具体构件:基础咖啡
class SimpleCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "基础咖啡";
    }

    @Override
    public double getPrice() {
        return 10.0;
    }
}

// 抽象装饰器:咖啡装饰器
abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription();
    }

    @Override
    public double getPrice() {
        return coffee.getPrice();
    }
}

// 具体装饰器:牛奶装饰器
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", 加牛奶";
    }

    @Override
    public double getPrice() {
        return coffee.getPrice() + 2.0;
    }
}

// 具体装饰器:糖装饰器
class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", 加糖";
    }

    @Override
    public double getPrice() {
        return coffee.getPrice() + 1.0;
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        // 创建基础咖啡
        Coffee coffee = new SimpleCoffee();
        System.out.println(coffee.getDescription() + ",价格:" + coffee.getPrice());

        // 为咖啡添加牛奶装饰器
        coffee = new MilkDecorator(coffee);
        System.out.println(coffee.getDescription() + ",价格:" + coffee.getPrice());

        // 为咖啡添加糖装饰器
        coffee = new SugarDecorator(coffee);
        System.out.println(coffee.getDescription() + ",价格:" + coffee.getPrice());
    }
}

我们通过Coffee接口定义了咖啡的基本行为,SimpleCoffee类实现了Coffee接口,提供了基础咖啡的功能。CoffeeDecorator抽象类实现了Coffee接口,并持有一个Coffee对象的引用,为具体装饰器提供了基础。MilkDecorator和SugarDecorator类继承自CoffeeDecorator,分别为咖啡添加了牛奶和糖的功能。

这样在客户端代码中,我们可以动态地为基础咖啡添加不同的装饰器,从而实现对咖啡功能的扩展。

三、装饰器模式应用场景​

在软件开发领域,装饰器模式能够动态地为对象添加功能,具有很高的灵活性和扩展性,在许多场景中发挥着关键作用。同样的,接下来,让我们从常见的框架中一同深入探究装饰器模式在实际应用中的典型场景。

3.1 Java IO 流体系​

在 Java 的 IO 流体系中,装饰器模式得到了淋漓尽致的体现。以BufferedInputStream和FileInputStream为例,FileInputStream是一个具体构件,它提供了从文件中读取字节的基本功能。而BufferedInputStream则是一个具体装饰器,它为FileInputStream添加了缓冲功能,从而提高了读取效率。

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class InputStreamDecoratorExample {
    public static void main(String[] args) {
        try {
            // 创建基础的文件输入流
            InputStream fileInputStream = new FileInputStream("example.txt");
            // 使用BufferedInputStream装饰文件输入流,添加缓冲功能
            InputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

            int data;
            while ((data = bufferedInputStream.read()) != -1) {
                System.out.print((char) data);
            }

            // 关闭流,先关闭外层装饰器流,它会自动关闭被装饰的流
            bufferedInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这里我们首先创建了一个FileInputStream对象,用于读取文件内容。然后,我们将FileInputStream对象传递给BufferedInputStream的构造函数,创建了一个BufferedInputStream对象。

此时,BufferedInputStream对象就像一个 “装饰者”,为FileInputStream对象添加了缓冲功能。在读取文件内容时,BufferedInputStream会先从缓冲区中读取数据,如果缓冲区中没有数据,它才会从文件中读取数据并填充缓冲区,这样可以减少磁盘 I/O 操作的次数,提高读取效率。​

这种设计方式的优势在于,我们可以根据实际需求,灵活地组合不同的装饰器,为基础的InputStream添加各种功能。

比如,我们还可以使用DataInputStream来装饰BufferedInputStream,使其能够读取基本数据类型;使用GZIPInputStream来装饰FileInputStream,实现对压缩文件的读取等。通过装饰器模式,Java IO 流体系变得更加灵活、可扩展,能够满足各种复杂的文件处理需求。

3.2 Spring Security 过滤器链​

在 Spring Security 中,过滤器链的实现也运用了装饰器模式。过滤器链就像是一个 “装饰链”,每个过滤器都可以看作是一个装饰器,它们按照一定的顺序对请求进行处理,为请求添加各种安全相关的功能,如身份验证、授权、CSRF 保护等。​

以请求处理流程为例,当一个 HTTP 请求到达应用时,首先会经过FilterChainProxy,它负责管理和调用过滤器链。过滤器链中的每个过滤器都会对请求进行特定的处理,比如UsernamePasswordAuthenticationFilter会处理用户名和密码的身份验证,BasicAuthenticationFilter会处理 HTTP Basic 认证,CsrfFilter会处理 CSRF 保护等。每个过滤器在处理请求时,会在调用下一个过滤器之前或之后执行自己的逻辑,从而实现对请求功能的增强。​

在 Spring Security 的配置中,我们可以通过HttpSecurity来定义过滤器链的组成和顺序。例如:

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
           .withUser("user").password("{noop}password").roles("USER")
           .and()
           .withUser("admin").password("{noop}admin").roles("ADMIN");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
           .authorizeRequests()
               .antMatchers("/public/**").permitAll()
               .anyRequest().authenticated()
           .and()
           .formLogin()
               .loginPage("/login")
               .permitAll()
           .and()
           .logout()
               .permitAll();
    }
}

在上述配置中,我们通过authorizeRequests()方法定义了请求的访问规则,通过formLogin()方法配置了表单登录的相关信息,通过logout()方法配置了登出的相关信息。这些配置会被转化为相应的过滤器,并添加到过滤器链中,从而实现对请求的安全处理。​

装饰器模式在 Spring Security 过滤器链中的作用主要体现在以下几个方面:​

  • 功能模块化:每个过滤器都专注于实现一个特定的安全功能,使得代码的职责更加清晰,易于维护和扩展。比如,UsernamePasswordAuthenticationFilter只负责处理用户名和密码的身份验证,CsrfFilter只负责处理 CSRF 保护,这样当我们需要修改或添加某个功能时,只需要关注对应的过滤器即可。​
  • 动态组合:我们可以根据应用的实际需求,灵活地添加、移除或调整过滤器的顺序,实现对请求处理流程的动态定制。比如,在某些场景下,我们可能不需要 CSRF 保护,那么可以直接将CsrfFilter从过滤器链中移除;在另一些场景下,我们可能需要添加自定义的过滤器来实现特定的安全逻辑,只需要按照正确的顺序将其添加到过滤器链中即可。​
  • 透明性:对于客户端来说,它不需要关心过滤器链的具体组成和内部实现细节,只需要发送请求,就可以得到经过安全处理后的响应。这种透明性使得客户端代码更加简洁、易用,同时也提高了系统的安全性和可维护性。

四、装饰器模式应用的简易实战:订单服务多层校验 

4.1 需求分析

在电商系统中,订单服务是核心模块之一,其涉及到多个业务规则和校验逻辑。以一个简单的订单创建流程为例,在创建订单时,我们不仅需要进行基础的业务逻辑处理,还需要进行库存校验,确保所订商品有足够的库存;

同时,还可能需要进行折扣计算,根据用户的等级、订单金额等因素来计算订单的最终价格。这些校验逻辑并非一成不变,随着业务的发展,可能会不断添加新的校验规则,如新增促销活动时,需要对订单进行促销规则的校验等。​

如果使用传统的方式,将所有的校验逻辑都硬编码在订单服务的实现类中,会导致代码臃肿、难以维护。而且,当有新的校验规则加入时,需要频繁修改订单服务的代码,这不仅增加了开发成本,也增加了出错的风险。

而装饰器模式正好可以解决这些问题,它允许我们在不改变原有订单服务核心代码的基础上,动态地添加新的校验逻辑,使得代码更加灵活、可维护。​

4.2 代码实现​

接下来,我们通过 Spring Boot 来实现订单服务的多层校验,具体代码如下:

// 定义订单数据传输对象
public class OrderDTO {
    // 商品列表
    private List<Item> items;
    // 用户等级
    private UserLevel userLevel;
    // 其他订单相关信息...

    // 省略getter和setter方法
}

// 定义商品类
public class Item {
    private String productId;
    private int quantity;

    // 省略getter和setter方法
}

// 定义用户等级枚举
public enum UserLevel {
    BASIC, SILVER, GOLD, PLATINUM
}

// 基础服务接口
public interface OrderService {
    Order createOrder(OrderDTO dto);
}

// 核心实现(被装饰对象)
@Service
public class BasicOrderService implements OrderService {
    @Override
    public Order createOrder(OrderDTO dto) {
        // 基础的订单创建逻辑,如保存订单到数据库等
        Order order = new Order();
        // 简单示例,实际应根据业务逻辑填充订单信息
        order.setOrderId(System.currentTimeMillis());
        order.setStatus(OrderStatus.CREATED);
        return order;
    }
}

// 抽象装饰器
public abstract class OrderDecorator implements OrderService {
    protected OrderService wrappee;

    public OrderDecorator(OrderService service) {
        this.wrappee = service;
    }
}

// 具体装饰器:库存校验
@Component
public class StockCheckDecorator extends OrderDecorator {
    @Override
    public Order createOrder(OrderDTO dto) {
        checkStock(dto.getItems()); // 增强逻辑:检查库存
        return wrappee.createOrder(dto);
    }

    private void checkStock(List<Item> items) {
        for (Item item : items) {
            // 模拟从库存服务查询库存
            int stock = inventoryService.getStock(item.getProductId());
            if (stock < item.getQuantity()) {
                throw new StockInsufficientException("商品 " + item.getProductId() + " 库存不足");
            }
        }
    }
}

// 具体装饰器:折扣计算
@Component
public class DiscountDecorator extends OrderDecorator {
    @Override
    public Order createOrder(OrderDTO dto) {
        calculateDiscount(dto); // 增强逻辑:计算折扣
        return wrappee.createOrder(dto);
    }

    private void calculateDiscount(OrderDTO dto) {
        double discount = 0;
        // 根据用户等级计算折扣
        if (dto.getUserLevel() == UserLevel.SILVER) {
            discount = 0.05;
        } else if (dto.getUserLevel() == UserLevel.GOLD) {
            discount = 0.1;
        } else if (dto.getUserLevel() == UserLevel.PLATINUM) {
            discount = 0.15;
        }
        // 根据订单金额计算折扣
        double totalAmount = calculateTotalAmount(dto.getItems());
        if (totalAmount > 1000) {
            discount += 0.05;
        }
        // 应用折扣到订单
        applyDiscount(dto, discount);
    }

    private double calculateTotalAmount(List<Item> items) {
        double total = 0;
        for (Item item : items) {
            // 模拟从商品服务获取商品价格
            double price = productService.getPrice(item.getProductId());
            total += price * item.getQuantity();
        }
        return total;
    }

    private void applyDiscount(OrderDTO dto, double discount) {
        // 简单示例,实际应更新订单的价格信息
        System.out.println("订单应用折扣:" + discount * 100 + "%");
    }
}

// 配置类实现装配
@Configuration
public class OrderConfig {
    @Bean
    public OrderService decoratedOrderService() {
        return new StockCheckDecorator(
                new DiscountDecorator(
                        new BasicOrderService()));
    }
}

// 订单类
public class Order {
    private long orderId;
    private OrderStatus status;
    // 其他订单属性...

    // 省略getter和setter方法
}

// 订单状态枚举
public enum OrderStatus {
    CREATED, PAID, SHIPPED, COMPLETED, CANCELED
}

// 库存不足异常类
public class StockInsufficientException extends RuntimeException {
    public StockInsufficientException(String message) {
        super(message);
    }
}

这里我们首先定义了OrderService接口及其实现类BasicOrderService,BasicOrderService提供了基础的订单创建逻辑。

然后,我们创建了抽象装饰器OrderDecorator,它持有一个OrderService对象的引用。具体装饰器StockCheckDecorator和DiscountDecorator继承自OrderDecorator,分别实现了库存校验和折扣计算的功能。

在StockCheckDecorator的createOrder方法中,先调用checkStock方法进行库存校验,然后再调用被装饰对象的createOrder方法;

在DiscountDecorator的createOrder方法中,先调用calculateDiscount方法进行折扣计算,然后再调用被装饰对象的createOrder方法。

最后,通过配置类OrderConfig,我们将BasicOrderService、StockCheckDecorator和DiscountDecorator进行装配,形成一个具有多层校验功能的订单服务。

4.3 测试与验证​

为了验证订单服务多层校验的功能,我们可以编写测试用例。这里使用 JUnit 5 和 Mockito 进行测试,示例代码如下:

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
public class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @MockBean
    private InventoryService inventoryService;

    @MockBean
    private ProductService productService;

    @Test
    public void testCreateOrderWithValidItems() {
        // 准备测试数据
        OrderDTO dto = new OrderDTO();
        List<Item> items = Arrays.asList(
                new Item("product1", 1),
                new Item("product2", 2)
        );
        dto.setItems(items);
        dto.setUserLevel(UserLevel.BASIC);

        // 模拟库存充足
        Mockito.when(inventoryService.getStock("product1")).thenReturn(10);
        Mockito.when(inventoryService.getStock("product2")).thenReturn(20);

        // 模拟商品价格
        Mockito.when(productService.getPrice("product1")).thenReturn(100.0);
        Mockito.when(productService.getPrice("product2")).thenReturn(200.0);

        // 调用订单服务创建订单
        Order order = orderService.createOrder(dto);

        // 验证订单创建成功
        assertNotNull(order);
        // 可以进一步验证订单的属性,如订单状态等
    }

    @Test
    public void testCreateOrderWithInsufficientStock() {
        // 准备测试数据
        OrderDTO dto = new OrderDTO();
        List<Item> items = Arrays.asList(
                new Item("product1", 100)
        );
        dto.setItems(items);
        dto.setUserLevel(UserLevel.BASIC);

        // 模拟库存不足
        Mockito.when(inventoryService.getStock("product1")).thenReturn(10);

        // 调用订单服务创建订单,预期抛出库存不足异常
        assertThrows(StockInsufficientException.class, () -> {
            orderService.createOrder(dto);
        });
    }
}

通过这些测试用例,我们可以成功验证订单服务在不同场景下的功能,包括订单能成功创建以及库存不足时会抛出StockInsufficientException异常,确保了订单服务的多层校验功能正常运作。

五、装饰器模式优缺点​

5.1 优点​

动态扩展功能:能在运行时给对象动态添加功能,无需修改原代码,相比继承更灵活,可随时添加或移除装饰器。​

符合开闭原则:添加新功能只需创建新装饰器类,不用修改被装饰对象代码,降低代码修改风险,提升可维护和扩展性。​

避免类爆炸:通过组合扩展功能,避免因过度继承使类数量剧增,如 Java IO 流体系,用装饰器模式减少类数量,使代码简洁灵活。​

提高代码可维护和可复用性:功能分散到多个装饰器类,职责单一易维护,且可在不同场景复用,提高开发效率、减少冗余。​

5.2 缺点​

⚠️代码复杂度增加:装饰器增多会使代码结构复杂,理解、维护困难,装饰链长时追踪逻辑和调试排查错误难度大,可读性也会下降。​

⚠️性能开销:创建多个装饰器对象会增加内存开销和性能损耗,高并发下可能导致内存溢出,需权衡功能扩展和性能关系,避免过度使用。

当然了,在我们实际开发过程中,一般建议优先采用组合的方式来替代多层继承。虽然面向对象编程里的继承关系虽然是一种强大的代码复用手段,但多层继承往往会带来诸多弊端。

比如代码的可读性变差,子类与父类之间的关系变得错综复杂,维护难度大幅增加。因此,组合通过将不同类的对象组合在一起,使得代码结构更加清晰,各个模块之间的耦合度降低,可维护性和可扩展性都得到显著提升 ,同时也能更灵活地应对业务需求的变化。

另外,虽然装饰器模式可以在不改变对象原有结构和功能的基础上,动态地给对象添加新的功能。然而,在使用装饰器时,也一定要警惕过度装饰

因为当装饰器层层嵌套过多时,程序的执行流程会变得难以追踪,这将极大地增加调试的复杂度。排查问题时,很难快速定位到究竟是哪一层装饰器出现了异常,从而严重影响开发效率和项目进度。所以任何一种设计模式我们都应该根据业务场景去合理应用能发挥他最大的作用

六、总结

装饰器模式为我们在软件开发中动态扩展对象功能提供了一种优雅而灵活的解决方案。通过将功能模块化并以组合的方式应用到对象上,它巧妙地避免了传统继承带来的类爆炸和灵活性不足的问题。从 Java IO 流到 Spring Security 过滤器链,再到订单服务的多层校验,装饰器模式在各个领域都展现了强大的生命力和实用性。​

然而,正如任何设计模式一样,装饰器模式并非银弹。在享受其带来的动态扩展能力时,我们也要警惕装饰链过长导致的性能问题和调试复杂度的增加。在实际应用中,大伙都应切记需要根据具体的业务场景和需求,谨慎地选择和使用装饰器模式,以达到代码的简洁性、可维护性和性能的最佳平衡。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

深情不及里子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值