在 Laravel 的数据库模型中使用状态模式

 在 Laravel 的数据库模型中使用状态模式

在讲怎么在 Laravel 模型中使用状态模式之前先让我们来熟悉一下状态模式。

 

状态模式

 

定义

允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。

 

解决什么问题

在实际的开发中我们经常会遇到一个表会存在不同的状态,比如常见的订单表一般会有预定支付已出货已取消等。注:由于我们使用的 ORM 工具会把数据库中表的每一行映射成一个对象实例,为了更好的表述模式,我会把表中的每一行称为对象。比如我会把某条订单记录称为订单对象,使得我们用面向对象的思维去思考业务。

我们常见的状态管理会向下面的代码这样,在 Order 类里面有 3 个行为分别是 pay(),shipping(),cancel()。在执行每个行为方法之前我们都会去验证当前对象的状态是否满足执行条件。
这样做使得跟状态相关的验证会分散在不同的地方,甚至会把这些验证逻辑泄露到控制器和服务层,在对象状态复杂且需求多变的情况下,后期的维护成本很高且容易出错。为了体现开放封闭原则和单一原则,则可以使用状态模式来管理对象的不同状态。

class Order
{
    private $stateCode;
    const RESERVED_STATE = 1; // 已预定
    const PAID_STATE = 2; // 已支付
    const SHIPPED_STATE = 3; // 已发货
    const CANCELED_STATE = 4; // 已取消

    // 支付
    public function paid()
    {
        if ($this.stateCode != self::RESERVED_STATE) {
            throw new Exception('只有预定后的订单才能付款');
        }
        $this.stateCode = self::PAID_STATE;
    }

    // 发货
    public function shipping()
    {
        if ($this->stateCode != self::PAID_STATE) {
            throw new Exception('只有支付后的订单才能发货');
        }
        $this.stateCode = self::SHIPPED_STATE;
    }

    // 取消订单
    public function cancel()
    {
        if ($this->stateCode != self::RESERVED_STATE) {
            throw new Exception('支付后的订单不能取消');
        }
        $this.stateCode = self::CANCELED_STATE;
    }

}
状态模式 UML
 
订单状态的接口
interface OrderState
{
    function paid(); // 支付
    function shipping(); // 发货
    function cancel();  // 取消订单
}
已预定订单状态的实现

把跟每个状态相关的逻辑都封装在了每个状态对象里面,状态的行为需要调用 order 对象的方法来改变 order 的状态,所以在需要改变 order 状态的行为里面需要持有一个 order 对象的引用。

class ReservedOrderState implements OrderState
{
    const STATE_CODE = 1;

    public function paid(Order $order)
    {
        $order->setPaid();
    }

    public function shipping()
    {
        throw new Exception('预定状态不能发货');
    }

    public function cancel(Order $order)
    {
        $order->setCanceled();
    }
}
已支付订单状态的实现
class PaidOrderState implements OrderState
{
    const STATE_CODE = 2;

    public function paid()
    {
        throw new Exception('已经支付');
    }

    public function shipping(Order $order)
    {
        $order->setShipped();
    }

    public function cancel()
    {
        throw new Exception('支付后不能取消订单');
    }
}
已发货订单状态的实现
class ShippedOrderState implements OrderState
{
    const STATE_CODE = 3;

    public function paid()
    {
        throw new Exception('已经支付');
    }

    public function shipping()
    {
        throw new Exception('已经发货');
    }

    public function cancel()
    {
        throw new Exception('发货后不能取消订单');
    }
}
已取消订单状态的实现
class CanceledOrderState implements OrderState
{
    const STATE_CODE = 4;

    public function paid()
    {
        throw new Exception('订单取消后不能支付');
    }

    public function shipping()
    {
        throw new Exception('订单取消后不能出货');
    }

    public function cancel()
    {
        throw new Exception('已经取消');
    }
}
订单类

这样一来把每个状态相关的逻辑都封装起来,很清晰的就能看出每个状态可以执行哪些行为哪些不能执行。每当要添加一个新的状态的时候只需要添加一个 OrderState 接口的一个实现就可以了。状态与状态之间互不依赖,也消除了之前对象行为的状态判断语句。

class Order
{
    // 订单状态
    private $orderState;

    // 支付
    public function paid()
    {
        $this->orderState->paid($this);
    }

    // 发货
    public function shipping()
    {
        $this->orderState->shipping($this);
    }

    // 取消订单
    public function cancel()
    {
        $this->orderState->cancel($this);
    }

    // 把订单状态设置成已预定状态
    public function setReserved()
    {
        $this->orderState = new ReservedOrderState();
    }

    // 把订单状态设置成已支付状态
    public function setPaid()
    {
        $this->orderState = new PaidOrderState();
    }

    // 把订单状态设置成已发货状态
    public function setShipped()
    {
        $this->orderState = new ShippedOrderState();
    }

    // 把订单状态设置成已取消状态
    public function setCanceled()
    {
        $this->orderState = new CanceledOrderState();
    }

}
 

解决 Laravel 对象模型跟数据库的映射问题

我在尝试在 Laravel 的模型上使用状态模式时遇到了一个问题,就是模型在数据库中查询到数据后怎么把字段 state(通常叫 state,也可以是其他表示状态的枚举字段)的值映射成我们对象上的某个状态对象。比如我们 order 表中的某一行的 state 字段是 2,那么映射到 order 对象上应该是 order 对象有一个 PaidOrderState 的对象。通过查询文档和查看源代码找到 Eloquent 在查询到数据后会触发每个模型的 retrieved 的事件。通过监听监听这个事件,我们可以在获取到数据后编写代码自动把 state 状态值和具体的状态对象进行关系映射。

在 Order 模型上添加如下事件监听代码:

public static function boot() {
    parent::boot();
    static::retrieved(function($model) {
       switch($model->state) {
            case 1: // 已预定
                $model->orderState = new ReservedOrderState();
                break; 
            case 2: // 已支付
                $model->orderState = new PaidOrderState();
                break; 
            case 3: // 已经发货
                $model->orderState = new ShippedOrderState();
                break; 
            case 4: // 已经取消
                $model->orderState = new CanceledOrderState();
                break; 
        }
    });
}

这样一来就解决了从数据表到模型对象的映射问题。

目前为止还存在一个问题就是数据回写的问题,当我们从对象的某个状态迁移到另外一个状态的后再通过对象的 save () 方法保存到数据库的时候,其实这个时候 state 的字段值并没有改变。解决办法就是在对象修改状态的时候去修改 state 值。

像这样:

public function setPaid()
{
    $this->orderState = new PaidOrderState();
    $this->stateCode = PaidOrderState::STATE_CODE;
}

只需要添 $this->stateCode = PaidOrderState::STATE_CODE; 来修改 state 的值就可以了。

由于 Eloquent 是基于活动记录(Activity record)的 ORM,所以很难使用继承结构来使用更多的设计模式。但是 Eloquent 提供了 2 个非常有用的模型事件分别是 retrieved 和 saving。retrieved 事件我们可以在模型查询到数据的时候对模型对象做一些更改,比如上面用它解决了映射问题。saving 是在模型保存的时候触发的事件,可以用它在保存到数据库的时候再对模型做一些更改,比如我们在解决 state 字段回写到数据库的问题也可以使用 saving 事件来解决。这样一来通过这 2 个事件,我们可以有更多的想象空间来运用更多的设计模式来解决复杂的业务问题。

 
原文地址:https://www.cnblogs.com/mouseleo/p/14335016.html