在日常开发中,我们往往忽视了设计模式的重要性。这可能是因为项目时间紧迫,或者对设计模式理解不深。其实,很多时候我们可能在不经意间已经使用了某些模式。
重要的是要有意识地学习和应用,让代码更加优雅和高效。也许是时候重新审视我们的编程实践,将设计模式融入其中了。
今天由浅入深,重学【装饰器模式】,让我们一起"重学设计模式"。
装饰器模式(Decorator Pattern)是一种结构型设计模式,允许通过一种灵活的方式动态地为对象添加新的行为或职责,而无需修改其原始代码。这种模式提供了比继承更有弹性的替代方案,通过将对象包装在一系列装饰器类中,可以在运行时组合出各种行为。
一、装饰器模式的结构
- Component(组件):定义一个对象接口,装饰器和被装饰对象必须实现的公共接口。
- ConcreteComponent(具体组件):实现Component接口的类,是可以被装饰的类。
- Decorator(装饰器):实现Component接口,同时持有Component类型的成员变量(可以是原始对象或其他装饰器),通过组合将功能动态扩展到该对象。
- ConcreteDecorator(具体装饰器):实现装饰器具体行为扩展的类,通过调用被装饰对象的行为,并在此基础上添加新的功能。
二、不使用装饰器模式时
假设我们有一个基本的电脑类,并想为它动态添加不同的配置(比如CPU、内存、GPU)。
当需要新增配置时,在Computer类中新增即可,清晰明了,没有一个ifelse解决不了的。
java
public class Computer {
private boolean hasRAMUpgrade;
private boolean hasGPUUpgrade;
private boolean hasStorageUpgrade;
// 基础电脑的价格
private double baseCost = 500;
public Computer(boolean hasRAMUpgrade, boolean hasGPUUpgrade, boolean hasStorageUpgrade) {
this.hasRAMUpgrade = hasRAMUpgrade;
this.hasGPUUpgrade = hasGPUUpgrade;
this.hasStorageUpgrade = hasStorageUpgrade;
}
public String getDescription() {
String description = "Basic Computer";
if (hasRAMUpgrade) {
description += ", with 16GB RAM";
}
if (hasGPUUpgrade) {
description += ", with NVIDIA GPU";
}
if (hasStorageUpgrade) {
description += ", with 1TB SSD";
}
return description;
}
public double cost() {
double totalCost = baseCost;
if (hasRAMUpgrade) {
totalCost += 150;
}
if (hasGPUUpgrade) {
totalCost += 300;
}
if (hasStorageUpgrade) {
totalCost += 200;
}
return totalCost;
}
}
package com.guor.designPattern.decorator.no;
public class Test {
public static void main(String[] args) {
// 用户选择了升级内存和显卡
Computer myComputer = new Computer(true, true, true);
System.out.println(myComputer.getDescription() + " costs $" + myComputer.cost());
}
}
三、使用装饰器模式优化
当需要新增配置时,直接实现ComputerDecorator,重写getDescription、cost方法即可,实现了高扩展,解决了ifelse low爆炸的问题。
1、基础电脑组件
java
/**
* 基础电脑
*/
public class BasicComputer extends Computer {
@Override
public String getDescription() {
return "Basic Computer";
}
@Override
public double cost() {
return 500; // 基础电脑的价格
}
}
2、抽象组件
java
package com.guor.designPattern.decorator;
/**
* 抽象组件
*/
public abstract class Computer {
// 描述
public abstract String getDescription();
// 成本
public abstract double cost();
}
3、抽象装饰器
java
/**
* 抽象装饰器
*/
public abstract class ComputerDecorator extends Computer {
protected Computer computer;
public ComputerDecorator(Computer computer) {
this.computer = computer;
}
public abstract String getDescription();
}
4、具体装饰器
java
public class StorageUpgrade extends ComputerDecorator {
public StorageUpgrade(Computer computer) {
super(computer);
}
@Override
public String getDescription() {
return computer.getDescription() + ", with 1TB SSD";
}
@Override
public double cost() {
return computer.cost() + 200;
}
}
public class RAMUpgrade extends ComputerDecorator {
public RAMUpgrade(Computer computer) {
super(computer);
}
@Override
public String getDescription() {
return computer.getDescription() + ", with 16GB RAM";
}
@Override
public double cost() {
return computer.cost() + 150; // 额外内存的价格
}
}
public class GPUUpgrade extends ComputerDecorator {
public GPUUpgrade(Computer computer) {
super(computer);
}
@Override
public String getDescription() {
return computer.getDescription() + ", with NVIDIA GPU";
}
@Override
public double cost() {
return computer.cost() + 300; // 额外显卡的价格
}
}
四、使用装饰器模式有哪些优势
1、动态扩展对象功能
装饰器模式允许在运行时动态地为对象添加功能,而不需要修改对象的结构。相比于继承,它提供了更为灵活的扩展方式,因为继承在编译时就固定了对象的行为,而装饰器则可以通过组合多个装饰器动态改变对象的行为。
2、更好的代码复用性
装饰器可以封装可复用的功能,不同的对象可以共享这些功能而不需要重复编写代码。这种方式提高了代码的可维护性和复用性。
3、遵循单一职责原则
装饰器将对象的核心职责和其他辅助功能(如日志、缓存、权限控制)分离开来,每个装饰器只负责一个特定的功能,避免了类的职责过多。
4、减少子类的数量
继承机制常常会导致大量的子类,因为每个新的功能都需要通过子类来实现。而装饰器模式可以通过组合多个装饰器来实现相同的功能扩展,减少类的数量和复杂度。
5、运行时灵活性
装饰器可以在运行时动态地添加或移除功能,而不需要修改已有的代码。这在开发过程中非常实用,尤其是在需要频繁调整功能的场景中。
五、哪些场景可以使用装饰器模式优化?
1、增强对象功能时
当需要为现有对象增加新功能,且不希望修改对象本身或者使用继承时,可以考虑使用装饰器模式。比如在 UI 组件系统中,基础组件可以通过装饰器动态添加滚动、边框等特性,而不需要修改原始组件的代码。
2、跨领域关注点(如日志、权限验证、性能监控)
在处理一些横切关注点(cross-cutting concerns)时,装饰器模式非常有用。比如日志记录、权限控制、事务管理、性能监控等功能,它们常常不属于对象的核心业务逻辑,通过装饰器模式可以将这些功能单独提取出来,并动态添加到业务逻辑中,而不污染原始代码。
示例:
- 日志记录:为方法调用自动添加日志功能,记录方法的输入、输出和执行时间。
- 权限控制:在执行某些业务方法前,动态检查用户权限。
- 事务管理:在数据库操作前后动态添加事务处理。
3、需要多个功能组合时
当对象的功能可以通过不同的方式组合时,装饰器模式是一个理想的选择。它允许通过多个装饰器的组合形成不同的功能,比如装饰器可以为对象依次添加不同的行为,从而形成灵活的功能组合。
示例:在咖啡订单系统中,咖啡基础类可以通过装饰器动态添加牛奶、糖、巧克力等配料,而不用创建大量的子类来表示每一种咖啡组合。
4、避免类爆炸(class explosion)
通过装饰器模式可以避免通过继承产生大量的子类。特别是在当类的功能是通过多个维度扩展时,比如一种基础功能的类可能同时需要加日志、加缓存、加权限控制,使用继承将导致很多子类,而使用装饰器可以有效减少类的数量。
5、与策略模式配合使用
装饰器模式可以与策略模式结合起来,为策略类提供更丰富的功能扩展。比如在电商网站中,可以为订单结算策略类动态添加打折、积分计算等功能,而不必为每种结算方式都创建新的子类。
六、优化场景举例
场景 1:Web 应用中的权限验证和日志记录
在 Web 应用中,通常需要在处理请求时添加权限验证和日志记录。假设系统已经有一个基本的请求处理类,通过装饰器可以实现动态添加权限验证和日志记录功能。
在这个场景中,通过装饰器,我们可以动态地为请求处理类添加权限验证和日志记录功能,而不必修改现有的 RequestHandler 类。
java
// 定义一个处理请求的接口
interface RequestHandler {
void handle(String request);
}
// 具体的请求处理类
class BasicRequestHandler implements RequestHandler {
@Override
public void handle(String request) {
System.out.println("Handling request: " + request);
}
}
// 抽象装饰器类
abstract class RequestHandlerDecorator implements RequestHandler {
protected RequestHandler handler;
public RequestHandlerDecorator(RequestHandler handler) {
this.handler = handler;
}
@Override
public void handle(String request) {
handler.handle(request);
}
}
// 日志记录装饰器
class LoggingDecorator extends RequestHandlerDecorator {
public LoggingDecorator(RequestHandler handler) {
super(handler);
}
@Override
public void handle(String request) {
System.out.println("Logging request: " + request);
super.handle(request);
}
}
// 权限验证装饰器
class AuthDecorator extends RequestHandlerDecorator {
public AuthDecorator(RequestHandler handler) {
super(handler);
}
@Override
public void handle(String request) {
if (checkPermission(request)) {
System.out.println("Permission granted");
super.handle(request);
} else {
System.out.println("Permission denied");
}
}
private boolean checkPermission(String request) {
// 简单模拟权限验证逻辑
return request.contains("admin");
}
}
// 测试客户端
public class DecoratorExample {
public static void main(String[] args) {
RequestHandler handler = new BasicRequestHandler();
RequestHandler authHandler = new AuthDecorator(handler);
RequestHandler loggingAuthHandler = new LoggingDecorator(authHandler);
// 测试通过权限验证和日志记录的请求处理
loggingAuthHandler.handle("admin: important data");
}
}
场景 2:电子商务系统中的商品定价策略
假设一个电子商务系统中,需要对商品的价格进行灵活调整。可以通过装饰器模式为商品价格添加不同的优惠策略,例如打折、满减、会员优惠等。
在此例中,通过装饰器可以灵活组合打折和满减的策略,而不需要为每种优惠策略创建新的子类,简化了系统结构。
java
// 商品接口
interface Product {
double getPrice();
}
// 基础商品类
class BasicProduct implements Product {
private double price;
public BasicProduct(double price) {
this.price = price;
}
@Override
public double getPrice() {
return price;
}
}
// 抽象装饰器类
abstract class ProductDecorator implements Product {
protected Product product;
public ProductDecorator(Product product) {
this.product = product;
}
@Override
public double getPrice() {
return product.getPrice();
}
}
// 打折装饰器
class DiscountDecorator extends ProductDecorator {
public DiscountDecorator(Product product) {
super(product);
}
@Override
public double getPrice() {
return product.getPrice() * 0.9; // 10% 折扣
}
}
// 满减装饰器
class FullReductionDecorator extends ProductDecorator {
public FullReductionDecorator(Product product) {
super(product);
}
@Override
public double getPrice() {
double price = product.getPrice();
if (price > 200) {
return price - 20; // 满200减20
}
return price;
}
}
// 测试客户端
public class EcommerceExample {
public static void main(String[] args) {
Product product = new BasicProduct(250);
Product discountProduct = new DiscountDecorator(product);
Product finalProduct = new FullReductionDecorator(discountProduct);
System.out.println("Final price: " + finalProduct.getPrice());
}
}
装饰器模式在需要动态扩展对象功能、复用功能模块、组合不同行为以及避免类爆炸的场景中非常有用。它可以通过装饰器类灵活地为对象添加职责,从而提升代码的可维护性、复用性和扩展性。
七、典型案例:Java I/O 流中的装饰器模式
在JDK源码中,装饰器模式有很多应用,特别是在I/O类库中,装饰器模式被广泛使用。通过装饰器模式,Java的I/O类库可以动态地为输入输出流添加缓冲、数据处理等功能,而不改变原始的输入输出流的实现。
在Java中,java.io包的输入输出流(InputStream、OutputStream)就是装饰器模式的一个典型应用。InputStream和OutputStream类是抽象基类,具体的实现类如FileInputStream、BufferedInputStream等都是具体组件,而BufferedInputStream、DataInputStream等类则是装饰器,它们通过封装InputStream实现了额外的功能,比如缓冲和数据类型处理。
1、BufferedInputStream 是 InputStream 的装饰器
java
// java.io.BufferedInputStream 源码片段
public class BufferedInputStream extends FilterInputStream {
// Buffer size
protected volatile byte[] buf;
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
public BufferedInputStream(InputStream in, int size) {
super(in); // 调用父类 FilterInputStream 的构造方法,传入被装饰的 InputStream
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
@Override
public int read() throws IOException {
// 通过缓存读取流数据,添加缓冲功能
}
// 其他相关的重写方法
}
FilterInputStream:抽象装饰器类,它继承了InputStream,并且持有一个InputStream的引用,装饰器类通过持有的这个引用来增强对象的功能。
BufferedInputStream:具体的装饰器类,通过为InputStream添加缓冲功能提高读取效率。
java
public class FileInputStream extends InputStream {
// 打开文件并创建文件输入流
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
// 其他相关代码...
}
// 客户端使用
public class DecoratorTest {
public static void main(String[] args) throws IOException {
InputStream fileStream = new FileInputStream("data.txt");
// 用 BufferedInputStream 装饰 FileInputStream,增加缓冲功能
InputStream bufferedStream = new BufferedInputStream(fileStream);
int data = bufferedStream.read(); // 读取数据
while (data != -1) {
System.out.print((char) data);
data = bufferedStream.read();
}
bufferedStream.close();
}
}
在这个例子中,FileInputStream是一个具体的输入流,而BufferedInputStream是装饰器,增加了缓冲功能。客户端可以通过组合来增强输入流的功能,而不必修改原始的FileInputStream类。
2、DataInputStream 是 InputStream 的装饰器
DataInputStream 是另一个经典的装饰器,它允许从输入流中读取基本数据类型(如int、float等)。
java
// java.io.DataInputStream 源码片段
public class DataInputStream extends FilterInputStream implements DataInput {
public DataInputStream(InputStream in) {
super(in); // 调用装饰的 InputStream
}
// 读取一个 int 数据
public final int readInt() throws IOException {
int ch1 = in.read();
int ch2 = in.read();
int ch3 = in.read();
int ch4 = in.read();
if ((ch1 | ch2 | ch3 | ch4) < 0)
throw new EOFException();
return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
}
// 其他相关的读取方法
}
在这个例子中,DataInputStream装饰了一个InputStream,并且扩展了功能,支持读取基本数据类型。
java
public class DecoratorTest {
public static void main(String[] args) throws IOException {
InputStream fileStream = new FileInputStream("data.bin");
// 使用 BufferedInputStream 增加缓冲功能
InputStream bufferedStream = new BufferedInputStream(fileStream);
// 使用 DataInputStream 增加读取基本数据类型的功能
DataInputStream dataStream = new DataInputStream(bufferedStream);
int number = dataStream.readInt(); // 从文件中读取一个 int 值
System.out.println("Read integer: " + number);
dataStream.close();
}
}
在这个例子中,DataInputStream为InputStream增加了读取基本数据类型的功能。通过使用BufferedInputStream和DataInputStream的组合,客户端可以获得既有缓冲功能,又可以读取基本数据类型的输入流。
3、PrintStream 和 PrintWriter 也是装饰器
PrintStream 和 PrintWriter 是对输出流的装饰器,它们添加了打印方法,比如print()和println(),使得输出流可以方便地输出字符串、对象等数据。
java
public class PrintStream extends FilterOutputStream implements Appendable, Closeable {
// 构造函数接受 OutputStream 作为参数
public PrintStream(OutputStream out) {
super(out); // 调用 FilterOutputStream 构造方法
}
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
// 其他相关代码...
}
在Java I/O类库中,装饰器模式通过扩展InputStream和OutputStream的功能而被广泛使用。
BufferedInputStream、DataInputStream、PrintStream等类都是装饰器,它们在不改变底层流的情况下增加了缓冲、数据类型处理和打印等功能。
通过装饰器模式,Java I/O类库实现了灵活的功能扩展,并且使用者可以根据需求动态组合这些装饰器。
4、除了I/O流之外,Java中的装饰器模式也被应用在其他场景中:
(1)Java的集合类包装器
Collections.synchronizedList()、Collections.unmodifiableList()等方法实际上是对集合进行包装,动态为集合对象增加线程安全或不可修改的特性。
(2)java.util.logging.Logger
日志框架中的Logger类允许通过装饰器为日志添加各种处理行为,例如格式化输出、发送到不同的目的地(控制台、文件、远程服务器等)。
八、总结
装饰器模式在JDK源码中的典型应用主要体现在I/O流的设计中。通过装饰器模式,Java的InputStream和OutputStream类可以在不修改原始类的情况下动态扩展功能。例如,BufferedInputStream和DataInputStream分别为基础流增加了缓冲读取和读取基本数据类型的能力,而PrintStream为输出流提供了打印字符串、对象等的便捷方法。
装饰器模式允许开发者通过组合多个装饰器类,为对象添加不同的功能,避免了创建大量子类的复杂性。例如,BufferedInputStream可以与FileInputStream组合使用,为文件输入流增加缓冲,提升读取效率。这种模式不仅灵活,还使代码复用性和扩展性大幅提升。
此外,装饰器模式还广泛应用于Java集合类和日志系统等场景,像Collections.synchronizedList()、Collections.unmodifiableList()等都通过装饰器动态增加线程安全或不可修改的特性。装饰器模式通过其灵活的结构设计,实现了功能增强与解耦的目的,是Java开发中的常见设计模式。