PHP 代码简洁之道

一、

PHP 简洁代码之道 clean-code-php, 是基于 Clean Code: A Handbook of Agile Software Craftmanship(Clean Code: 敏捷软件开发工艺手册) 这本书做的指南,该书是 Bob Martin 叔叔写的关于如何编写可维护代码的经典书籍。

clean-code-php 指南的灵感来源于 Javascript 版本的 clean-code-javascript ,在其基础加上了 PHP 的特点。

以下是我最喜欢的 clean-code-php 仓库中的一些点:

不添加不必要的上下文

不好的:

<?php

class Car
{
    public $carMake;
    public $carModel;
    public $carColor;

    //...
}

好的:

<?php

class Car
{
    public $make;
    public $model;
    public $color;

    //...
}

函数参数(不要超过2个)

不好的:

<?php

function createMenu($title, $body, $buttonText, $cancellable) {
    // ...
}

好的:

<?php

class MenuConfig
{
    public $title;
    public $body;
    public $buttonText;
    public $cancellable = false;
}

$config = new MenuConfig();
$config->title = 'Foo';
$config->body = 'Bar';
$config->buttonText = 'Baz';
$config->cancellable = true;

function createMenu(MenuConfig $config) {
    // ...
}

一个函数只做一件事情

不好的:

<?php

function emailClients($clients) {
    foreach ($clients as $client) {
        $clientRecord = $db->find($client);
        if ($clientRecord->isActive()) {
            email($client);
        }
    }
}

好的:

function emailClients($clients) {
    $activeClients = activeClients($clients);
    array_walk($activeClients, 'email');
}

function activeClients($clients) {
    return array_filter($clients, 'isClientActive');
}

function isClientActive($client) {
    $clientRecord = $db->find($client);
    return $clientRecord->isActive();
}

观点

项目作者概述了本指南的目的如下:

这里的原则并不是所有的都必须遵守,而且几乎只有少部分是被广泛认同的。它们只是一些指导方针,而不是其他,但是它们都是 Clean Code的作者集多年编纂而成的。

在一门动态语言(像 PHP或其他任何语言)领域中,某些开发者可能会不同意其中的某些(或者很多)概念和观点,但是我需要指出的是即使你对其中一些观点不认同,也不要将它们全盘否定掉。

二、变量部分

使用有意义的并且可以读出来的变量名称

不好的:

$ymdstr = $moment->format('y-m-d');

好的:

$currentDate = $moment->format('y-m-d');

对于同一类型的变量使用相同的词汇

不好的:

getUserInfo();
getUserData();
getUserRecord();
getUserProfile();

好的:

getUser();

(译者注:都是要取用户信息,不好的案例中为同一件事起了多个名字,在编码中是要避免的)

使用易于查找的名称(第一部分)

我们读代码的时候要比写代码的时候多的多,所以我们写的代码易读易查找是很重要的。如果不命名好对理解我们的程序有意义的变量,我们会伤害到读我们代码的人。确保你的变量易于查找。

不好的:

// 448 是什么鬼?
$result = $serializer->serialize($data, 448);

好的:

$json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

使用易查找的名称(第二部分)

不好的:

// 4 是什么鬼?
if ($user->access & 4) {
    // ...
}

好的:

class User
{
    const ACCESS_READ = 1;
    const ACCESS_CREATE = 2;
    const ACCESS_UPDATE = 4;
    const ACCESS_DELETE = 8;
}

if ($user->access & User::ACCESS_UPDATE) {
    // do edit ...
}

使用可以解释的变量

不好的:

$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/';
preg_match($cityZipCodeRegex, $address, $matches);

saveCityZipCode($matches[1], $matches[2]);

好一点的:

这个好了一点,但是我们还是非常依赖正则

$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/';
preg_match($cityZipCodeRegex, $address, $matches);

list(, $city, $zipCode) = $matches;
saveCityZipCode($city, $zipCode);

好的:
通过使用命名子模式我们不必再依赖正则

$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,\\]+[,\\\s]+(?<city>.+?)\s*(?<zipCode>\d{5})?$/';
preg_match($cityZipCodeRegex, $address, $matches);

saveCityZipCode($matches['city'], $matches['zipCode']);

不要让读者猜

不要强迫你代码的读者去翻译变量的含义,显式比隐式要好

不好的:

$l = ['Austin', 'New York', 'San Francisco'];

for ($i = 0; $i < count($l); $i++) {
    $li = $l[$i];
    doStuff();
    doSomeOtherStuff();
    // ...
    // ...
    // ...
    // $li 变量代表什么???
    dispatch($li);
}

好的:

$locations = ['Austin', 'New York', 'San Francisco'];

foreach ($locations as $location) {
    doStuff();
    doSomeOtherStuff();
    // ...
    // ...
    // ...
    dispatch($location);
}

不增加不必要的语境

如果你的类名或者对象名已经告诉了你一些信息,就不要在方法和和属性上重复他们。

不好的:

class Car
{
    public $carMake;
    public $carModel;
    public $carColor;

    //...
}

好的:

class Car
{
    public $make;
    public $model;
    public $color;

    //...
}

使用默认参数

不太好的:
这样不太好,因为 $breweryName 可能被传入 NULL

function createMicrobrewery($breweryName = 'Hipster Brew Co.')
{
    // ...
}

好一点的:
这个比上一个版本好一点,因为可以保证 $breweryName 不为 Null

function createMicrobrewery($name = null)
{
    $breweryName = $name ?: 'Hipster Brew Co.';
    // ...
}

好的:

如果你只需要支持 PHP 7以上的版本, 你可以使用使用类型提示来保证 $breweryName 不为 NULL

function createMicrobrewery(string $breweryName = 'Hipster Brew Co.')
{
    // ...
}

三、函数部分(一)

1. 函数参数(不要超过两个)

限制函数的参数数量是非常重要的,因为它使你的函数更容易测试。超过三个参数会导致参数之间的组合过多,你必须对每个单独的参数测试大量不同的情况。

没有参数最理想的情况,一个或两个参数是可以接受的,三个以上是应该避免的。这是很重要的。通常,如果你有两个以上的参数,那么你的函数可能试图做的太多,如果不是,你可能需要将一个高级别的对象传当做参数传进去。

不好的

function createMenu($title, $body, $buttonText, $cancellable)
{
    // ...
}

好的:

class MenuConfig
{
    public $title;
    public $body;
    public $buttonText;
    public $cancellable = false;
}

$config = new MenuConfig();
$config->title = 'Foo';
$config->body = 'Bar';
$config->buttonText = 'Baz';
$config->cancellable = true;

function createMenu(MenuConfig $config)
{
    // ...
}

2. 一个函数只做一件事

这是软件工程中最重要的原则。
不好的:

function emailClients($clients)
{
    foreach ($clients as $client) {
        $clientRecord = $db->find($client);
        if ($clientRecord->isActive()) {
            email($client);
        }
    }
}

不好的

function emailClients($clients)
{
    $activeClients = activeClients($clients);
    array_walk($activeClients, 'email');
}

function activeClients($clients)
{
    return array_filter($clients, 'isClientActive');
}

function isClientActive($client)
{
    $clientRecord = $db->find($client);

    return $clientRecord->isActive();
}

3. 函数名要能说明它是做什么的

不好的:

class Email
{
    //...

    public function handle()
    {
        mail($this->to, $this->subject, $this->body);
    }
}

$message = new Email(...);
// 这是什么?一条消息的句柄? 你是要写一个文件么?(读者的疑问)
$message->handle();

好的

class Email 
{
    //...

    public function send()
    {
        mail($this->to, $this->subject, $this->body);
    }
}

$message = new Email(...);
//  一目了然
$message->send();

4. 函数应该只做一层抽象

当你有多个层次的抽象时,你的函数通常都在试图做的太多。拆分这些函数,可以让代码可重用性更高且更易测试。

不好的:

function parseBetterJSAlternative($code)
{
    $regexes = [
        // ...
    ];

    $statements = explode(' ', $code);
    $tokens = [];
    foreach ($regexes as $regex) {
        foreach ($statements as $statement) {
            // ...
        }
    }

    $ast = [];
    foreach ($tokens as $token) {
        // lex...
    }

    foreach ($ast as $node) {
        // parse...
    }
}

同样不好的:
我们以前从函数中迁出去了一些工作,但是 parseBetterJSAlternative() 函数还是很复杂,不可测试。

function tokenize($code)
{
    $regexes = [
        // ...
    ];

    $statements = explode(' ', $code);
    $tokens = [];
    foreach ($regexes as $regex) {
        foreach ($statements as $statement) {
            $tokens[] = /* ... */;
        }
    }

    return $tokens;
}

function lexer($tokens)
{
    $ast = [];
    foreach ($tokens as $token) {
        $ast[] = /* ... */;
    }

    return $ast;
}

function parseBetterJSAlternative($code)
{
    $tokens = tokenize($code);
    $ast = lexer($tokens);
    foreach ($ast as $node) {
        // parse...
    }
}

好的:
最好的解决方案是移除 parseBetterJSAlternative 函数的依赖

