在日常工作中,我们常常会遇到产品增加需求的情况,或者需要设计一个工作流引擎。如果采用硬编码的方式,不仅不利于代码的解耦,后期的维护工作也会让人头疼不已。这时,责任链(Chain of responsibility)模式就能派上用场。

责任链模式概述

责任链模式是一种行为型设计模式。通过使用该模式,我们可以为请求创建一条由多个处理器组成的链路。每个处理器专注于自己的职责,彼此之间不存在耦合关系。当一个处理器完成自己的任务后,请求对象会被传递到链路中的下一个处理器继续处理。

责任链模式在许多流行框架中都有广泛应用,例如中间件和拦截器等框架组件就采用了这种设计模式。在进行 Web 接口开发时,像记录访问日志、解析 Token、格式化接口响应的统一结构等项目公共逻辑,通常都在中间件和拦截器中完成。这样做可以将这些基础操作与接口的业务逻辑解耦。

责任链模式的实现示例:医院患者就诊流程

PHP 实现

责任链模式使用面向对象语言实现非常简单,因为可以通过抽象类复用公共逻辑,链路的执行逻辑也可以由各个处理器自行实现。下面我们以医院患者就诊流程为例,展示如何使用 PHP 实现责任链模式。

declare(strict_types=1);

// 就诊状态枚举
enum PatientStatus: int
{
    case Start = 1; // 状态:开始看病
    case Reception = 2; // 状态:已挂号
    case Clinic = 3; // 状态:已看诊
    case Cashier = 4; // 状态:已缴费
    case Pharmacy = 5; // 状态:已拿药
}

// 患者类
class Patient
{
    public ?PatientStatus $status = null;
    public function __construct(
        public string $name
    )
    {}
}

// 就诊流程责任链接口定义
interface PatientHandler
{
    public function execute(Patient $patient): void;
    public function setNext(PatientHandler $handler): PatientHandler;
    public function do(Patient $patient): void;
}

// 抽象类复用公共定义
abstract class BaseHandler implements PatientHandler
{
    private ?PatientHandler $nextHandler = null;
    public function execute(Patient $patient): void
    {
        $this->do($patient);
        $this->nextHandler?->execute($patient);
    }

    public function setNext(PatientHandler $handler): PatientHandler
    {
        $this->nextHandler = $handler;
        return $handler;
    }

    abstract public function do(Patient $patient): void;
}

class Reception extends BaseHandler
{
    public function do(Patient $patient): void
    {
        if ($patient->status > PatientStatus::Start) {
            echo "患者已经挂号\n";
            return;
        }
        echo "挂号处为患者 {$patient->name} 挂号\n";
        $patient->status = PatientStatus::Reception;
    }
}

class Clinic extends BaseHandler
{
    public function do(Patient $patient): void
    {
        if ($patient->status === PatientStatus::Start) {
            echo "患者还未挂号\n";
            return;
        }

        if ($patient->status > PatientStatus::Reception) {
            echo "患者已经完成医生诊断\n";
            return;
        }
        echo "医生为患者 {$patient->name} 进行诊断\n";
        $patient->status = PatientStatus::Clinic;
    }
}

class Cashier extends BaseHandler
{
    public function do(Patient $patient): void
    {
        if ($patient->status < PatientStatus::Clinic) {
            echo "患者还未看诊\n";
            return;
        }

        if ($patient->status > PatientStatus::Clinic) {
            echo "患者已经完成缴费\n";
            return;
        }
        echo "收费处收取患者 {$patient->name} 的费用\n";
        $patient->status = PatientStatus::Cashier;
    }
}

class Pharmacy extends BaseHandler
{
    public function do(Patient $patient): void
    {
        if ($patient->status < PatientStatus::Cashier) {
            echo "患者还未缴费\n";
            return;
        }

        if ($patient->status > PatientStatus::Cashier) {
            echo "患者已经领取药物\n";
            return;
        }
        echo "药房为患者 {$patient->name} 配药\n";
        $patient->status = PatientStatus::Pharmacy;
    }
}

// 医院责任链处理器
class HospitalChainHandler extends BaseHandler
{
    public function do(Patient $patient): void
    {
        echo "患者 {$patient->name} 开始看病\n";
        $patient->status = PatientStatus::Start;
    }
}

$processes = [
    Reception::class,
    Clinic::class,
    Cashier::class,
    Pharmacy::class,
];
$handler = new HospitalChainHandler();
$current = $handler;
foreach ($processes as $process) {
    $current = $current->setNext(new $process);
}

$patient = new Patient('张三');
$handler->execute($patient);

运行上述代码,输出结果如下:

患者 张三 开始看病
挂号处为患者 张三 挂号
医生为患者 张三 进行诊断
收费处收取患者 张三 的费用
药房为患者 张三 配药

从上述代码可以看出,每个链路只需关注自己的执行逻辑,无需关心前面或后面的处理结果。通过接口和抽象类的规范定义及复用处理,实现了链路传递的效果。

Go 实现

接下来,我们看看如何使用 Go 语言实现责任链模式,同样以医院患者就诊流程为例。

定义就诊状态及患者结构
const (
    PatientStatusStart     = iota + 1 // 状态:开始看病
    PatientStatusReception            // 状态:已挂号
    PatientStatusClinic               // 状态:已看诊
    PatientStatusCashier              // 状态:已缴费
    PatientStatusPharmacy             // 状态:已拿药
)

