介绍

软件工程原理,来自 Robert C. Martin 的书 Clean Code,适用于 PHP。这不是一个风格指南。它是使用 PHP 生成可读、可重用和可重构软件的指南。
并非这里的每一个原则都必须严格遵守,更不用说得到普遍认同。这些只是指导方针,仅此而已,但它们是由 Clean Code 的作者多年集体经验总结而成的。
灵感来自 clean-code-javascript
尽管许多开发人员仍在使用 PHP 5,但本文中的大多数示例仅适用于 PHP 7.1+。

变量

使用有意义的变量名

Bad:

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

Good:

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

相同类型的变量使用相同的词汇

Bad:

  1. getUserInfo();
  2. getUserData();
  3. getUserRecord();
  4. getUserProfile();

Good:

  1. getUser();

使用可搜索的名称 (part 1)

写代码是用来读的。所以写出可读性高、可搜索的代码至关重要。命名的变量如果没有意义、不好理解,那就是在伤害读者。请让你的名称可搜索。
Bad:

  1. // 448 是干啥的?
  2. $result = $serializer->serialize($data, 448);

Good:

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

使用可搜索的名称 (part 2)

Bad:

  1. class User
  2. {
  3. // 7 是干啥的?
  4. public $access = 7;
  5. }
  6. // 4 是干啥的?
  7. if ($user->access & 4) {
  8. // ...
  9. }
  10. // 这里会发生什么?
  11. $user->access ^= 2;

Good:

  1. class User
  2. {
  3. public const ACCESS_READ = 1;
  4. public const ACCESS_CREATE = 2;
  5. public const ACCESS_UPDATE = 4;
  6. public const ACCESS_DELETE = 8;
  7. // 默认情况下用户具有读、写和更新权限
  8. public $access = self::ACCESS_READ | self::ACCESS_CREATE | self::ACCESS_UPDATE;
  9. }
  10. if ($user->access & User::ACCESS_UPDATE) {
  11. // do edit ...
  12. }
  13. // 禁用创建权限
  14. $user->access ^= User::ACCESS_CREATE;

使用自解释型变量

Bad:

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

Not bad:
好一些,但仍然强依赖正则表达式。

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

Good:
通过命名子规则减少对正则表达式的依赖。

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

避免嵌套太深并尽早返回 (part 1)

过多的 if-else 语句会使您的代码难以理解。
Bad:

  1. function isShopOpen($day): bool
  2. {
  3. if ($day) {
  4. if (is_string($day)) {
  5. $day = strtolower($day);
  6. if ($day === 'friday') {
  7. return true;
  8. } elseif ($day === 'saturday') {
  9. return true;
  10. } elseif ($day === 'sunday') {
  11. return true;
  12. }
  13. return false;
  14. }
  15. return false;
  16. }
  17. return false;
  18. }

Good:

  1. function isShopOpen(string $day): bool
  2. {
  3. if (empty($day)) {
  4. return false;
  5. }
  6. $openingDays = ['friday', 'saturday', 'sunday'];
  7. return in_array(strtolower($day), $openingDays, true);
  8. }

避免嵌套太深并尽早返回 (part 2)

Bad:

  1. function fibonacci(int $n)
  2. {
  3. if ($n < 50) {
  4. if ($n !== 0) {
  5. if ($n !== 1) {
  6. return fibonacci($n - 1) + fibonacci($n - 2);
  7. }
  8. return 1;
  9. }
  10. return 0;
  11. }
  12. return 'Not supported';
  13. }

Good:

  1. function fibonacci(int $n): int
  2. {
  3. if ($n === 0 || $n === 1) {
  4. return $n;
  5. }
  6. if ($n >= 50) {
  7. throw new Exception('Not supported');
  8. }
  9. return fibonacci($n - 1) + fibonacci($n - 2);
  10. }

避免心理映射

不要强迫代码的读者翻译变量的含义。
Bad:

  1. $l = ['Austin', 'New York', 'San Francisco'];
  2. for ($i = 0; $i < count($l); $i++) {
  3. $li = $l[$i];
  4. doStuff();
  5. doSomeOtherStuff();
  6. // ...
  7. // ...
  8. // ...
  9. // 等等, `$li` 又代表什么?
  10. dispatch($li);
  11. }

Good:

  1. $locations = ['Austin', 'New York', 'San Francisco'];
  2. foreach ($locations as $location) {
  3. doStuff();
  4. doSomeOtherStuff();
  5. // ...
  6. // ...
  7. // ...
  8. dispatch($location);
  9. }

不要添加不需要的上下文

如果您的类/对象名称告诉您某些信息,请不要在变量名称中重复。
Bad:

  1. class Car
  2. {
  3. public $carMake;
  4. public $carModel;
  5. public $carColor;
  6. //...
  7. }

Good:

  1. class Car
  2. {
  3. public $make;
  4. public $model;
  5. public $color;
  6. //...
  7. }

比较

使用相同的比较

Bad:
简单的比较会将字符串转换为整数。

  1. $a = '42';
  2. $b = 42;
  3. if ($a != $b) {
  4. // The expression will always pass
  5. }

比较 $a != $b 返回 false,但实际上是 true!字符串 '42' 与整数 42 不同。
Good:
相同的比较将比较类型和值。

  1. $a = '42';
  2. $b = 42;
  3. if ($a !== $b) {
  4. // The expression is verified
  5. }

比较 $a !== $b 返回 true

Null 合并运算符

Null 合并运算符是 PHP 7 新特性 。Null 合并运算符 ?? 是用来简化判断 isset() 的语法糖。如果第一个操作数存在且不为 null 则返回;否则返回第二个操作数。
Bad:

  1. if (isset($_GET['name'])) {
  2. $name = $_GET['name'];
  3. } elseif (isset($_POST['name'])) {
  4. $name = $_POST['name'];
  5. } else {
  6. $name = 'nobody';
  7. }

Good:

  1. $name = $_GET['name'] ?? $_POST['name'] ?? 'nobody';

函数

使用参数默认值

Bad:
这不好,因为 $breweryName 可以 null

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

Not bad:
好一些,但最好能控制变量的值。

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

Good:
您可以使用 类型声明 并确保 $breweryName 不会出现 null

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

函数参数 (最好少于2个)

限制函数参数的数量非常重要,因为它使测试函数更容易。超过三个会导致组合爆炸,您必须使用每个单独的参数测试大量不同的案例。
零参数是理想的情况。一两个参数是可以的,三个应该避免。除此之外的任何东西都应该合并。通常,如果您有两个以上的参数,那么您的函数会尝试做太多事情。如果必须要传入很多数据,建议封装一个高级别对象作为参数。
Bad:

  1. class Questionnaire
  2. {
  3. public function __construct(
  4. string $firstname,
  5. string $lastname,
  6. string $patronymic,
  7. string $region,
  8. string $district,
  9. string $city,
  10. string $phone,
  11. string $email
  12. ) {
  13. // ...
  14. }
  15. }

Good:

  1. class Name
  2. {
  3. private $firstname;
  4. private $lastname;
  5. private $patronymic;
  6. public function __construct(string $firstname, string $lastname, string $patronymic)
  7. {
  8. $this->firstname = $firstname;
  9. $this->lastname = $lastname;
  10. $this->patronymic = $patronymic;
  11. }
  12. // getters ...
  13. }
  14. class City
  15. {
  16. private $region;
  17. private $district;
  18. private $city;
  19. public function __construct(string $region, string $district, string $city)
  20. {
  21. $this->region = $region;
  22. $this->district = $district;
  23. $this->city = $city;
  24. }
  25. // getters ...
  26. }
  27. class Contact
  28. {
  29. private $phone;
  30. private $email;
  31. public function __construct(string $phone, string $email)
  32. {
  33. $this->phone = $phone;
  34. $this->email = $email;
  35. }
  36. // getters ...
  37. }
  38. class Questionnaire
  39. {
  40. public function __construct(Name $name, City $city, Contact $contact)
  41. {
  42. // ...
  43. }
  44. }

函数名称应该说明它们的作用

Bad:

  1. class Email
  2. {
  3. //...
  4. public function handle(): void
  5. {
  6. mail($this->to, $this->subject, $this->body);
  7. }
  8. }
  9. $message = new Email(...);
  10. // 这是什么? 处理消息? 是往文件里写吗?
  11. $message->handle();

Good:

  1. class Email
  2. {
  3. //...
  4. public function send(): void
  5. {
  6. mail($this->to, $this->subject, $this->body);
  7. }
  8. }
  9. $message = new Email(...);
  10. // 清晰明显
  11. $message->send();

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

当您有多个抽象级别时,您的函数通常做得太多。需要拆分功能来提高可重用性,以便更容易的测试。
Bad:

  1. function parseBetterPHPAlternative(string $code): void
  2. {
  3. $regexes = [
  4. // ...
  5. ];
  6. $statements = explode(' ', $code);
  7. $tokens = [];
  8. foreach ($regexes as $regex) {
  9. foreach ($statements as $statement) {
  10. // ...
  11. }
  12. }
  13. $ast = [];
  14. foreach ($tokens as $token) {
  15. // lex...
  16. }
  17. foreach ($ast as $node) {
  18. // parse...
  19. }
  20. }

Not bad:
我们已经实现了一些功能,但是 parseBetterPHPAlternative() 功能还是很复杂,无法测试。

  1. function tokenize(string $code): array
  2. {
  3. $regexes = [
  4. // ...
  5. ];
  6. $statements = explode(' ', $code);
  7. $tokens = [];
  8. foreach ($regexes as $regex) {
  9. foreach ($statements as $statement) {
  10. $tokens[] = /* ... */;
  11. }
  12. }
  13. return $tokens;
  14. }
  15. function lexer(array $tokens): array
  16. {
  17. $ast = [];
  18. foreach ($tokens as $token) {
  19. $ast[] = /* ... */;
  20. }
  21. return $ast;
  22. }
  23. function parseBetterPHPAlternative(string $code): void
  24. {
  25. $tokens = tokenize($code);
  26. $ast = lexer($tokens);
  27. foreach ($ast as $node) {
  28. // parse...
  29. }
  30. }

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...
        }
    }
}

不要使用 flag 作为函数参数

flag 就是在告诉大家,这个函数不止做一件事。一个函数应该只做一件事。如果它们基于布尔值遵循不同的代码路径,则拆分您的函数。
Bad:

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

Good:

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

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

避免副作用

如果一个函数除了接受一个值并返回另一个或多个值之外,它还会产生副作用。副作用可能是写入文件、修改某些全局变量或意外地将您所有的钱汇给陌生人。
现在,您有时确实需要在程序中产生副作用。与前面的示例一样,您可能需要写入文件。你要做的是把它们集中起来。不要用几个函数和类来写入一个特定的文件。用一个服务来做它。
重点是避免常见的陷阱,例如在没有任何结构的对象之间共享状态,使用可以被任何东西写入的可变数据类型,以及不集中出现副作用的位置。如果你能做到这一点,你会比绝大多数其他程序员更快乐。
Bad:

// 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(): void
{
    global $name;

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

splitIntoFirstAndLastName();

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

Good:

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(),但它可能会与另一个试图做同样事情的库发生冲突。
Bad:

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

Good:

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 关于问题的根源有个很好的想法。
Bad:

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();

Good:

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

    // ...
}

创建 DBConnection 类实例并使用 DSN 对其进行配置。

$connection = new DBConnection($dsn);

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

封装条件语句

Bad:

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

Good:

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

避免否定条件判断

Bad:

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

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

Good:

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

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

避免条件判断

这似乎是一项不可能完成的任务。第一次听到这个,大多数人都会说,“我怎么能在没有 if 语句的情况下做任何事情呢?” 答案是在许多情况下,您可以使用多态性来完成相同的任务。第二个问题通常是,“这很好,但我为什么要这样做?” 答案是我们之前学到的一个干净的代码概念:一个函数应该只做一件事。当你的类和函数有很多 if 语句时,你的函数不止做一件事。记住,只做一件事。
Bad:

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();
        }
    }
}

Good:

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();
    }
}

避免类型检查 (part 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'));
}

避免类型检查 (part 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');

对象和数据结构

使用对象封装

在 PHP 中,您可以为方法设置 publicprotectedprivate 关键字。使用它,您可以控制对象的属性修改。

  • 当您想要做的不仅仅是获取对象属性时,您不必查找和更改代码库中的每个访问器。
  • 在执行 set
  • 封装内部表示。
  • 在获取和设置时易于添加日志记录和错误处理。
  • 继承此类,您可以覆盖默认功能。
  • 您可以延迟加载对象的属性,比如说从服务器获取它。

此外,这是开放/关闭原则的一部分。
Bad:

class BankAccount
{
    public $balance = 1000;
}

$bankAccount = new BankAccount();

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

Good:

class BankAccount
{
    private $balance;

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

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

        $this->balance -= $amount;
    }

    public function deposit(int $amount): void
    {
        $this->balance += $amount;
    }

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

$bankAccount = new BankAccount();

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

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

使对象具有私有/受保护成员

  • public 方法和属性对于更改是最危险的,因为一些外部代码可能很容易依赖它们,而您无法控制哪些代码依赖它们。类中的修改对类的所有用户都是危险的。
  • protected 修饰符与 public 一样危险,因为它们在任何子类的范围内都可用。这实际上意味着 public 和 protected 之间的区别仅在于访问机制,但封装保持不变。类中的修改对所有后代类都是危险的。
  • private 修饰符保证仅在单个类的边界中修改代码是危险的(您可以安全地进行修改,并且不会有 Jenga 效应)。

因此,private 默认情况下以及 public/protected 需要为外部类提供访问权限时使用。
有关更多信息,您可以阅读 Fabien Potencier 撰写的关于此主题的博文
Bad:

class Employee
{
    public $name;

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

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

Good:

class Employee
{
    private $name;

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

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

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

更喜欢组合而不是继承

正如 the Gang of Four 在设计模式中所说的那样,在可能的情况下,您应该更喜欢组合而不是继承。使用继承有很多很好的理由,使用组合也有很多很好的理由。这个准则的要点是,如果你的大脑本能地选择继承,试着想想组合是否可以更好地模拟你的问题。在某些情况下可以。
你可能想知道,“我什么时候应该使用继承?” 这取决于您手头的问题,下面有一个很好的列表,说明何时继承比组合更有意义:

  1. 您的继承表示“is-a”关系而不是“has-a”关系(Human->Animal vs. User->UserDetails)。
  2. 您可以复用基类中的代码(人类可以像所有动物一样移动)。
  3. 您希望通过更改基类来对派生类进行全局更改。(改变所有动物移动时的热量消耗)。

Bad:

class Employee
{
    private $name;

    private $email;

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

    // ...
}

// 不好,因为 Employees 有 taxdata
// 而 EmployeeTaxData 不是 Employee 类型的

class EmployeeTaxData extends Employee
{
    private $ssn;

    private $salary;

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

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

    // ...
}

Good:

class EmployeeTaxData
{
    private $ssn;

    private $salary;

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

    // ...
}

class Employee
{
    private $name;

    private $email;

    private $taxData;

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

    public function setTaxData(EmployeeTaxData $taxData): void
    {
        $this->taxData = $taxData;
    }

    // ...
}

避免链式接口

Fluent 接口是一种面向对象的 API,旨在通过使用方法链来提高源代码的可读性 。
虽然可能有一些上下文,通常是构建器对象,但这种模式减少了代码的冗长性(例如 PHPUnit Mock BuilderDoctrine Query Builder),但更多时候它需要付出一些代价:

  1. 破坏对象封装
  2. 破坏装饰器模式
  3. 在测试套件中更难模拟。
  4. 使提交的差异更难阅读。

有关更多信息,您可以阅读 Marco Pivetta 撰写的关于此主题的博文
Bad:

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();

Good:

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();

推荐使用 final 类

应尽可能使用 final 关键字:

  1. 它可以防止不受控制的继承链。
  2. 它鼓励组合。
  3. 它鼓励单一职责原则。
  4. 它鼓励开发人员使用您的公共方法而不是扩展类来访问受保护的方法。
  5. 它允许您在不破坏使用您的类的应用程序的情况下更改代码。

唯一的条件是你的类应该实现一个接口并且没有定义其他公共方法。
有关更多信息,您可以阅读 Marco Pivetta (Ocramius) 撰写的关于此主题的博文
Bad:

final class Car
{
    private $color;

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

    /**
     * @return string The color of the vehicle
     */
    public function getColor()
    {
        return $this->color;
    }
}

Good:

interface Vehicle
{
    /**
     * @return string The color of the vehicle
     */
    public function getColor();
}

final class Car implements Vehicle
{
    private $color;

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

    public function getColor()
    {
        return $this->color;
    }
}

SOLID 原则

SOLID 是 Michael Feathers 为 Robert Martin 命名的易于记忆的首字母缩写词,表示面向对象编程和设计的五个基本原则。

  • S:单一职责原则 (SRP)
  • O:开/闭原则 (OCP)
  • L:里氏替换原则 (LSP)
  • 一:接口隔离原则 (ISP)
  • D:依赖倒置原则 (DIP)

    单一职责原则 (SRP)

    正如在 Clean Code 所述,”修改一个类应该只为一个理由”。人们总是易于用一堆方法塞满一个类,例如我们只能在飞机上携带一个行李箱(把所有的东西都塞到箱子里)。这样做的问题是:从概念上这样的类不是高内聚的,并且留下了很多理由去修改它。将你需要修改类的次数降低到最小很重要。 这是因为,当有很多方法在类中时,修改其中一处,你很难知晓在代码库中哪些依赖的模块会被影响到。
    Bad:

    class UserSettings
    {
      private $user;
    
      public function __construct(User $user)
      {
          $this->user = $user;
      }
    
      public function changeSettings(array $settings): void
      {
          if ($this->verifyCredentials()) {
              // ...
          }
      }
    
      private function verifyCredentials(): bool
      {
          // ...
      }
    }
    

    Good: ```php class UserAuth { private $user;

    public function __construct(User $user) {

      $this->user = $user;
    

    }

    public function verifyCredentials(): bool {

      // ...
    

    } }

class UserSettings { private $user;

private $auth;

public function __construct(User $user)
{
    $this->user = $user;
    $this->auth = new UserAuth($user);
}

public function changeSettings(array $settings): void
{
    if ($this->auth->verifyCredentials()) {
        // ...
    }
}

}

<a name="z1QU9"></a>
### 开闭原则 (OCP)
正如 Bertrand Meyer 所说,“软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。” 那是什么意思呢?该原则基本上表明您应该允许用户在不更改现有代码的情况下添加新功能。<br />**Bad:**
```php
abstract class Adapter
{
    protected $name;

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

class AjaxAdapter extends Adapter
{
    public function __construct()
    {
        parent::__construct();

        $this->name = 'ajaxAdapter';
    }
}

class NodeAdapter extends Adapter
{
    public function __construct()
    {
        parent::__construct();

        $this->name = 'nodeAdapter';
    }
}

class HttpRequester
{
    private $adapter;

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

    public function fetch(string $url): Promise
    {
        $adapterName = $this->adapter->getName();

        if ($adapterName === 'ajaxAdapter') {
            return $this->makeAjaxCall($url);
        } elseif ($adapterName === 'httpNodeAdapter') {
            return $this->makeHttpCall($url);
        }
    }

    private function makeAjaxCall(string $url): Promise
    {
        // request and return promise
    }

    private function makeHttpCall(string $url): Promise
    {
        // request and return promise
    }
}

Good:

interface Adapter
{
    public function request(string $url): Promise;
}

class AjaxAdapter implements Adapter
{
    public function request(string $url): Promise
    {
        // request and return promise
    }
}

class NodeAdapter implements Adapter
{
    public function request(string $url): Promise
    {
        // request and return promise
    }
}

class HttpRequester
{
    private $adapter;

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

    public function fetch(string $url): Promise
    {
        return $this->adapter->request($url);
    }
}

里氏替换原则 (LSP)

这是一个非常简单的概念的可怕术语。它被正式定义为“如果 S 是 T 的子类,则类 T 的对象可以被类 S 的对象替换(即,类 S 的对象可以替换类 T 的对象)而不改变该程序的任何所需属性(正确性、执行的任务等)。” 这是一个更可怕的定义。
对此最好的解释是,如果你有一个父类和一个子类,那么父类和子类可以互换使用而不会得到错误的结果。这可能仍然令人困惑,所以让我们看一下经典的 Square-Rectangle 示例。从数学上讲,正方形是长方形,但如果您通过继承使用“is-a”关系对其进行建模,您很快就会遇到麻烦。
Bad:

class Rectangle
{
    protected $width = 0;

    protected $height = 0;

    public function setWidth(int $width): void
    {
        $this->width = $width;
    }

    public function setHeight(int $height): void
    {
        $this->height = $height;
    }

    public function getArea(): int
    {
        return $this->width * $this->height;
    }
}

class Square extends Rectangle
{
    public function setWidth(int $width): void
    {
        $this->width = $this->height = $width;
    }

    public function setHeight(int $height): void
    {
        $this->width = $this->height = $height;
    }
}

function printArea(Rectangle $rectangle): void
{
    $rectangle->setWidth(4);
    $rectangle->setHeight(5);

    // BAD: Will return 25 for Square. Should be 20.
    echo sprintf('%s has area %d.', get_class($rectangle), $rectangle->getArea()) . PHP_EOL;
}

$rectangles = [new Rectangle(), new Square()];

foreach ($rectangles as $rectangle) {
    printArea($rectangle);
}

Good:
最好的方法是将四边形分开并为两种形状分配更通用的子类。
尽管正方形和长方形有明显的相似之处,但它们是不同的。正方形与菱形有很多共同点,长方形与平行四边形有很多共同之处,但它们不是子类。正方形、矩形、菱形和平行四边形是具有各自属性的独立形状,尽管相似。

interface Shape
{
    public function getArea(): int;
}

class Rectangle implements Shape
{
    private $width = 0;
    private $height = 0;

    public function __construct(int $width, int $height)
    {
        $this->width = $width;
        $this->height = $height;
    }

    public function getArea(): int
    {
        return $this->width * $this->height;
    }
}

class Square implements Shape
{
    private $length = 0;

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

    public function getArea(): int
    {
        return $this->length ** 2;
    }
}

function printArea(Shape $shape): void
{
    echo sprintf('%s has area %d.', get_class($shape), $shape->getArea()).PHP_EOL;
}

$shapes = [new Rectangle(4, 5), new Square(5)];

foreach ($shapes as $shape) {
    printArea($shape);
}

接口隔离原则 (ISP)

ISP 声明“不应强迫客户依赖他们不使用的接口”。
有一个很好的例子来说明示范这条原则。当一个类需要一个大量的设置项, 为了方便不会要求调用方去设置大量的选项,因为通常他们不需要所有的设置项。使设置项可选有助于我们避免产生“胖接口”。
Bad:

interface Employee
{
    public function work(): void;

    public function eat(): void;
}

class HumanEmployee implements Employee
{
    public function work(): void
    {
        // ....working
    }

    public function eat(): void
    {
        // ...... eating in lunch break
    }
}

class RobotEmployee implements Employee
{
    public function work(): void
    {
        //.... working much more
    }

    public function eat(): void
    {
        //.... robot can't eat, but it must implement this method
    }
}

Good:
不是每个工人都是雇员,但每个雇员都是工人。

interface Workable
{
    public function work(): void;
}

interface Feedable
{
    public function eat(): void;
}

interface Employee extends Feedable, Workable
{
}

class HumanEmployee implements Employee
{
    public function work(): void
    {
        // ....working
    }

    public function eat(): void
    {
        //.... eating in lunch break
    }
}

// robot can only work
class RobotEmployee implements Workable
{
    public function work(): void
    {
        // ....working
    }
}

依赖倒置原则 (DIP)

这个原则说明了两个基本的要点:

  1. 高级模块不应该依赖于低级模块。两者都应该依赖于抽象。
  2. 抽象不应依赖于实现。实现应该依赖于抽象。

起初这可能很难理解,但如果您使用过 PHP 框架(如 Symfony),您就会看到以依赖注入 (DI) 的形式实现这一原则。虽然它们不是相同的概念,但 DIP 使高级模块无法了解其低级模块的详细信息并对其进行设置。它可以通过 DI 实现这一点。这样做的一个巨大好处是它减少了模块之间的耦合。耦合是一种非常糟糕的开发模式,因为它使您的代码难以重构。
Bad:

class Employee
{
    public function work(): void
    {
        // ....working
    }
}

class Robot extends Employee
{
    public function work(): void
    {
        //.... working much more
    }
}

class Manager
{
    private $employee;

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

    public function manage(): void
    {
        $this->employee->work();
    }
}

Good:

interface Employee
{
    public function work(): void;
}

class Human implements Employee
{
    public function work(): void
    {
        // ....working
    }
}

class Robot implements Employee
{
    public function work(): void
    {
        //.... working much more
    }
}

class Manager
{
    private $employee;

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

    public function manage(): void
    {
        $this->employee->work();
    }
}

DRY 原则

尽最大努力避免重复代码。重复代码是不好的,因为这意味着如果您需要更改某些逻辑,则需要在多个地方进行更改。
想象一下,如果您经营一家餐馆并跟踪您的库存:所有的西红柿、洋葱、大蒜、香料等。如果您有多个清单要保留,那么当您提供带有西红柿的菜肴时,所有清单都必须更新。如果您只有一个列表,那么只有一个地方需要更新!
通常你有重复的代码,因为你有两个或多个稍微不同的东西,它们有很多共同点,但它们的差异迫使你有两个或多个独立的函数来做很多相同的事情。删除重复代码意味着创建一个抽象,它可以只用一个函数/模块/类来处理这组不同的事情。
获得正确的抽象是至关重要的,这就是为什么你应该遵循部分中列出的 SOLID 原则。糟糕的抽象可能比重复代码更糟糕,所以要小心!说了这么多,如果你能做出好的抽象,那就去做吧!不要重复自己,否则您会发现自己在任何时候想要更改一件事时都会更新多个地方。
Bad:

function showDeveloperList(array $developers): void
{
    foreach ($developers as $developer) {
        $expectedSalary = $developer->calculateExpectedSalary();
        $experience = $developer->getExperience();
        $githubLink = $developer->getGithubLink();
        $data = [$expectedSalary, $experience, $githubLink];

        render($data);
    }
}

function showManagerList(array $managers): void
{
    foreach ($managers as $manager) {
        $expectedSalary = $manager->calculateExpectedSalary();
        $experience = $manager->getExperience();
        $githubLink = $manager->getGithubLink();
        $data = [$expectedSalary, $experience, $githubLink];

        render($data);
    }
}

Good:

function showList(array $employees): void
{
    foreach ($employees as $employee) {
        $expectedSalary = $employee->calculateExpectedSalary();
        $experience = $employee->getExperience();
        $githubLink = $employee->getGithubLink();
        $data = [$expectedSalary, $experience, $githubLink];

        render($data);
    }
}

Very good:
最好使用代码的紧凑版本。

function showList(array $employees): void
{
    foreach ($employees as $employee) {
        render([$employee->calculateExpectedSalary(), $employee->getExperience(), $employee->getGithubLink()]);
    }
}