class Tokenizer
{
    public function tokenize($code)
    {
        $regexes = [
            // ...
        ];

        $statements = explode(' ', $code);
        $tokens = [];
        foreach ($regexes as $regex) {
            foreach ($statements as $statement) {
                $tokens[] = /* ... */;
            }
        }

        return $tokens;
    }
}

class Lexer
{
    public function lexify($tokens)
    {
        $ast = [];
        foreach ($tokens as $token) {
            $ast[] = /* ... */;
        }

        return $ast;
    }
}

class BetterJSAlternative
{
    private $tokenizer;
    private $lexer;

    public function __construct(Tokenizer $tokenizer, Lexer $lexer)
    {
        $this->tokenizer = $tokenizer;
        $this->lexer = $lexer;
    }

    public function parse($code)
    {
        $tokens = $this->tokenizer->tokenize($code);
        $ast = $this->lexer->lexify($tokens);
        foreach ($ast as $node) {
            // parse...
        }
    }
}

5.不要使用标志做函数参数

标志相当于告诉使用者,这个函数不止做一件事。函数应该只做一件事,如果函数根据布尔值执行不同代码路径,则你应该将函数拆分。

不好的:

function createFile($name, $temp = false)
{
    if ($temp) {
        touch('./temp/'.$name);
    } else {
        touch($name);
    }
}

好的:

function createFile($name)
{
    touch($name);
}

function createTempFile($name)
{
    touch('./temp/'.$name);
}

6.避免副作用

如果一个函数做了“拿到一个值并返回一个值或者多个值”以外的事情,那么这个函数就有可能产生副作用,副作用可能是意外的写入了文件、修改了全局变量、或者打钱给了陌生人。

现在假如你确实要在函数中做一些有可能产生副作用的事情。 比如要写一个文件,你需要做的是将写文件的操作集中到一处,而不是在几个函数或者类里对同一个文件做操作,实现一个服务(函数或者类)去操作它,有且仅有一个。

关键是要能避免常见的陷阱:像是在没有结构的对象之间共享状态、使用可能被写入任何值的可变数据类型、 不集中处理有可能产生副作用的操作。 如果你能做到这些,你会比绝大多数程序员更快乐。

不好的:

// Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
$name = 'Ryan McDermott';

function splitIntoFirstAndLastName()
{
    global $name;

    $name = explode(' ', $name);
}

splitIntoFirstAndLastName();

var_dump($name); // ['Ryan', 'McDermott'];

好的:

function splitIntoFirstAndLastName($name)
{
    return explode(' ', $name);
}

$name = 'Ryan McDermott';
$newName = splitIntoFirstAndLastName($name);

var_dump($name); // 'Ryan McDermott';
var_dump($newName); // ['Ryan', 'McDermott'];

7. 不要修改全局变量

在许多编程语言中污染全局是一种糟糕的做法,因为你的库可能会与另一个库冲突,但是你的库的用户却一无所知,直到在生产环境中爆发异常。让我们来考虑一个例子:如果你想要拿到配置数组怎么办?你可以编写全局函数,如config(),但是它可能与另一个试图做同样事情的库冲突。

不好的:

function config()
{
    return  [
        'foo' => 'bar',
    ]
}

好的

class Configuration
{
    private $configuration = [];

    public function __construct(array $configuration)
    {
        $this->configuration = $configuration;
    }

    public function get($key)
    {
        return isset($this->configuration[$key]) ? $this->configuration[$key] : null;
    }
}

加载配置并且创建 Configuration 类的实例

$configuration = new Configuration([
    'foo' => 'bar',
]);

现在你应该在你的应用程序中使用 Configuration类 的实例 。

四、函数部分(二)

8. 不要使用单例模式

(译者注:这一条有些难理解,看不懂就略过吧)

单例模式是一种反模式,Brian Button 的解释:

  1. 单例通常被用做一个全局的实例,为什么不好?因为你在代码中隐藏了依赖,而不是通过接口暴露他们。通过将一些东西放到全局来避免传递他们是一种“代码异味(code smell)”

code smell 是指能够被开发者察觉到的不好的形式

  1. 它违反了 SPR原则(single responsibility principle): 由它自己控制自己的创建和生命周期
  2. 它本身就导致了代码的紧耦合。大多数情况下这使得通过伪造数据来测试变的相当困难。

不好的

class DBConnection
{
    private static $instance;

    private function __construct($dsn)
    {
        // ...
    }

    public static function getInstance()
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    // ...
}

$singleton = DBConnection::getInstance();

好的:

class DBConnection
{
    public function __construct(array $dsn)
    {
        // ...
    }

     // ...
}

