一、变量
使用有意义且可读的变量名
Bad:
$ymdstr = $moment->format('y-m-d');
Good:
$currentDate = $moment->format('y-m-d');
对同一类型的变量使用相同的词汇
Bad:
getUserInfo();
getUserData();
getUserRecord();
getUserProfile();
Good:
getUser();
使用可搜索的名称
Bad:
// 448 到底是干什么用的?
$result = $serializer->serialize($data, 448);
Good:
$json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
使用解释变量
Bad:
$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/';
preg_match($cityZipCodeRegex, $address, $matches);
saveCityZipCode($matches[1], $matches[2]);
Good:
$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,]+,\s*(?<city>.+?)\s*(?<zipCode>\d{5})$/';
preg_match($cityZipCodeRegex, $address, $matches);
saveCityZipCode($matches['city'], $matches['zipCode']);
避免嵌套太深并尽早返回(第 1 部分)
过多的 if-else 语句会使您的代码难以理解。
显式优于隐式。
Bad:
function isShopOpen($day): bool
{
if ($day) {
if (is_string($day)) {
$day = strtolower($day);
if ($day === 'friday') {
return true;
} elseif ($day === 'saturday') {
return true;
} elseif ($day === 'sunday') {
return true;
}
return false;
}
return false;
}
return false;
}
Good:
function isShopOpen(string $day): bool
{
if (empty($day)) {
return false;
}
$openingDays = ['friday', 'saturday', 'sunday'];
return in_array(strtolower($day), $openingDays, true);
}
避免嵌套太深并尽早返回(第 2 部分)
Bad:
function fibonacci(int $n)
{
if ($n < 50) {
if ($n !== 0) {
if ($n !== 1) {
return fibonacci($n - 1) + fibonacci($n - 2);
}
return 1;
}
return 0;
}
return 'Not supported';
}
Good:
function fibonacci(int $n): int
{
if ($n === 0 || $n === 1) {
return $n;
}
if ($n >= 50) {
throw new Exception('Not supported');
}
return fibonacci($n - 1) + fibonacci($n - 2);
}
避免心理映射
不要强迫代码的读者理解变量的含义。
显式优于隐式。
Bad:
$l = ['Austin', 'New York', 'San Francisco'];
for ($i = 0; $i < count($l); $i++) {
$li = $l[$i];
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// Wait, what is `$li` for again?
dispatch($li);
}
Good:
$locations = ['Austin', 'New York', 'San Francisco'];
foreach ($locations as $location) {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch($location);
}
不要添加不需要的上下文
如果你的 类或对象
已经有明确的含义,不要在变量中再次重复它。
Bad:
class Car
{
public $carMake;
public $carModel;
public $carColor;
//...
}
Good:
class Car
{
public $make;
public $model;
public $color;
//...
}
二、函数
使用默认参数替代短路或条件
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:
您可以使用 类型提示 并确保 $breweryName 不会是 NULL。
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:
您可以使用 类型提示 并确保 $breweryName 不会是 NULL。
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:
您可以使用 类型提示 并确保 $breweryName 不会是 NULL。
class Email
{
//...
public function send(): void
{
mail($this->to, $this->subject, $this->body);
}
}
$message = new Email(...);
// 清晰明了
$message->send();
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...
}
}
}
不要使用标志作为函数参数
标志会告诉您的用户,这个函数可以做不止一件事情。函数应该做一件事情。 如果函数遵循不同的代码路径,请基于布尔值将它们拆分。
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:
// 通过以下函数引用的全局变量。
// 如果我们有另外一个使用这个名字函数,现在它将是一个数组,它可能会破坏它。
$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 的实例。
不要使用单例模式
- 它通常被用作 全局实例,为什么会这么糟糕呢?因为在您的代码中 隐藏了应用程序的依赖,而不是通过接口暴露它们。使之全局以避免传递是一种 代码味道。
- .它们违反了 单一责任原则:由于 它们控制自己的创建和生命周期。
- 它们内在地导致代码紧密的 耦合。在许多情况下,这使得 在测试中伪装它们 变得相当困难。
- 在应用程序的生命周期中,它们始终保持着状态。测试的另一个亮点是 您可能会遇到对测试需要排序的情况,这对于单元测试来说是一个很大的禁忌。为什么?因为每个单元测试应该相互独立。
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();
}
}
避免类型检查(第 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');
面向对象和数据结构
使用对象封装
在 PHP 中,您可以为方法设置 public、protected 和 private 关键字。使用它,您可以控制对象的属性修改。
当您想要做的不仅仅是获取对象属性时,您不必查找和更改代码库中的每个访问器。
- 在执行「设置」时使添加验证变得简单。
- 封装内部表现形式。
- 获取和设置时易于添加日志记录和错误处理。
- 继承此类,您可以覆盖默认功能。
- 您可以延迟加载对象的属性,比如说从服务器获取它。
此外,这是 开闭原则 的一部分。
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
修饰符保证代码 仅在单个类的边界内修改是危险的(您可以安全地修改并且不会有 叠叠乐效应).
因此,当需要为外部类提供访问权限时,默认情况下使用 private
和 public/protected
。
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();
类相关
组合优于继承
正如在四人帮著名的 设计模式 中所说的那样,
你应该更青睐于组合而不是继承。使用继承和组合的理由有很多。这句格言的主要观点是,如果你倾向于继承,试着想想组合是否能更好的模拟你的问题。在某些情况下,它可以做到。
你可能会想,“我应该什么时候使用继承?” 这取决于你手头上的问题,但是这是一个关于继承比组合更有意义的列表:
- 你的继承代表了 “是一个” 的关系,并且不是 “有一个” 的关系 (人类 -> 动物 vs. 用户 -> 用户详情)。
- 你可以从基类中重用代码 (人类可以像所有动物一样移动)。
- 你想通过更改基类对派生类进行全局更改。(改变所有动物移动时所消耗的卡路里)。
Bad
class Employee
{
private $name;
private $email;
public function __construct(string $name, string $email)
{
$this->name = $name;
$this->email = $email;
}
// ...
}
// 不好是因为 Employees “有” 税收数据。
// 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;
}
// ...
}
避免流畅的接口
流畅接口 是一个面向对象的 API,旨在通过 方法链 提高源代码的可读性。
虽然可能存在一些上下文,频繁的构建对象,这个模式在那里减少了代码的冗余度 (如 PHPUnit 模拟构造器 或者是 查询构造器),更多的时候,它需要付出一些代价:
- 破坏 封装。
- 破坏 装饰。
- 在测试套件中更难 模拟 。
- 使提交的差异更难阅读。
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 classes 优先
应尽可能使用 final 关键字:
- 它防止了不受控制的继承链。
- 它鼓励 组成。
- 它鼓励单一职责原则。
- 它鼓励开发人员使用您的公共方法而不是扩展类来访问受保护的方法。
- 它允许您在不破坏使用您的类应用程序的情况下更改代码。
唯一的条件是您的类应该实现一个接口,并且没有定义其他公共方法。
Bad
final class Car
{
private $color;
public function __construct($color)
{
$this->color = $color;
}
/**
* @return 返回汽车的颜色
*/
public function getColor()
{
return $this->color;
}
}
Good
interface Vehicle
{
/**
* @return 字符串 车辆颜色
*/
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 命名的前五个原则引入的助记首字母缩写词,表示面向对象编程和设计的五个基本原则。
单一职责原则 (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
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()) {
// ...
}
}
}
开 / 闭原则 (OCP)
正如 Bertrand Meyer 所说,「软件实体(类、模块、函数、
等等。) 应该对扩展开放,但对修改关闭。」那是什么意思呢? 该原则基本上表明您应该允许用户在不更改现有代码的情况下添加新功能。
Bad
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
{
// ....工作中
}
public function eat(): void
{
//.... 午休时间吃饭
}
}
// 机器人只能工作
class RobotEmployee implements Workable
{
public function work(): void
{
// ....工作中
}
}
依赖反转原则 (DIP)
这个原则声明了两件基本的事情:
- 高层模块不应依赖于低层模块。两者都应依赖于抽象。
- 抽象不应依赖于具体。 具体应依赖于抽象。
一开始这很难理解,但如果您使用过 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 编程原则
尝试遵循 DRY 原则。
尽最大努力避免重复代码。重复代码是不好的,因为这意味着如果要更改某些逻辑,你需要更改多个地方的内容。
想象一下,如果你经营了一家餐厅并记录了你的库存:所有的西红柿,洋葱,大蒜,香料等。如果你有多个清单并记录了它,当你上了一道西红柿菜品,这时所有的清单必须被更新。如果你只有一个清单,那么只更新一个地方!
你经常写重复的代码,是因为你有两个或多个略微不同的事情,它们有很多共同之处,但是它们的不同迫使你有两个或多个独立的函数去处理相同的事情。删除重复代码意味着创建一个能够处理这些不同设置的抽象,只需一个函数 / 模块 / 类就可以。
正确的抽象至关重要,这就是为什么你应该遵循 Classes 部分中列出的 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()]);
}
}
数据对比
使用 相同比较
Bad
$a = '42';
$b = 42;
if ($a != $b) {
// 表达式将始终通过
}
Good
$a = '42';
$b = 42;
if ($a !== $b) {
// 表达式已验证
}
Null 合并运算符
Null 合并是一个新的运算符 在 PHP 7 中引入。 null 合并运算符 ?? 已被添加为语法糖,用于常见情况下需要将三元组与 isset() 结合使用。如果第一个操作数存在且不为 null,则返回第一个操作数;否则返回第二个操作数。
Bad
if (isset($_GET['name'])) {
$name = $_GET['name'];
} elseif (isset($_POST['name'])) {
$name = $_POST['name'];
} else {
$name = 'nobody';
}
Good
$name = $_GET['name'] ?? $_POST['name'] ?? 'nobody';