7 个 Laravel 编程好习惯助你减少代码中 Bug

703 阅读3分钟

文章转发自专业的Laravel开发者社区,原始链接:learnku.com/laravel/t/3…

我们添加到项目的每一行代码,都会增加了它们的复杂性,并且增加了随时会产生bug的可能性。可能是在客户开会前几分钟,也可能是我们周末在电影院期间,不在我们的键盘前。

为了防止那些可怕的情况出现,让我们通过下面七个技巧,来编写更好的代码:

1. 为变量、函数、参数和方法指定描述性名称:

代码只写一次,但会被其他开发人员和您多次阅读和解释。因此,值得花一些额外的时间来命名这个新的类或方法,因此它的名称显示了它的真实意图或内容。 Let’s compare these two lines. Which one is easier to understand?

我们来比较一下这两行。哪一个更容易理解?

$evnt->add($req->q);

$event->addTickets($request->quantity);

第一行有输入错误,add 方法不清楚添加了什么,变量 $req 不够清楚,很难理解 q 是指数量。

另一方面,第二个例子即使对于非开发人员也很容易理解。

2. 使用类似PSR-2的PHP标准

永远不要低估以有序和一致的方式编写代码的重要性,因为这样可以让您更快地发现问题。

考虑以下两个例子:

public function addTickets($quantity)
{
    foreach (range(1, $quantity) as $i)
    {
            $code = Code::generate(); }
            $this->tickets()->create(
                [
                'code' => $code,
            ]);
    }

public function addTickets($quantity)
{
    foreach (range(1, $quantity) as $i) {
        $code = Code::generate();
    }

    $this->tickets()->create([
        'code' => $code,
    ]);
}

两个代码块都有相同的错误:当它们应该创建 N 时,两个代码块都只创建一个 ticket 。但是在哪个代码块中,您更快地发现了问题?现在想象一下处理格式错误的复杂代码的后果。

3. 減少臨時變量的數量

儘管我們學習演算法的第一章是如何宣告及使用臨時變量,他們仍然會讓代碼的閱讀及維護變得困難。

思考一下下方的例子:

$contact                           = array();
$contact['firstname']              = $user->first_name;
$contact['surname']                = $user->last_name;
$contact['id']                     = $user->id;
$contact_emails                    = array();
$contact_email                     = array();
$contact_email['email']            = $user->email;
$contact_emails[]                  = $contact_email;
$contact['emails']                 = $contact_emails;

$this->create('contact', $contact);

$contact = [
    'id' => $user->id,
    'firstname' => $user->first_name,
    'surname' => $user->last_name,
    'emails' => [
        [
            'email' => $user->email,
        ],
    ],
];

$this->create('contact', $contact);

哪個例子更容易理解?

順道一提,使用等號是一個壞習慣。這不僅是違反了 PSR-2,也會讓代碼變得難以維護。

所以,回到我們的第二個例子,這個例子可以藉由去除 code 變量,以行內的寫法來優化:

public function addTickets($quantity)
{
    foreach (range(1, $quantity) as $i) {
        $this->tickets()->create([
            'code' => Code::generate(6),
        ]);
    }
}

然而,在某些情況下,使用局部變量可以提高代碼的易讀性,例如:

function calculateCode($price, $quantity, $deliveryCost)
{
    $subtotal = $price * $quantity;

    if ($subtotal < 30) {
        $subtotal += $deliveryCost;
    }

    return $subtotal;
}

可能會比下面這個更清晰:

<?php

function calculateTotal($price, $quantity, $deliveryCost)
{
    if ($price * $quantity < 30) {
        return $price * $quantity + $deliveryCost;
    }

    return $price * $quantity;
}

4. 不要使用“魔法数字”

如果一份订单低于30元,那么将不包邮,并支付额外的运费, 这个时候我们应该使用常量来标识,如果订单价格低于30元,那么将支付运费。常量的配置使用如下:

    if ($subtotal < DELIVERY_COST_THRESHOLD) {
        $subtotal += $deliveryCost;
    }

