在日常工作中,我们常常会遇到产品增加需求的情况,或者需要设计一个工作流引擎。如果采用硬编码的方式,不仅不利于代码的解耦,后期的维护工作也会让人头疼不已。这时,责任链(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 的定义即可。这样可以大大提高代码的可维护性和灵活性。