装饰模式
装饰模式(Decorator Pattern)是一种常见的设计模式,允许你在不改变原有对象的前提下,对对象的功能进行增强。
注意装饰器模式和前面讲到的 适配器模式 的区别:
适配器模式是说,原始对象实现了接口 A,但我们现在需要接口 B,所以用一个适配器在中间把接口 A 转换成接口 B。
装饰器模式是说,原始对象实现了接口 A,但是我们需要对它的功能进行增强,所以在原始对象外面包一层装饰器来达到这个目的。
这么说有点抽象,我们通过一个具体的例子来理解。
缓存场景
假设我们有一个数据访问对象(DAO),负责根据用户 ID 从数据库中查询用户名。
// 数据查询接口
interface UserDao {
String getUserNameBy(int id);
}
// 数据查询的实现类
class UserDaoImpl implements UserDao {
@Override
public String getUserNameBy(int id) {
// 模拟数据库查询
String userName = "User" + id;
System.out.println("去数据库查询 id = " + id + " 对应的用户名为:" + userName);
return userName;
}
}
现在,客户端代码每次调用 getUserNameBy
方法都会访问一次数据库:
// 业务代码
public class Main {
public static void main(String[] args) {
UserDao dao = new UserDaoImpl();
dao.getUserNameBy(1);
// 输出:去数据库查询 id = 1 对应的用户名为:User1
dao.getUserNameBy(1);
// 输出:去数据库查询 id = 1 对应的用户名为:User1
}
}
如果同一个用户被频繁查询,每次都访问数据库会造成不必要的性能开销。我们希望增加一个缓存层,查询过的用户直接从缓存中读取,避免重复查询数据库。
最直接的想法是修改 UserDaoImpl
的代码,在 getUserNameBy
方法里增加缓存逻辑。但这样做违背了开闭原则(对扩展开放,对修改关闭),直接修改原有代码容易引入新的 bug,并且如果以后想去掉缓存功能,又得去修改代码。
我们也不应该把缓存的逻辑写到业务代码中,因为业务代码应该专注业务逻辑,而不应该关心数据获取的底层细节。
这时候,装饰模式就派上用场了,我们可以创建一个「装饰器」类,像一个包装盒一样把原始的 UserDaoImpl
对象包起来,在不改变 UserDaoImpl
内部代码的情况下增加缓存功能,同时还不需要修改业务代码。
// 装饰器
class UserDaoCacheDecorator implements UserDao {
private final UserDao decoratedDao;
// 用哈希表来模拟缓存,key 是用户 ID,value 是用户名
private final Map<Integer, String> cache = new HashMap<>();
public UserDaoCacheDecorator(UserDao decoratedDao) {
this.decoratedDao = decoratedDao;
}
@Override
public String getUserNameBy(int id) {
// 先查缓存
if (cache.containsKey(id)) {
String userName = cache.get(id);
System.out.println("命中缓存,用户名为:" + userName);
return userName;
}
// 缓存未命中,查询数据库
String userName = decoratedDao.getUserNameBy(id);
System.out.println("去数据库查询 id = " + id + " 对应的用户名为:" + userName);
// 将结果存入缓存
cache.put(id, userName);
return userName;
}
}
这个 UserDaoCacheDecorator
就是一个装饰器,它也实现了 UserDao
接口,所以对于客户端来说,它的使用方式和 UserDaoImpl
完全一样。
现在,客户端可以这样使用:
public class Main {
public static void main(String[] args) {
// 用缓存装饰器包装原始的数据库查询对象
UserDao cachedDao = new UserDaoCacheDecorator(new UserDaoImpl());
cachedDao.getUserNameBy(1);
// 输出:去数据库查询 id = 1 对应的用户名为:User1
cachedDao.getUserNameBy(1);
// 输出:命中缓存,用户名为:User1
}
}
看,我们没有修改 UserDaoImpl
的任何代码,就给它无感地增加了缓存功能。UserDaoImpl
中只有数据库查询的逻辑,而缓存逻辑被封装在了 UserDaoCacheDecorator
中。
装饰器模式主要包含以下几个核心角色:
- 组件接口(Component):定义了被装饰对象和装饰器对象的通用接口。在上面的例子中,就是
UserDao
接口。 - 具体组件(Concrete Component):实现了组件接口的具体对象。在上面的例子中,就是
UserDaoImpl
类。 - 装饰器(Decorator):也实现了组件接口,并持有另一个被装饰组件的实例。装饰器的任务是在调用被装饰对象的方法前后,增加额外的逻辑。在上面的例子中,就是
UserDaoCacheDecorator
类。
在业务代码中,我们只需要使用 UserDao
接口,而不需要关心具体的实现类是 UserDaoImpl
还是 UserDaoCacheDecorator
。这就是装饰模式的精髓:在不改变对象自身的基础上,动态地给对象添加额外的职责。
中间件场景
装饰器模式在实际开发中一个非常经典的应用就是中间件(Middleware)。比如在一个 Web 服务器中,一个 HTTP 请求在到达真正的业务处理逻辑之前,可能需要经过一系列的中间件处理,例如日志记录、用户认证、数据压缩等。
这些中间件就像一层层的洋葱,将核心的业务逻辑包裹在最里面。每个中间件完成自己的任务后,将请求传递给下一个中间件,最终到达业务处理器。
这种结构可以让我们灵活地组合和重用这些功能模块,同时又不影响业务逻辑。
下面我们来模拟这个场景,首先定义简化的 Request
和 Response
类,它们分别存储 HTTP 请求和响应数据:
public class Request {
private final String path;
private final Map<String, String> headers = new HashMap<>();
public Request(String path) {
this.path = path;
}
public String getPath() {
return path;
}
public void addHeader(String key, String value) {
this.headers.put(key, value);
}
public String getHeader(String key) {
return this.headers.get(key);
}
@Override
public String toString() {
return "Request [path=" + path + ", headers=" + headers + "]";
}
}
public class Response {
private String body;
private int statusCode;
public void setBody(String body) {
this.body = body;
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
@Override
public String toString() {
return "Response [statusCode=" + statusCode + ", body='" + body + "']";
}
}
接下来,定义所有处理器(包括中间件和业务处理器)都需要遵守的契约 Handler
接口:
// Component
public interface Handler {
void handle(Request request, Response response);
}
然后,定义核心业务逻辑处理器 BusinessHandler
:
// Concrete Component
public class BusinessHandler implements Handler {
@Override
public void handle(Request request, Response response) {
System.out.println(">>> Entering BusinessHandler...");
// 模拟业务逻辑
if ("/api/user".equals(request.getPath())) {
response.setStatusCode(200);
response.setBody("{ \"name\": \"Alice\", \"email\": \"alice@example.com\" }");
} else {
response.setStatusCode(404);
response.setBody("Not Found");
}
System.out.println("Business logic finished.");
System.out.println("<<< Exiting BusinessHandler...");
}
}
接下来,我们定义两个中间件(装饰器):LoggingMiddleware
和 AuthenticationMiddleware
。
LoggingMiddleware
用于记录请求的关键信息和耗时,并将请求传递给下一个处理器:
// Concrete Decorator
public class LoggingMiddleware implements Handler {
private final Handler next;
public LoggingMiddleware(Handler next) {
this.next = next;
}
@Override
public void handle(Request request, Response response) {
System.out.println(">>> Entering LoggingMiddleware...");
long startTime = System.currentTimeMillis();
System.out.println("Request received: " + request.getPath());
// 将请求传递给下一个处理器
this.next.handle(request, response);
long duration = System.currentTimeMillis() - startTime;
System.out.println("Response sent with status: " + response.getStatusCode() + ". Request took " + duration + "ms.");
System.out.println("<<< Exiting LoggingMiddleware...");
}
}
AuthenticationMiddleware
用于校验请求的身份,如果认证成功,则将请求传递给下一个处理器,否则不再继续向后传递:
// Concrete Decorator
public class AuthenticationMiddleware implements Handler {
private final Handler next;
public AuthenticationMiddleware(Handler next) {
this.next = next;
}
@Override
public void handle(Request request, Response response) {
System.out.println(">>> Entering AuthenticationMiddleware...");
String authToken = request.getHeader("Authorization");
if ("valid-token".equals(authToken)) {
System.out.println("Authentication successful. Passing to next handler.");
// 认证成功,传递给下一个处理器
this.next.handle(request, response);
} else {
// 认证失败,直接返回,不再继续向后传递
System.out.println("Authentication failed. Stop the request.");
response.setStatusCode(401);
response.setBody("Unauthorized");
}
System.out.println("<<< Exiting AuthenticationMiddleware...");
}
}
可以看到,这两个中间件都实现了 Handler
接口,并且都持有一个 Handler
类型的 next
成员变量。它们在 handle
方法中执行自己的逻辑,然后决定是否调用 next.handle()
将请求传递下去。
最后,看客户端如何将它们组装起来:
public class Main {
public static void main(String[] args) {
// 1. 核心业务处理器
Handler businessHandler = new BusinessHandler();
// 2. 用认证中间件装饰业务处理器
Handler authMiddleware = new AuthenticationMiddleware(businessHandler);
// 3. 用日志中间件装饰认证中间件
Handler handlerChain = new LoggingMiddleware(authMiddleware);
// 最终的调用顺序是:
// LoggingMiddleware -> AuthenticationMiddleware -> BusinessHandler
// 认证成功的请求
Request successfulRequest = new Request("/api/user");
successfulRequest.addHeader("Authorization", "valid-token");
Response response1 = new Response();
handlerChain.handle(successfulRequest, response1);
System.out.println("Final Response: " + response1);
System.out.println("\n");
// 认证失败的请求
Request failedRequest = new Request("/api/user");
failedRequest.addHeader("Authorization", "invalid-token");
Response response2 = new Response();
handlerChain.handle(failedRequest, response2);
System.out.println("Final Response: " + response2);
}
}
输出如下:
>>> Entering LoggingMiddleware...
Request received: /api/user
>>> Entering AuthenticationMiddleware...
Authentication successful. Passing to next handler.
>>> Entering BusinessHandler...
Business logic finished.
<<< Exiting BusinessHandler...
<<< Exiting AuthenticationMiddleware...
Response sent with status: 200. Request took 1ms.
<<< Exiting LoggingMiddleware...
Final Response: Response [statusCode=200, body='{ "name": "Alice", "email": "alice@example.com" }']
>>> Entering LoggingMiddleware...
Request received: /api/user
>>> Entering AuthenticationMiddleware...
Authentication failed. Stop the request.
<<< Exiting AuthenticationMiddleware...
Response sent with status: 401. Request took 0ms.
<<< Exiting LoggingMiddleware...
Final Response: Response [statusCode=401, body='Unauthorized']
尤其注意中间件的调用顺序,外层的中间件会先执行、后退出。
其实这就是 单链表的递归遍历 中你把代码写到前序位置,执行顺序就是从前往后;你把代码写到后序位置,执行顺序就是从后往前。
这个例子清晰地展示了装饰器模式的强大之处:
- 链式调用:请求像穿过管道一样依次通过各个中间件,每个中间件都可以对请求进行处理。
- 职责分离:每个中间件只关心自己的职责(日志、认证等),核心业务逻辑不受干扰。
- 灵活组合:我们可以轻易地增加、删除或改变中间件的顺序,而不需要修改任何一个中间件或业务处理器的代码。例如,如果我们还想增加一个数据压缩的中间件,只需再写一个
CompressionMiddleware
,然后把它加到调用链中即可。
总结
装饰器模式和适配器模式有些相似,都是包装另一个对象。但它们的目的不同:
- 适配器模式的目的是转换接口,让原本不兼容的两个对象能够协同工作。
- 装饰器模式的目的是增强功能,在不改变原有对象接口的情况下,为其添加新的行为。
装饰器模式的主要优势:
- 符合开闭原则:可以在不修改现有代码的情况下,为对象扩展功能。
- 灵活性高:可以动态地、按需地为对象添加或移除功能,并且可以自由组合多个装饰器。
- 职责分离:将核心职责和附加职责分离开,使得代码结构更清晰,每个类的功能更单一。
装饰器模式的主要缺点是可能导致类数量增加,因为每增加一种装饰功能,就需要增加一个新的装饰器类,如果装饰组合很多,会导致系统中出现大量的小类。
总的来说,当你需要在一个现有类的基础上动态地添加功能,并且不想通过继承来修改它时,装饰器模式是一个非常不错的选择。