编程中的委托模式:让你的代码更灵活优雅
开发 135

在软件设计中,委托模式是一种强大且优雅的设计模式,它是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,以此来实现记录exampleMapput元素数量的功能。

然而,这种做法的缺点也是非常突出的:这种方式需要修改代码中大量的地方、对已有代码的侵入性非常强,而且如果以后新增功能中需要exampleMap进行put操作,处理该需求的开发者可能会遗漏对exampleMapPutCount的更新操作。

而且,这段修改仅对该Controller生效,如果我们的项目中还有其他Map需要记录put元素数量,我们将不得不为每个需要记录的Map对象都创建一个计数器,同时寻找调用那些Mapput方法的地方,并在相应的地方为计数器执行自增操作。当我们的项目规模逐渐增大时,其工作量将会成倍地增加。

总之:这种修改不符合我们对软件工程中高内聚、低耦合的要求。

面向对象编程

那么,有没有一种方法能够更轻松、更优雅的完成这个需求呢?精通面向对象编程的程序员给出了答案:

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类型都创建一个带计数器的版本:CounterLinkedHashMapCounterConcurrentHashMapCounterXxxMap等,我们将会创建大量的、重复的类。

总之,虽然这种方法在一定程度上减少了耦合,但依然不符合我们对优秀软件中高内聚、低耦合的理想。要想真正优雅地完成这个需求,我们可以借助委托模式来实现。

委托模式

在这个需求中应用委托模型,具体代码如下:

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<>())

委托模型允许一个对象将某些任务或职责委托给另一个对象来执行。简单来说,就是 “我有个任务,我不直接去做,而是把这个任务交给另一个更合适的人来处理”。

想象一下现实中的委托:你是公司老板(委托者),需要处理一份法务合同。你不是亲自研究法律条文,而是委托给你的律师(受托者)去处理。你只关心结果(合同是否合规),不关心律师内部如何研究条款、查找案例等具体过程。

总结:为什么使用委托模型?优势何在?

  • 委托通常比继承更灵活,因为继承在编译时就固定了实现,而委托允许在运行时动态改变受托对象。

  • 委托者只依赖接口,不依赖具体实现类,这符合依赖倒置原则。

  • 委托者不需要知道受托者内部如何实现,只要受托者遵守接口约定,委托者就能正常工作。

  • 委托有效降低了类之间的直接依赖,解决了单继承语言的局限性,避免了深层次的、僵化的类层次结构。

特性

委托

继承

关系

has-aCounterMap有一个HashMap

is-aCounterMap是一个HashMap

耦合度

松耦合 (通过接口交互)

紧耦合 (子类依赖父类实现细节)

灵活性

(运行时可替换受托对象)

(编译时确定,关系固定)

复用

行为复用 (复用具体实现类的功能)

代码/结构复用 (复用父类代码和结构)

多继承

可以(通过委托链的形式)

大多语言不支持 (易导致复杂性问题)

主要目的

动态分配职责

建立类型层次和代码复用

委托模型通过将职责转移而非继承,为代码设计带来了巨大的灵活性、可维护性和可扩展性。它是实现松耦合、遵循面向对象设计原则的关键技术之一。

当你发现一个类试图做太多事情,或者想在运行时灵活改变对象的行为,或者想避免构建复杂的继承树时,问问自己:"这个任务可以委托给谁?" 答案往往能让你的代码变得更加优雅和灵活。

编程中的委托模式:让你的代码更灵活优雅
https://www.hamster3.cn/archives/bian-cheng-zhong-de-wei-tuo-mo-shi-rang-ni-de-dai-ma-geng-ling-huo-you-ya
作者
叁只仓鼠
发布于
更新于
许可