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年前 加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

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