type Patient struct {
    name   string // 姓名
    status int    // 状态
}
定义抽象类

在 Go 中无法通过继承来抽象链路执行逻辑,我们直接使用结构体来定义抽象类。

type PatientHandler interface {
    Execute(patient *Patient) error                // 处理链路传递
    SetNext(handler PatientHandler) PatientHandler // 设置下一个处理者
    Do(patient *Patient) error                     // 执行处理逻辑
}

// BaseHandler 充当抽象类的角色
// Do 方法无法抽象出来,故不实现
type BaseHandler struct {
    nextHandler PatientHandler // 下一个处理者
}

func (h *BaseHandler) Execute(patient *Patient) error {
    if patient == nil {
        return nil
    }
  
    // 因为 Do 方法未实现,这里无法调用自身的 Do
    // 实际调用链路时,需要为初始流程定义一个起始执行者

    if h.nextHandler != nil {
        if err := h.nextHandler.Do(patient); err != nil { // 调用下一个处理者的Do方法
            return err
        }
        return h.nextHandler.Execute(patient) // 调用下一个处理者的Execute方法
    }

    return nil
}

func (h *BaseHandler) SetNext(handler PatientHandler) PatientHandler {
    h.nextHandler = handler
    return handler
}
定义患者就诊流程

由于 Go 的理念是 “组合大于继承”,我们可以通过匿名组合的方式来编写患者的就诊流程定义。

type Reception struct {
    BaseHandler // 嵌入BaseHandler以实现链式调用
}

func (r *Reception) Do(patient *Patient) error {
    if patient.status > PatientStatusStart {
        return errors.New("患者已经挂号")
    }

    fmt.Printf("挂号处为患者 %s 挂号\n", patient.name)
    patient.status = PatientStatusReception // 更新患者状态为已挂号
    return nil
}

type Clinic struct {
    BaseHandler // 嵌入BaseHandler以实现链式调用
}

func (c *Clinic) Do(patient *Patient) error {
    if patient.status == PatientStatusStart {
        return errors.New("患者还未挂号")
    }

    if patient.status > PatientStatusReception {
        return errors.New("患者已经完成医生诊断")
    }

    fmt.Printf("医生为患者 %s 进行诊断\n", patient.name)
    patient.status = PatientStatusClinic // 更新患者状态为已看诊
    return nil
}

type Cashier struct {
    BaseHandler // 嵌入BaseHandler以实现链式调用
}

func (c *Cashier) Do(patient *Patient) error {
    if patient.status < PatientStatusClinic {
        return errors.New("患者还未看诊")
    }

    if patient.status > PatientStatusClinic {
        return errors.New("患者已经完成缴费")
    }

    fmt.Printf("收费处收取患者 %s 的费用\n", patient.name)
    patient.status = PatientStatusCashier // 更新患者状态为已缴费
    return nil
}

type Pharmacy struct {
    BaseHandler // 嵌入BaseHandler以实现链式调用
}

func (p *Pharmacy) Do(patient *Patient) error {
    if patient.status < PatientStatusCashier {
        return errors.New("患者还未缴费")
    }

    if patient.status > PatientStatusCashier {
        return errors.New("患者已经领取药物")
    }

    fmt.Printf("药房为患者 %s 配药\n", patient.name)
    patient.status = PatientStatusPharmacy // 更新患者状态为已拿药
    return nil
}
定义起始执行者
type HospitalChainHandler struct {
    BaseHandler // 嵌入BaseHandler以实现链式调用
}

// Do 该方法可无逻辑,如需执行逻辑则需要重写 Execute 来确保执行
func (h *HospitalChainHandler) Do(patient *Patient) error {
    fmt.Printf("患者 %s 开始看病\n", patient.name)
    patient.status = PatientStatusStart // 更新患者状态为开始看病

    return nil
}

// Execute 重写该方法以确保责任链的执行顺序
func (h *HospitalChainHandler) Execute(patient *Patient) error {
    if patient == nil {
        return nil
    }

    if err := h.Do(patient); err != nil { // 首先执行自己的Do方法
        return err
    }

    return h.BaseHandler.Execute(patient) // 然后继续责任链
}
编写测试用例
import "testing"

func TestChain(t *testing.T) {
    // 创建患者实例
    patient := &Patient{name: "张三"}

    // 创建看病处理者链
    hospital := &HospitalChainHandler{}

    // 设置病人看病的链路
    processes := []PatientHandler{
        &Reception{},
        &Clinic{},
        &Cashier{},
        &Pharmacy{},
    }
    var current PatientHandler = hospital
    for _, process := range processes {
        current = current.SetNext(process)
    }

    // 执行处理链
    if err := hospital.Execute(patient); err != nil {
        t.Errorf("处理患者失败: %v", err)
    }

    // 检查最终状态
    if patient.status != PatientStatusPharmacy {
        t.Errorf("患者状态不正确,期望 %d,实际 %d", PatientStatusPharmacy, patient.status)
    }
}

执行该测试用例,输出结果如下:

=== RUN   TestChain
患者 张三 开始看病
挂号处为患者 张三 挂号
医生为患者 张三 进行诊断
收费处收取患者 张三 的费用
药房为患者 张三 配药
--- PASS: TestChain (0.00s)
PASS

使用责任链模式的好处是,如果后续需要对流程进行改动,或者在中间穿插新的流程,只需要优雅地调整 processes 的定义即可。这样可以大大提高代码的可维护性和灵活性。