在软件设计中,委托模式是一种强大且优雅的设计模式,它是23种经典设计模式中的一种。委托模式的核心思想是:一个对象(委托者)将某些职责或任务交给另一个对象(受托者)来处理。
这听起来很简单,如果你已经是一个有经验的开发者,且曾经在事件处理、回调函数或者某些设计模式中遇到过它,那么恭喜你,你已经与委托打过照面了。但仅仅只是用过可不算完全掌握,只有理解其精髓才能显著提升代码质量和灵活性。
然而,网络上的大部分教程对于委托模型的讲解都过于公式化,给开发者学习造成了一定的障碍。我想从一个简单的需求入手,用最通俗易懂的方式为大家讲解委托模式。
阅读本文需要具备面向对象编程基础,如果你尚且不会使用面向对象编程,则理解本文可能会有困难。
需求:记录Map存入元素的数量
假设现在有一个需求:为了方便之后的性能分析,要求能够记录项目中每个Map存入元素的数量。
import java.util.HashMap;
import java.util.Map;
public class ExampleController {
// todo 记录这个 map 中 put 元素的数量
private Map<String, String> exampleMap = new HashMap<>();
public void handle1() {
// 其他业务代码...
exampleMap.put("example", "test1");
}
public void handle2() {
// 其他业务代码...
exampleMap.put("example", "test2");
}
// 其他方法
}
初级解法(面向过程)
对于这个需求,即使是一些刚学会编程的开发者,也应该能够很轻松的想出解决方法。
import java.util.HashMap;
import java.util.Map;
public class ExampleController {
private int exampleMapPutCount = 0;
private Map<String, String> exampleMap = new HashMap<>();
public void handle1() {
// 其他业务代码...
exampleMap.put("example", "test1");
exampleMapPutCount++;
}
public void handle2() {
// 其他业务代码...
exampleMap.put("example", "test2");
exampleMapPutCount++;
}
// 其他方法
}
在这段更改后代码中,其核心的逻辑就是查找业务代码中所有对exampleMap
执行put
操作的地方,然后在其put
操作之后exampleMapPutCount
自增1,以此来实现记录exampleMap
中put
元素数量的功能。
然而,这种做法的缺点也是非常突出的:这种方式需要修改代码中大量的地方、对已有代码的侵入性非常强,而且如果以后新增功能中需要exampleMap
进行put
操作,处理该需求的开发者可能会遗漏对exampleMapPutCount
的更新操作。
而且,这段修改仅对该Controller
生效,如果我们的项目中还有其他Map
需要记录put
元素数量,我们将不得不为每个需要记录的Map
对象都创建一个计数器,同时寻找调用那些Map
的put
方法的地方,并在相应的地方为计数器执行自增操作。当我们的项目规模逐渐增大时,其工作量将会成倍地增加。
总之:这种修改不符合我们对软件工程中高内聚、低耦合的要求。
面向对象编程
那么,有没有一种方法能够更轻松、更优雅的完成这个需求呢?精通面向对象编程的程序员给出了答案:
import java.util.HashMap;
import java.util.Map;
public class CounterHashMap<K, V> extends HashMap<K, V> {
private int counter = 0;
@Override
public V put(K key, V value) {
counter++;
return super.put(key, value);
}
@Override
public void putAll(Map<? extends K, ? extends V> map) {
counter += map.size();
super.putAll(map);
}
public int getCounter() {
return counter;
}
}
在这段代码中,我们创建了一个CounterHashMap
类,在内部创建一个计数器,使其继承HashMap
并覆写put
方法,每次调用put
方法时都会使内部的计数器自增,并在最后暴露一个getter用于获取该计数器。
import java.util.Map;
public class ExampleController {
private Map<String, String> exampleMap = new CounterHashMap<>();
public void handle1() {
// 其他业务代码...
exampleMap.put("example", "test1");
}
public void handle2() {
// 其他业务代码...
exampleMap.put("example", "test2");
}
// 其他方法
}
接下来,我们只需要回到Controller中,将new HashMap<>()
替换为new CounterHashMap<>()
,这样每当有元素被放入这个Map时,我们的CounterHashMap
就会使其内部的计数器自动增加,比之前的耦合度降低了。
那么,这种方法是否就是本篇文章要讨论的委托模式呢?显然不是!因为它没有实现委托模型的本质:职责转移,关注点分离。
想象一下,Java中的Map类型有许多,除了常用的HashMap
以外还有LinkedHashMap
(元素有序)、ConcurrentHashMap
(线程安全)等其他类型的Map。按照上面提到的这种方法,我们将不得不为我们需要使用的每个Map类型都创建一个带计数器的版本:CounterLinkedHashMap
、CounterConcurrentHashMap
、CounterXxxMap
等,我们将会创建大量的、重复的类。
总之,虽然这种方法在一定程度上减少了耦合,但依然不符合我们对优秀软件中高内聚、低耦合的理想。要想真正优雅地完成这个需求,我们可以借助委托模式来实现。
委托模式
在这个需求中应用委托模型,具体代码如下:
import org.jetbrains.annotations.NotNull;
import java.util.Map;
public class CounterMap<K, V> extends Map<K, V> {
private final Map<K, V> delegate;
private int counter = 0;
public CounterMap(Map<K, V> delegate) {
this.delegate = delegate;
}
@Override
public int size() {
return delegate.size();
}
@Override
public boolean isEmpty() {
return delegate.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return delegate.containsKey(key);
}
@Override
public V remove(Object key) {
return delegate.remove(key);
}
@Override
public V put(K key, V value) {
counter++;
return delegate.put(key, value);
}
@Override
public void putAll(@NotNull Map<? extends K, ? extends V> m) {
counter += m.size();
delegate.putAll(m);
}
// ... 将其他方法改为 delegate.xxx()
public int getCounter() {
return counter;
}
}
在这段代码中,我们创建了一个CounterMap
,它只负责记录put的元素数量,而不亲自处理put的内容,具体put的工作将委托给其他有能力的Map去实现。
import java.util.HashMap;
import java.util.Map;
public class ExampleController {
private Map<String, String> exampleMap = new CounterMap<>(new HashMap<>());
public void handle1() {
// 其他业务代码...
exampleMap.put("example", "test1");
}
public void handle2() {
// 其他业务代码...
exampleMap.put("example", "test2");
}
// 其他方法
}
与之相对的,我们要将原本new HashMap<>()
替换成new CounterMap<>(new HashMap<>())
,这段代码创建了一个CounterMap
的委托类,并为其分配一个委托对象HashMap
。该委托类将会在put调用时记录存入的元素数量,同时put(和Map类的其他方法)的具体工作都将交由委托对象实现。
乍一看,也许你会觉得CounterMap
的实现比起上一个版本多了许多内容,可能导致工作量的增加。然而,这种设计模式最大的优点便是它极其优秀的灵活性:当我们需要使用具有不同特性的Map时,只需要将受托者替换成对应的实例即可。例如:当我们需要ConcurrentHashMap
时,只需要将代码替换成new CounterMap<>(new ConcurrentHashMap<>())
。
委托模型允许一个对象将某些任务或职责委托给另一个对象来执行。简单来说,就是 “我有个任务,我不直接去做,而是把这个任务交给另一个更合适的人来处理”。
想象一下现实中的委托:你是公司老板(委托者),需要处理一份法务合同。你不是亲自研究法律条文,而是委托给你的律师(受托者)去处理。你只关心结果(合同是否合规),不关心律师内部如何研究条款、查找案例等具体过程。
总结:为什么使用委托模型?优势何在?
委托通常比继承更灵活,因为继承在编译时就固定了实现,而委托允许在运行时动态改变受托对象。
委托者只依赖接口,不依赖具体实现类,这符合依赖倒置原则。
委托者不需要知道受托者内部如何实现,只要受托者遵守接口约定,委托者就能正常工作。
委托有效降低了类之间的直接依赖,解决了单继承语言的局限性,避免了深层次的、僵化的类层次结构。
委托模型通过将职责转移而非继承,为代码设计带来了巨大的灵活性、可维护性和可扩展性。它是实现松耦合、遵循面向对象设计原则的关键技术之一。
当你发现一个类试图做太多事情,或者想在运行时灵活改变对象的行为,或者想避免构建复杂的继承树时,问问自己:"这个任务可以委托给谁?" 答案往往能让你的代码变得更加优雅和灵活。