创建 DBConneciton 的实例,并配置 DSN

这样你就可以在你的应用程序中使用 DBConnection 的实例了。

9 封装条件判断

不好的:

if ($article->state === 'published') {
    // ...
}

好的:

if ($article->isPublished()) {
    // ...
}

避免否定类型的判断

不好的:

function isDOMNodeNotPresent($node)
{
    // ...
}

if (!isDOMNodeNotPresent($node))
{
    // ...
}

好的

function isDOMNodePresent($node)
{
    // ...
}

if (isDOMNodePresent($node)) {
    // ...
}

10. 避免条件判断

这似乎是一个不可能完成的任务。人们会问“如果不用 if 语句我该怎么做?”,答案是在许多情况下,你可以用多态来实现同样的效果。你可能还会问“这样有什么好处?”,答案是我们之前提到的原则:“一个函数应该只做一件事”, 当你的类或函数中有了 if 语句,相当于告诉别人你的函数做了一件以上的事情。

不好的:

class Airplane
{
    // ...

    public function getCruisingAltitude()
    {
        switch ($this->type) {
            case '777':
                return $this->getMaxAltitude() - $this->getPassengerCount();
            case 'Air Force One':
                return $this->getMaxAltitude();
            case 'Cessna':
                return $this->getMaxAltitude() - $this->getFuelExpenditure();
        }
    }
}


好的:

interface Airplane
{
    // ...

    public function getCruisingAltitude();
}

class Boeing777 implements Airplane
{
    // ...

    public function getCruisingAltitude()
    {
        return $this->getMaxAltitude() - $this->getPassengerCount();
    }
}

class AirForceOne implements Airplane
{
    // ...

    public function getCruisingAltitude()
    {
        return $this->getMaxAltitude();
    }
}

class Cessna implements Airplane
{
    // ...

    public function getCruisingAltitude()
    {
        return $this->getMaxAltitude() - $this->getFuelExpenditure();
    }
}

11. 避免类型检查(第一部分)

PHP是弱类型语言,意味着你的函数可以接收任何类型的参数。有时你会因为这点自由而受害,这时你可能会在函数中检查参数类型,有许多途径可以避免在函数能做类型检查,首先要做的是有一致的接口。

不好的

function travelToTexas($vehicle)
{
    if ($vehicle instanceof Bicycle) {
        $vehicle->peddleTo(new Location('texas'));
    } elseif ($vehicle instanceof Car) {
        $vehicle->driveTo(new Location('texas'));
    }
}

好的:

function travelToTexas(Traveler $vehicle)
{
    $vehicle->travelTo(new Location('texas'));
}

避免类型检查(第二部分)

如果现在要处理的是基础数据类型,像字符串,整型和数组。现在如果你用的 PHP7以上的版本,而且现在很明显你也不能用多态。现在你应该考虑使用类型检查或者严格模式,它使你能够在php语法层面提供静态类型检查。手动判断类型的问题是,你需要添加许多额外的代码,你获得的人造的“类型安全”抵不过你代码可读性的损失。

不好的

function combine($val1, $val2)
{
    if (!is_numeric($val1) || !is_numeric($val2)) {
        throw new \Exception('Must be of type Number');
    }

    return $val1 + $val2;
}

好的

function combine(int $val1, int $val2)
{
    return $val1 + $val2;
}

12.移除废弃的代码

废弃的代码跟重复的代码一样不好,没有理由在你的代码库继续保留他们,如果你想找回他们,到版本历史中找回就行了。

不好的

function oldRequestModule($url)
{
    // ...
}

function newRequestModule($url)
{
    // ...
}

$request = newRequestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');

好的

function requestModule($url)
{
    // ...
}

$request = requestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');

五、对象部分

1.使用 setter 和 getter

在 PHP 中,你可以为方法设置 public, protectedprivate 关键字。使用这些关键字你可以控制一个对象的属性修改权限。

  • 如果除了获取对象属性你还想做一些别的事,就不用再到代码库中去寻找并修改每一个修改对象属性的地方(属性入口归一)。
  • 方便数据验证
  • 封装内部实现
  • 获取和设置时方便添加日志和错误处理
  • 继承了类,你可以重写默认的函数
  • 我们可以延迟加载类的属性,假设它是从服务器获取的

另外,这是开放/封闭原则的一部分,是面向对象的基本设计原则。

不好的

class BankAccount
{
    public $balance = 1000;
}

$bankAccount = new BankAccount();

// Buy shoes...
$bankAccount->balance -= 100;

好的

class BankAccount
{
    private $balance;

    public function __construct($balance = 1000)
    {
      $this->balance = $balance;
    }

