跳至主要內容

模板模式(下):模板模式与 Callback

AruNi_Lu设计模式设计模式与范式约 1844 字大约 6 分钟

本文内容

前言

上一章open in new window 中,讲解了模板模式的原理和应用,其主要功能就是 提高代码的复用性、基于扩展点可以在不改变源码的情况下方便的扩展功能

而我们常用的 Callback 回调技术,也能起到模板模式的两大作用,那他们有什么区别呢?

1. 什么是回调?

回调 是一种 双向调用 关系,当调用一个 A 函数时,可以 以某种方式将另一个 B 函数注册到 A 函数中,在 A 函数执行时,可以在合适的时机 反过来调用其注册进来的 B 函数,这种机制就叫作回调。

那么如何将回调函数注册到某个函数中呢?

函数参数支持传递函数的编程语言可以直接通过参数来注册,比如 Golang、Python。

而像 Java,就需要使用包裹了回调函数的类对象来实现,这个对象称为回调对象。

下面先来看看 Golang 的实现,比较简单:

func funcA() {
	// 调用 funcB,传入回调函数
	funcB(func(args ...any) {
		fmt.Printf("I am callback function. received args: %v\n", args)
	})
}

func funcB(callback func(args ...any)) {
	// funcB 逻辑...
	callback(1, "a") // 执行回调(funcA 传入的方法)
}

func main() {
	funcA()
}

// 输出:I am callback function. received args: [1 a]

再来看看 Java,其实就是把函数参数改成了某个接口的实现类,然后再调用类的回调方法:

public interface ICallback {
  	void methodToCallback();
}

public class BClass {
    public void process(ICallback callback) {
        // 逻辑处理...
        callback.methodToCallback();	// 执行回调函数
        // 逻辑处理...
    }
}

public class AClass {
    public static void main(String[] args) {
        BClass b = new BClass();
        b.process(new ICallback() { 	// 回调对象
            @Override
            public void methodToCallback() {
                System.out.println("Call back me.");
            }
        });
    }
}

可以发现,回调 也具有 复用和扩展 的功能:

  • 复用:BClass 类中的 process() 方法中的逻辑处理都可以复用,只是回调方法有所不同;
  • 扩展:ICallback 接口中的回调方法可自行定义,扩展 process() 的功能。

回调还分为 同步回调和异步回调,同步回调是在 函数返回之前 执行回调函数,而异步回调则是在 函数返回之后 才执行回调函数(比如可以开启另一个线程去执行处理逻辑和回调函数)。

我们上面的例子就是一个同步回调,在 process() 返回之前执行了回调函数。

异步回调通常用在处理比较耗时的任务,如网络请求、IO 处理等。下面来看看一个异步的网络请求例子,为了方便就使用 Go 来编写了:

// Result 响应结果
type Result struct {
	Data string
	Err  error
}

func requestAsync(url string, callback func(Result)) {
    // 异步处理
	go func() {
		// 模拟网络请求,睡眠一段时间
		time.Sleep(2 * time.Second)

		// 返回结果
		result := Result{
			Data: url + "'s response: {Hello, World!}",
			Err:  nil,
		}

		// 调用回调函数处理结果
		callback(result)
	}()
}

func main() {
	// 发起异步请求
	requestAsync("https://code.0x3f4.run", func(result Result) {
		if result.Err != nil {
			fmt.Println("Error:", result.Err)
		} else {
			fmt.Println("Data:", result.Data)
		}
	})

	// 继续处理其他任务

	fmt.Println("Waiting for response...")
	time.Sleep(3 * time.Second)
	fmt.Println("Done.")
}

/*
输出:
Waiting for response...
Data: https://code.0x3f4.run's response: {Hello, World!}
Done.
*/

判断一个回调是否是异步回调很简单,只需要看 在调用回调方法时,是否需要等待该回调方法执行完毕,才能执行后续逻辑,不需要则属于异步回调。

2. 模板模式 vs 回调

回调的原理和用法讲完后,来看看模板模式跟回调,到底有什么区别?

应用场景 来看,需要分为同步回调和异步回调:

  • 同步回调:与模板模式几乎一致,都是在一个大的算法骨架中,自由替换其中的几个步骤,起到代码复用和扩展的功能;
  • 异步回调:其实更像是 观察者模式,因为异步回调其实并不是按照某套算法骨架的顺序执行,而是在执行完算法骨架后,在某个时间点(有不同的规则)触发这个回调函数。

为了方便理解,再列举一个异步回调的例子

在 JVM 中有一个 shutdown hook(钩子函数 hook 是基于回调的一种应用),可以在程序中事先注册一个 JVM 关闭的 Hook,等到程序关闭前,JVM 会自动调用 Hook。代码实例如下:

public class ShutdownHookDemo {
  
    private static class ShutdownHook extends Thread {
        public void run() {
            System.out.println("I am called during shutting down.");
        }
  }

    public static void main(String[] args) {
        // 注册一个 shutdown hook
        Runtime.getRuntime().addShutdownHook(new ShutdownHook());
    }
  
}

这样就完成了一个 Hook 的注册,下面来看看 addShutdownHook() 的源码实现:

public class Runtime {
    public void addShutdownHook(Thread hook) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("shutdownHooks"));
        }
        // 实际调用 ApplicationShutdownHooks.add(hook);
        ApplicationShutdownHooks.add(hook);
    }
}

class ApplicationShutdownHooks {
    /* The set of registered hooks */
    private static IdentityHashMap<Thread, Thread> hooks;
    static {
            hooks = new IdentityHashMap<>();
        } catch (IllegalStateException e) {
            hooks = null;
        }
    }

    static synchronized void add(Thread hook) {
        if(hooks == null)
            throw new IllegalStateException("Shutdown in progress");

        if (hook.isAlive())
            throw new IllegalArgumentException("Hook already running");

        if (hooks.containsKey(hook))
            throw new IllegalArgumentException("Hook previously registered");

        hooks.put(hook, hook);
    }

    static void runHooks() {
        Collection<Thread> threads;
        synchronized(ApplicationShutdownHooks.class) {
            threads = hooks.keySet();
            hooks = null;
        }
		
        // 遍历 hooks,执行
        for (Thread hook : threads) {
            hook.start();
        }
        for (Thread hook : threads) {
            while (true) {
                try {
                    hook.join();
                    break;
                } catch (InterruptedException ignored) {
                }
            }
        }
    }
}

可以看到,当应用程序关闭时,JVM 会调用 runHooks() 方法,将注册进来的 Hooks 并发的执行。

我们在注册完 Hook 后,并不需要等待 Hook 执行完成,而是在程序关闭时,JVM 会执行这些 Hook,所以是一种异步调用。而 JVM 就像是观察者模式中的观察者,当发先程序要关闭时,就会触发之前注入的事件(Hook)。

代码实现 来看,回调和模板模式完全不同

  • 回调基于 组合关系 来实现,把一个对象传递给另一个对象,是一种对象之间的关系;
  • 模板模式基于 继承关系 来实现,子类重的抽象方法,是一种类之间的关系。

在设计原则中也说过,组合优于继承,所以回调其实比模板模式更加灵活,主要体现在下面几个方面:

  • 像 Java 这种只支持单继承的语言,基于模板模式编写的子类,只能继承一个父类,即只具备一种能力;
  • 回调可以使用匿名类(或直接传递函数)来创建回调对象,无需先定义类;而模板模式不同的实现就需要定义多个不同的子类;
  • 模板方法中的步骤方法是抽象方法时,子类需要实现每一个方法;而回调则没有限制,只需要往模板方法中注入回调对象/方法即可。
上次编辑于: