函数

未匹配的标注

使用默认参数替代短路或条件

Not good:

这样做并不好,因为 $breweryName 可以是 NULL

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

Not bad:

这个观点比之前的版本更容易理解,可以更好的控制了变量的值。

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

Good:

您可以使用 类型提示 并确保 $breweryName 不会是 NULL

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

函数参数(理想情况下为 2 个或更少)

限制函数参数的数量非常重要,可以轻松地测试您的功能。
一旦一个方法拥有三个以上的参数会导致组合爆炸,您必须使用每个单独的参数测试大量不同的情况。

零参数是理想的情况。 一两个参数是可以的,三个应该避免。
除此之外的任何东西都应该合并。
通常,如果您有两个以上参数这说明你的函数做的事情太多了。如果不是,大多数情况下一个更高级别的对象就足以作为一个参数。

Bad:

class Questionnaire
{
    public function __construct(
        string $firstname,
        string $lastname,
        string $patronymic,
        string $region,
        string $district,
        string $city,
        string $phone,
        string $email
    ) {
        // ...
    }
}

Good:

class Name
{
    private $firstname;

    private $lastname;

    private $patronymic;

    public function __construct(string $firstname, string $lastname, string $patronymic)
    {
        $this->firstname = $firstname;
        $this->lastname = $lastname;
        $this->patronymic = $patronymic;
    }

    // getters ...
}

class City
{
    private $region;

    private $district;

    private $city;

    public function __construct(string $region, string $district, string $city)
    {
        $this->region = $region;
        $this->district = $district;
        $this->city = $city;
    }

    // getters ...
}

class Contact
{
    private $phone;

    private $email;

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

    // getters ...
}

class Questionnaire
{
    public function __construct(Name $name, City $city, Contact $contact)
    {
        // ...
    }
}

函数名字应该语义化

Bad:

class Email
{
    //...

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

$message = new Email(...);
// 这是什么? 消息的句柄? 我们现在正在写入文件吗?
$message->handle();

Good:

class Email
{
    //...

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

$message = new Email(...);
// 清晰明了
$message->send();

函数应该只是一个抽象级别

当您有多个抽象级别时,通常是因为您的函数做太多事情了。
拆分功能可以让代码可重用并且容易测试。

Bad:

function parseBetterPHPAlternative(string $code): void
{
    $regexes = [
        // ...
    ];

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

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

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

Bad too:

我们已经实现了一些功能,但是 parseBetterPHPAlternative() 函数仍然非常复杂,无法测试。

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

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

    return $tokens;
}

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

    return $ast;
}

function parseBetterPHPAlternative(string $code): void
{
    $tokens = tokenize($code);
    $ast = lexer($tokens);
    foreach ($ast as $node) {
        // parse...
    }
}

Good:

最好的解决方案是移出 parseBetterPHPAlternative() 函数的依赖关系。

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

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

        return $tokens;
    }
}

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

        return $ast;
    }
}

