Menu
CoddyTech

状态模式

本课属于 Coddy PHP 学习旅程的“面向对象编程”单元。第 79 课,共 91 课。

状态模式是一种行为型设计模式,它允许对象在内部状态发生变化时改变行为。你无需使用复杂的条件语句来处理不同状态,而是将每种状态封装在自己的类中,并将行为委托给当前状态对象。

想一想自动售货机。它会根据当前状态采取不同的行为:等待投币、收到付款,或者正在出货。

每种状态都有自己的规则,用来规定允许执行哪些操作。状态模式通过为每种状态创建一个类来实现这一点。

<?php
interface OrderState {
    public function proceed(Order $order): string;
    public function getStatus(): string;
}

class PendingState implements OrderState {
    public function proceed(Order $order): string {
        $order->setState(new ProcessingState());
        return "Order moved to processing";
    }
    
    public function getStatus(): string {
        return "Pending";
    }
}

class ProcessingState implements OrderState {
    public function proceed(Order $order): string {
        $order->setState(new ShippedState());
        return "Order has been shipped";
    }
    
    public function getStatus(): string {
        return "Processing";
    }
}

class ShippedState implements OrderState {
    public function proceed(Order $order): string {
        return "Order already shipped";
    }
    
    public function getStatus(): string {
        return "Shipped";
    }
}

上下文类持有对当前状态的引用,并将调用委托给它:

<?php
class Order {
    private OrderState $state;
    
    public function __construct() {
        $this->state = new PendingState();
    }
    
    public function setState(OrderState $state): void {
        $this->state = $state;
    }
    
    public function proceed(): string {
        return $this->state->proceed($this);
    }
    
    public function getStatus(): string {
        return $this->state->getStatus();
    }
}

$order = new Order();
echo $order->getStatus() . "\n";
echo $order->proceed() . "\n";
echo $order->getStatus() . "\n";
echo $order->proceed() . "\n";
echo $order->getStatus();

输出:

Pending
Order moved to processing
Processing
Order has been shipped
Shipped

状态模式可以消除冗长的 switch 语句,让添加新状态变得简单。每个状态类都负责知道下一个状态是什么,从而让逻辑更有条理,也更易于维护。

challenge icon

挑战

简单

让我们使用 State Pattern 构建一个文档工作流系统。内容管理系统中的文档会经历不同阶段:草稿、审核和已发布。每个阶段都有自己的规则,规定允许执行哪些操作,以及尝试推进文档时会发生什么。

你将把代码组织在四个文件中:

  • DocumentState.php:定义一个 DocumentState 接口,为所有文档状态规定契约。它应包含两个方法:publish(Document $document): string 用于尝试推进文档,getStatus(): string 用于返回当前状态名称。
  • States.php:引入 DocumentState 接口,并创建三个实现该接口的状态类:
    • DraftState:调用 publish() 时,将文档切换到 ReviewState,并返回 "Document sent for review"。它的状态是 "Draft"。
    • ReviewState:调用 publish() 时,将文档切换到 PublishedState,并返回 "Document published"。它的状态是 "Under Review"。
    • PublishedState:调用 publish() 时,返回 "Document is already published",但不改变状态。它的状态是 "Published"。
  • Document.php:引入 States 文件,并创建一个作为上下文的 Document 类。你的文档应当:
    • 在构造函数中接收标题,并以 DraftState 作为初始状态
    • 包含一个用于更改当前状态的 setState(DocumentState $state): void 方法
    • 包含一个 publish(): string 方法,将调用委托给当前状态的 publish() 方法
    • 包含一个 getStatus(): string 方法,返回当前状态的状态信息
  • main.php:引入 Document 文件。你会收到一个输入:在文档上调用 publish() 的次数。

    创建一个标题为 "My Article" 的 Document。首先,打印初始状态。然后,按指定次数调用 publish(),并将每次调用的结果打印在新的一行。最后,打印最终状态。

    输出格式:

    Status: [initial status]
    [publish result 1]
    [publish result 2]
    ...
    Status: [final status]

每个状态类都知道工作流中的下一个状态,因此转换逻辑清晰有序,也便于修改。添加一个像 "Archived" 这样的新状态,只需创建一个新类并更新相关转换即可。

亲手试试

<?php

require_once 'Document.php';

// 读取输入,即调用 publish() 的次数
$n = intval(trim(fgets(STDIN)));

// TODO:创建一个标题为 "My Article" 的 Document 对象

// TODO:按 "Status: [status]" 格式打印初始状态

// TODO:调用 publish() n 次,每次的结果打印在新的一行

// TODO:按 "Status: [status]" 格式打印最终状态

?>
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: PHP 在线编译器