    public function withdrawBalance($amount)
    {
        if ($amount > $this->balance) {
            throw new \Exception('Amount greater than available balance.');
        }

        $this->balance -= $amount;
    }

    public function depositBalance($amount)
    {
        $this->balance += $amount;
    }

    public function getBalance()
    {
        return $this->balance;
    }
}

$bankAccount = new BankAccount();

// Buy shoes...
$bankAccount->withdrawBalance($shoesPrice);

// Get balance
$balance = $bankAccount->getBalance();

2.让对象有 私有(private)/受保护的(protected) 的成员

不好的

class Employee
{
    public $name;

    public function __construct($name)
    {
        $this->name = $name;
    }
}

$employee = new Employee('John Doe');
echo 'Employee name: '.$employee->name; // Employee name: John Doe

好的

class Employee
{
    private $name;

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }
}

$employee = new Employee('John Doe');
echo 'Employee name: '.$employee->getName(); // Employee name: John Doe

六、类部分

使用组合而不是继承

Gang of Four”在设计模式里所声明的,你应该优先选择“组合模式”而不是“继承”

译者注:Gang of Four 译成 四人帮,指代 DesignPatternBook 的四位作者

不论是使用“组合模式”还是使用“继承”都有许多理由。
这个话题的要点是当你本能的要使用继承时就想一想是否“组合模式”能帮你更好的解决问题。

你可能会问,“我什么时候应该用继承?”, 这取决于你手头的问题。这里有一个列表说明什么时候使用继承会更合适:

  1. 你的继承表达了一个“is-a(是)” 的关系,不是“has-a(有)”的关系(对比人类 -> 动物 和 用户-> 用户详情 )
  2. 你能从基础类中复用代码(人类能够像所有动物一样移动)
  3. 你想通过修改全局类来对所有派生类进行修改。(例如改变所有动物移动消耗的热量)

不好的:

class Employee 
{
    private $name;
    private $email;

    public function __construct($name, $email)
    {
        $this->name = $name;
        $this->email = $email;
    }

    // ...
}

// 不好,因为是雇员有税收数据
// 税收数据不是一种雇员,所以使用继承不适合

class EmployeeTaxData extends Employee 
{
    private $ssn;
    private $salary;

    public function __construct($name, $email, $ssn, $salary)
    {
        parent::__construct($name, $email);

        $this->ssn = $ssn;
        $this->salary = $salary;
    }

    // ...
}

好的:

class EmployeeTaxData 
{
    private $ssn;
    private $salary;

    public function __construct($ssn, $salary)
    {
        $this->ssn = $ssn;
        $this->salary = $salary;
    }

    // ...
}

class Employee 
{
    private $name;
    private $email;
    private $taxData;

    public function __construct($name, $email)
    {
        $this->name = $name;
        $this->email = $email;
    }

    public function setTaxData($ssn, $salary)
    {
        $this->taxData = new EmployeeTaxData($ssn, $salary);
    }

    // ...
}

避免使用流式接口(fluent interfaces)

fluent interfaces 是一种面向对象的接口,通过Method chaining(链式操作)来提高代码的可读性。

然而有一些Context,通常是构建对象,这个设计模式虽然减少了代码的啰嗦(例如:PHPUnit Mock Builder 或者 Doctrine Query Builder),大部分时候他会产生如下这些消耗:

不好的:

class Car
{
    private $make = 'Honda';
    private $model = 'Accord';
    private $color = 'white';

    public function setMake(string $make): self
    {
        $this->make = $make;

        // NOTE: Returning this for chaining
        return $this;
    }

    public function setModel(string $model): self
    {
        $this->model = $model;

        // NOTE: Returning this for chaining
        return $this;
    }

    public function setColor(string $color): self
    {
        $this->color = $color;

        // NOTE: Returning this for chaining
        return $this;
    }

    public function dump(): void
    {
        var_dump($this->make, $this->model, $this->color);
    }
}

$car = (new Car())
  ->setColor('pink')
  ->setMake('Ford')
  ->setModel('F-150')
  ->dump();

好的:

class Car
{
    private $make = 'Honda';
    private $model = 'Accord';
    private $color = 'white';

    public function setMake(string $make): void
    {
        $this->make = $make;
    }

    public function setModel(string $model): void
    {
        $this->model = $model;
    }

    public function setColor(string $color): void
    {
        $this->color = $color;
    }

    public function dump(): void
    {
        var_dump($this->make, $this->model, $this->color);
    }
}

$car = new Car();
$car->setColor('pink');
$car->setMake('Ford');
$car->setModel('F-150');
$car->dump();

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章