class BetterPHPAlternative
{
    private $tokenizer;
    private $lexer;

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

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

不要使用标志作为函数参数

标志会告诉您的用户,这个函数可以做不止一件事情。函数应该做一件事情。 如果函数遵循不同的代码路径,请基于布尔值将它们拆分。

坏的:

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

好的:

function createFile(string $name): void
{
    touch($name);
}

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

避免副作用

如果函数取一个值并返回另外一个值或多个值之外,就会产生副作用。副作用可能是写入一个文件,修改一些全局变量,或者不小心将您的所有钱汇给一个陌生人。

现在,你确实需要在程序中偶尔出现副作用。就像之前的例子一样,你可能需要写入文件。你想要做的是集中执行此操作的位置。没有几个可以写入特定内容的函数和类文件。只有一个服务可以做到这一点。 也只有一个。

主要的一点是要避免常见的陷阱,比如在对象之间共享状态而不使用任何结构,使用任何可以被写入的数据类型,而不是集中在副作用发生的地方。如果你能做到这点,你将会更快乐,比绝大多数程序员都要好。

坏的:

// 通过以下函数引用的全局变量。
// 如果我们有另外一个使用这个名字函数,现在它将是一个数组,它可能会破坏它。
$name = 'Ryan McDermott';

function splitIntoFirstAndLastName(): void
{
    global $name;

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

splitIntoFirstAndLastName();

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

好的:

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

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

var_dump($name);
// 'Ryan McDermott';

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

不要写入全局函数

在许多语言中,污染全局函数是一个不好的做法,因为你可能会与另外一个库发生冲突,并且使用你 API 的用户会一无所知,直到他在生产环境中得到异常。让我们想一个例子:如果你想要配置数组怎么办?
你可以编写全局函数,比如 config(),但它可能会与另外一个尝试做相同操作的库发生冲突。

坏的:

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

好的:

class Configuration
{
    private $configuration = [];

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

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

加载配置和创建 Configuration 类的实例

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

现在,你必须在应用程序中使用 Configuration 的实例。

不要使用单例模式

单例是一种 反模式。摘自布莱恩·巴顿:

  1. 它通常被用作 全局实例,为什么会这么糟糕呢?因为在您的代码中 隐藏了应用程序的依赖,而不是通过接口暴露它们。使之全局以避免传递是一种 代码味道
  2. 它们违反了 单一责任原则:由于 它们控制自己的创建和生命周期
  3. 它们内在地导致代码紧密的 耦合。在许多情况下,这使得 在测试中伪装它们 变得相当困难。
  4. 在应用程序的生命周期中,它们始终保持着状态。测试的另一个亮点是 您可能会遇到对测试需要排序的情况,这对于单元测试来说是一个很大的禁忌。为什么?因为每个单元测试应该相互独立。

Misko Hevery 也有关于 root of problem 非常好的想法。

坏的:

class DBConnection
{
    private static $instance;

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

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

        return self::$instance;
    }

    // ...
}

$singleton = DBConnection::getInstance();

好的:

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

    // ...
}

创建 DBConnection 类的实例并且用 DSN 对其配置。

$connection = new DBConnection($dsn);

现在你必须在应用程序中使用 DBConnection 的实例。

封装条件

坏的:

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

好的:

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

避免否定条件

坏的:

function isDOMNodeNotPresent(DOMNode $node): bool
{
    // ...
}

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

好的:

function isDOMNodePresent(DOMNode $node): bool
{
    // ...
}

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

避免有条件

这似乎是一个不可能的任务。在第一次听到这个时,大多数人都说,“如果没有 if 语句我该怎么做呢?” 答案是在许多情况下,你可以使用多态来实现相同的任务。第二个问题通常是,“那太好了,但为什么要这样做?” 这答案是我们之前学到的代码整洁概念:一个函数只做一件事。当你的类和函数具有 if 语句时,你在告诉你的用户这个函数不止做一件事情。但是要记着,只做一件事。

坏的:

class Airplane
{
    // ...

    public function getCruisingAltitude(): int
    {
        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(): int;
}

class Boeing777 implements Airplane
{
    // ...

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

class AirForceOne implements Airplane
{
    // ...

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

class Cessna implements Airplane
{
    // ...

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

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

PHP 是无类型的,这意味着您的函数可以采用任何类型的参数。
但我们有时不得不在函数中进行类型检查,这时候我们反而会被这种自由所困扰。有很多方法可以避免这样做。首先要考虑的是一致的 API。

Bad:

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

Good:

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

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

如果您正在使用基本的原始值,如字符串、整数和数组,并且您使用 PHP 7+ 并且不能使用多态,但您仍然觉得需要类型检查,你应该考虑 类型声明 或严格模式。

它在标准 PHP 语法之上为您提供静态类型。
手动类型检查的问题在于,这样做需要大量额外的废话,以至于你得到的虚假「类型安全」并不能弥补失去的可读性。
保持你的 PHP 干净,编写良好的测试,并进行良好的代码审查。否则,除了使用 PHP 严格类型声明或严格模式外,所有这些都可以。

Bad:

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

    return $val1 + $val2;
}

Good:

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

删除无效代码

无效代码代码与重复代码一样糟糕,没有理由保留它。如果它没有被调用,请删除它! 如果您仍然需要它,它在您的版本历史记录中仍然是安全的。

Bad:

function oldRequestModule(string $url): void
{
    // ...
}

function newRequestModule(string $url): void
{
    // ...
}

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

Good:

function requestModule(string $url): void
{
    // ...
}

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

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/clean-code-php/...

译文地址:https://learnku.com/docs/clean-code-php/...

上一篇 下一篇
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
贡献者:3
讨论数量: 0
发起讨论 查看所有版本


暂无话题~