Whip Monstrous Code Into Shape - 10 Consider Normalizing

You'll frequently find yourself in the position of needing to normalize a particular value. Sure, the value might be in the proper format from the start; but it could also be in the form of a function that you need to trigger. Or maybe, based upon various business rules, you need to first modify the value before continuing on. Well, if you're not careful, your code can get pretty messy, pretty fast.

One option you might consider is extracting all of the normalizing-specific code into either its own method, or a different class entirely. In this episode, we'll cycle through a number of refactors that you might consider.


很多时候我们在处理业务逻辑的时候,面临一个比较常见的问题是你不得不规范化(normalize)输入的数据。比如用户提交的值,你需要去验证是否合法;比如这个数据需要满足不同的要求(单个值,数组,闭包 etc...),在使用之前你要把他们转换成统一的格式以便后续处理;再比如有些数据你需要修改后才能使用。在这个过程中,如果你没有特别注意的话,很快逻辑代码就会变得混乱。碰到这种情况,你可以考虑把这些 normalize 数据的逻辑放到自己单独的方法或者类中。这个视频示范了一个渐进的重构过程。

想象一个场景,比如之前 laracast 黑五推出的订阅优惠码,在不考虑任何数据规范化的情况下,我们的控制器逻辑是这样:

class SubscriptionsController
{
    public function store(Request $request)
    {
        $code = $request->code;
        $plan = $request->plan;

        $this->user
            ->subscription()
            ->usingCoupon($code)
            ->swap($plan);
    }
}

这个时候我们需要开始考虑一些情况了:客户提交的优惠码是否合法?优惠码和订阅计划是不是相符(比如 monthly 的优惠码就不能用在 yearly 订阅)? 于是我们在控制器的代码中加上了这些判断语句,这其实就是数据的规范化(normalize)。

// normalize
if (! $coupon = Coupon::havingCode($code)) {
    $code = null;
} else {
    if (! $coupon->workWithPlan($plan)) {
        $code = null;
    }
}

$this->user
    ...

在上面的代码中,我们先判定了优惠码是否合法,另外也判断了优惠码和订阅计划是不是相符。这个时候我们再考虑另外一个情况,我们上面的优惠码是字符串,假如是个闭包的话,我们还得在 store 里面加上闭包的判断。

if (is_callable($code)) {
    return call_user_func($code);
}

随着逻辑的增加, store 方法代码会变得越来越多,这个时候可以考虑把数据的规范化(normalize)重构到一个新的方法里面。

class SubscriptionsController
{
    public function store(Request $request)
    {
        $code = $request->code;
        $plan = $request->plan;

        $this->user
            ->subscription()
            ->usingCoupon($this->normalizeCoupon($code, $plan))
            ->swap($plan);
    }

    protected function normalizeCoupon($code, $plan)
    {
        if (is_callable($code)) {
            $code = call_user_func($code);
        }

        $coupon = Coupon::havingCode($code);

        if (!$coupon || !$coupon->workWithPlan($plan)) {
            return false;
        }

        return $code;
    }
}

这个时候已经把数据的规范化(normalize)重构到一个新的方法里面,这个时候我们再考虑一下"responsibility(职责)":这个方法是 SubscriptionsController 这个控制器的职责吗?responsibility 是 jeff 在重构代码时讲的比较多的一个概念,区分责任,把责任归属到正确的类中,如果责任混乱的话,毫无疑问会导致耦合性高,且无法重用。这种情形下,我们不妨考虑把这部分职责交给 Coupon 模型。

public function store(Request $request)
{
    $code = $request->code;
    $plan = $request->plan;

    $this->user
        ->subscription()
        ->usingCoupon(Coupon::validateForPlan($code, $plan))    // Coupon 模型添加validateForPlan静态方法
        ->swap($plan);
}

Coupon 模型:

class Coupon extends Model
{
    public function workWithPlan($plan)
    {
        // ...
    }

    public static function scopeHavingCode($query, $code)
    {
        return $query->where('code', $code)->first();
    }

    public static function validateForPlan($code, $plan)
    {
        $coupon = static::havingCode($code);

        if (!$coupon || !$coupon->workWithPlan($plan)) {
            return false;
        }

        return $coupon->code;
    }
}

当然每个人碰到的情况都不一样,也许你觉得这个不应该是模型的职责,应该是 FormRequest 的职责,你也可以放到 FormRequest 里面,不必纠结于哪种才是最好的实践,选择一个最适合你自己的方式。

更进一步的,或许我们可以把验证优惠码是否合法和验证优惠码和订阅计划是不是相符这两个步骤区分开来:

$this->user
    ->subscription()
    ->usingCoupon(Coupon::normalize($code)->against($plan))
    ->swap($plan);

接下来重构 Coupon 模型:

class Coupon extends Model
{
    public static function normalize($code) {
        return static::where('code', $code)->first();
    }

    public function against($plan)
    {
        // ...
    }
}

到这里已经完成了重构,但是不要忘了里面有个巨大的坑,Coupon::normalize($code)->against($plan),如果 $code 合法,返回一个 Coupon 实例没有问题,如果 $code 不合法返回一个 null 的话,在 null 上调用 against 肯定会出错,所以,还需要重构一下 normalize 方法:

public static function normalize($code) {
    $coupon = static::where('code', $code)->first();
    return $coupon ?: new static();
}
本帖已被设为精华帖!
本帖由 Summer 于 7年前 加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!