翻译代码对照如下:

    if ( 订单价格 < 不包邮的价格) {
        $当前订单价格 += 运费价格;
    }

在这个方法中, 我们展示了使用常量的便捷, 同样我们也可以在其他项目需要使用的部分重复使用这个常量。

如果我们需要改变不包邮的规则, 我们仅仅只需要更新一行常量代码, 既减少重复,同时也减少了在代码中使用固定数字来判断的不确定性。

5. 分而治之

许多场景都可以将过长的代码分离成多个小方法,使得每一个方法都有不同的职责。例如:

新方法 getContactInfo 将返回带有用户联系信息的数组:

$this->create('contact', $user->getContactInfo());

面向对象编程要求我们将数据和函数集中在一个地方(类)。我们将在包含所有用户信息的(User 模型)中组装包含联系人信息的数组。

再看另一个例子

    $subtotal = $item->price * $quantity;
    $subtotal = $this->addDeliveryCost($subtotal);

方法 addDeliveryCost将返回一个交付成本的金额,但前提是该金额不超过设置的阀值,否则将返回原始金额。

现在让我们删除本地变量并内联代码:

return $this->addDeliveryCost($price * $quantity);

声明和使用许多小方法是减少代码中使用临时变量的好方法。

6.默认为简单解决方案

许多承诺你将编写出更好的代码的教程到最后都会使代码变的过于复杂。

如果你正在使用 Laravel 和 Eloquent,这些教程就会告诉你,将下面这段代码放在控制器是错误的:

// Somewhere in UserController.php

User::create([
    'name' => $request->name,
    'email' => $request->email,
    'password' => bcrypt($request->password),
]);

你应该这样写:

// Somewhere in UserController.php

$this->commandTransport->handleCommand(
    new UserCreationCommand(
        new UserNameField($request->name),
        new UserEmailField($request->email),
        new UserPasswordField(bcrypt($request->password)),
    )
);

在  UserCreationCommandHandler 类中,你同样不能创建 user,因为这违反了 SOLID 原则。你应该使用 repository:

class UserCreationCommandHandler
{
    //...

    public function handle(UserCreationCommand $command)
    {
        $this->userRepository->create(
           $command->name,
           $command->email,
           $command->password,
        );
    }
}

最终,在 UserEloquentRepository 类中, 你将最后调用 User::create:

class UserEloquentRepository implements UserRepository
{
    //...

    public function create(
        UserNameField $name,
        UserEmailField $email,
        UserPasswordField $password
    ) {
    return User::create([
        'name' => $name->getValue(),
        'email' => $email->getValue(),
        'password' => bcrypt($password->getValue()),
    ]);
}

}

过了一会,客户端要求你向 User 模型添加另一个字段。

哪个更简单?哪个方案更容易出 bug (很可能你就忘记将一个字段从一个方法传入另一个方法)。

同时,你是否注意到在例2中调用了两次  bcrypt ?所以第二个例子有 bug。

不幸的是,有些接口和类不会阻止你犯错。所以,需要仔细的去测试代码,说到测试代码:

7. 编写自动化测试

会计师采用一种叫做「复式记账法」的方式记账。这种方法要求他们把所有的交易都记录两次。编写单元测试需要我们编写两次代码,一次定义每一个测试:

function test_order_without_delivery_cost()
{
    $order = new Order;
    $order->addItem(new Item(['price' => 20]), 5);

    $expectedTotal = 20 * 5;
    $this->assertSame($expectedTotal, $order->getTotal());
}

function test_order_with_delivery_cost()
{
    $order = new Order;
    $order->addItem(new Item(['price' => 20]), 1);

    $expectedTotal = 20 + DELIVERY_COST;
    $this->assertSame($expectedTotal, $order->getTotal());
}

第二次编写代码的实现 (这个艰巨的任务就交给你了)。

许多开发人员抱怨这种做法是因为它迫使我们“加倍工作”,但是通过编写两次代码,我们减少了以同样的方式犯同样错误的可能性(如果我们犯两个不同的错误,测试可能会失败)。这就是为什么实现一些单元测试的项目往往会有很小的bug,然而需要好几个小时的调试时间。