如何编写编写干净的 PHP 代码
如何编写编写干净的 PHP 代码
介绍
软件工程原理,摘自 Robert C. Martin 的著作 Clean Code, 适用于PHP。这不是风格指南。这是一份生产指南 PHP 中可读、可重用和可重构的软件。
并非这里的每一项原则都必须严格遵守,甚至更少是普遍遵守的 商定。这些是指导方针,仅此而已,但它们是许多准则的编纂 Clean Code 作者多年的集体经验。
灵感来自 clean-code-javascript。
尽管许多开发人员仍在使用 PHP 5,但本文中的大多数示例仅适用于 PHP 7.1+。
变量
使用有意义且可发音的变量名称
坏:
$ymdstr = $moment->format('y-m-d');
好:
$currentDate = $moment->format('y-m-d');
对相同类型的变量使用相同的词汇
坏:
getUserInfo();
getUserData();
getUserRecord();
getUserProfile();
好:
getUser();
使用可搜索的名称(第 1 部分)
我们将阅读比我们编写的更多的代码。重要的是,我们编写的代码是 可读和可搜索。通过_不_命名最终有意义的变量 了解我们的程序,我们伤害了我们的读者。 使你的名字可搜索。
坏:
// What the heck is 448 for?
$result = $serializer->serialize($data, 448);
好:
$json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
使用可搜索的名称(第 2 部分)
坏:
class User
{// What the heck is 7 for?public $access = 7;
}// What the heck is 4 for?
if ($user->access & 4) {// ...
}// What's going on here?
$user->access ^= 2;
好:
class User
{public const ACCESS_READ = 1;public const ACCESS_CREATE = 2;public const ACCESS_UPDATE = 4;public const ACCESS_DELETE = 8;// User as default can read, create and update somethingpublic $access = self::ACCESS_READ | self::ACCESS_CREATE | self::ACCESS_UPDATE;
}if ($user->access & User::ACCESS_UPDATE) {// do edit ...
}// Deny access rights to create something
$user->access ^= User::ACCESS_CREATE;
使用解释变量
坏:
$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);[, $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']);
避免嵌套太深,提早返回(第 1 部分)
过多的 if-else 语句会使代码难以理解。显式更好 比隐含。
坏:
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;
}
好:
function isShopOpen(string $day): bool
{if (empty($day)) {return false;}$openingDays = ['friday', 'saturday', 'sunday'];return in_array(strtolower($day), $openingDays, true);
}
避免嵌套太深,提早返回(第 2 部分)
坏:
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';
}
好:
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);
}
避免思维导图
不要强迫代码的读者翻译变量的含义。 显式比隐式好。
坏:
$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);
}
好:
$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;//...
}
比较
使用相同的比较
不好:
简单的比较会将字符串转换为整数。
$a = '42';
$b = 42;if ($a != $b) {// The expression will always pass
}
比较回来了,但实际上是! 字符串与整数不同。$a != $b``FALSE``TRUE``42``42
好:
相同的比较将比较类型和值。
$a = '42';
$b = 42;if ($a !== $b) {// The expression is verified
}
比较返回 。$a !== $b``TRUE
Null 合并运算符
Null 合并是 PHP 7 中引入的一个新运算符。已将 null 合并运算符添加为句法糖,用于需要将三元与 结合使用的常见情况。如果它存在并且不存在,则返回其第一个操作数;否则,它将返回其第二个操作数。??``isset()``null
坏:
if (isset($_GET['name'])) {$name = $_GET['name'];
} elseif (isset($_POST['name'])) {$name = $_POST['name'];
} else {$name = 'nobody';
}
好:
$name = $_GET['name'] ?? $_POST['name'] ?? 'nobody';
功能
使用默认参数而不是短路或条件
不好:
这不好,因为可以.$breweryName``NULL
function createMicrobrewery($breweryName = 'Hipster Brew Co.'): void
{// ...
}
不错:
这种观点比以前的版本更容易理解,但它更好地控制了变量的值。
function createMicrobrewery($name = null): void
{$breweryName = $name ?: 'Hipster Brew Co.';// ...
}
好:
您可以使用类型提示,并确保 不会是 .$breweryName``NULL
function createMicrobrewery(string $breweryName = 'Hipster Brew Co.'): void
{// ...
}
函数参数(理想情况下为 2 个或更少)
限制函数参数的数量非常重要,因为它使 更轻松地测试您的函数。拥有三个以上会导致组合爆炸 你必须用每个单独的参数测试大量不同的情况。
零参数是理想的情况。一两个论点是可以的,应该避免三个论点。 除此之外,任何事情都应该被合并。通常,如果您有两个以上的 参数,那么你的函数试图做太多。如果不是,大多数 在更高级别的对象作为参数就足够了。
坏:
class Questionnaire
{public function __construct(string $firstname,string $lastname,string $patronymic,string $region,string $district,string $city,string $phone,string $email) {// ...}
}
好:
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){// ...}
}
函数名称应说明它们的作用
坏:
class Email
{//...public function handle(): void{mail($this->to, $this->subject, $this->body);}
}$message = new Email(...);
// What is this? A handle for the message? Are we writing to a file now?
$message->handle();
好:
class Email
{//...public function send(): void{mail($this->to, $this->subject, $this->body);}
}$message = new Email(...);
// Clear and obvious
$message->send();
函数应该只是一个抽象级别
当你有多个抽象级别时,你的函数通常是 做得太多了。拆分功能可提高可重用性,更轻松 测试。
坏:
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...}
}
也不好:
我们已经执行了一些功能,但功能仍然非常复杂且无法测试。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...}
}
好:
最好的解决方案是移出函数的依赖关系。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);
}
避免副作用
如果函数执行除 和 中的值之外的任何操作,则会产生副作用 返回另一个或多个值。副作用可能是写入文件、修改 一些全局变量,或者不小心把你所有的钱都汇给一个陌生人。
现在,您确实需要偶尔在程序中出现副作用。和以前一样 例如,您可能需要写入文件。您要做的是集中在哪里 你正在这样做。不要有多个函数和类写入特定的 文件。有一个服务可以做到这一点。一个也是唯一一个。
重点是避免常见的陷阱,例如在没有 任何结构,使用可变数据类型,可以由任何内容写入,而不是 集中出现副作用的位置。如果你能做到这一点,你会更快乐 比绝大多数其他程序员都要好。
坏:
// 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'];
好:
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 operatorreturn $this->configuration[$key] ?? null;}
}
加载配置并创建类的实例Configuration
$configuration = new Configuration(['foo' => 'bar',
]);
现在,您必须在应用程序中使用实例。Configuration
不要使用单一实例模式
Singleton 是一种反模式。转述自布莱恩·巴顿(Brian Button):
-
它们通常被用作全局实例,为什么这么糟糕?因为您将应用程序的依赖项隐藏在代码中,而不是通过接口公开它们。制作全球性的东西以避免传递它是一种代码味道。
-
他们违反了单一责任原则:因为他们控制着自己的创造和生命周期。
-
它们固有地导致代码紧密耦合。这使得在许多情况下在测试中伪造它们变得相当困难。
-
它们在应用程序的生存期内携带状态。测试的另一个打击,因为你最终可能会遇到需要订购测试的情况,这对单元测试来说是一个很大的否定。为什么?因为每个单元测试都应该彼此独立。
Misko Hevery对问题的根源也有很好的想法。
坏:
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){// ...}// ...
}
创建类的实例并使用 DSN 对其进行配置。DBConnection
$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。
坏:
function travelToTexas($vehicle): void
{if ($vehicle instanceof Bicycle) {$vehicle->pedalTo(new Location('texas'));} elseif ($vehicle instanceof Car) {$vehicle->driveTo(new Location('texas'));}
}
好:
function travelToTexas(Vehicle $vehicle): void
{$vehicle->travelTo(new Location('texas'));
}
避免类型检查(第 2 部分)
如果您正在使用字符串、整数和数组等基本基元值, 你使用 PHP 7+,你不能使用多态性,但你仍然觉得有必要 类型检查,您应该考虑类型声明或严格模式。它为您提供了基于标准PHP语法的静态类型。 手动类型检查的问题在于,这样做需要很多 额外的措辞是,你得到的虚假“类型安全”并不能弥补损失 可读性。保持你的PHP干净,编写好的测试,并有良好的代码审查。 否则,除了PHP严格类型声明或严格模式之外,还可以执行所有这些操作。
坏:
function combine($val1, $val2): int
{if (! is_numeric($val1) || ! is_numeric($val2)) {throw new Exception('Must be of type Number');}return $val1 + $val2;
}
好:
function combine(int $val1, int $val2): int
{return $val1 + $val2;
}
删除死代码
死代码和重复代码一样糟糕。没有理由保留它 您的代码库。如果它没有被调用,请摆脱它!它仍然是安全的 如果您仍然需要它,请在您的版本历史记录中。
坏:
function oldRequestModule(string $url): void
{// ...
}function newRequestModule(string $url): void
{// ...
}$request = newRequestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');
好:
function requestModule(string $url): void
{// ...
}$request = requestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');
对象和数据结构
使用对象封装
在 PHP 中,您可以为方法设置 、 和关键字。 使用它,您可以控制对象的属性修改。public``protected``private
-
当您想在获取对象属性之外执行更多操作时,您没有 查找和更改代码库中的每个访问器。
-
使在执行 .
set -
封装内部表示形式。
-
在获取和设置时易于添加日志记录和错误处理。
-
继承此类后,可以重写默认功能。
-
您可以延迟加载对象的属性,例如从 服务器。
此外,这是开/闭原则的一部分。
坏:
class BankAccount
{public $balance = 1000;
}$bankAccount = new BankAccount();// Buy shoes...
$bankAccount->balance -= 100;
好:
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 一样危险,因为它们在任何子类的范围内都可用。这实际上意味着公共和受保护之间的区别仅在于访问机制,但封装保证保持不变。类中的修改对于所有后代类都是危险的。 -
privatemodifier 保证代码仅在单个类的边界内修改是危险的(修改是安全的,并且不会产生 Jenga 效应)。
因此,在默认情况下以及需要为外部类提供访问权限时使用。private``public/protected
有关更多信息,您可以阅读 Fabien Potencier 撰写的有关此主题的博客文章。
坏:
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;
好:
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();
类
首选组合而不是继承
正如四人帮在《设计模式》中所说的那样, 在可能的情况下,您应该更喜欢组合而不是继承。有很多 使用继承的充分理由和使用组合的充分理由。 这句格言的要点是,如果你的思想本能地去做 继承,试着想想组合是否可以更好地模拟你的问题。在一些 案例可以。
那么,您可能想知道,“我什么时候应该使用继承?它 取决于你手头的问题,但这是一个不错的继承清单 比构图更有意义:
-
你的继承代表一种“是”的关系,而不是“有”的关系 关系(人-动物与用户->>UserDetails)。
-
您可以重用基类中的代码(人类可以像所有动物一样移动)。
-
您希望通过更改基类来对派生类进行全局更改。 (改变所有动物移动时的热量消耗)。
坏:
class Employee
{private $name;private $email;public function __construct(string $name, string $email){$this->name = $name;$this->email = $email;}// ...
}// Bad because Employees "have" tax data.
// EmployeeTaxData is not a type of Employeeclass 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;}// ...
}
好:
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,旨在通过使用方法链接提高源代码的可读性。
虽然可能有一些上下文,通常是构建器对象,其中 pattern 降低了代码的冗长程度(例如 PHPUnit Mock Builder 或 Doctrine Query Builder), 更多时候,它需要付出一些代价:
-
中断封装。
-
中断装饰器。
-
在测试套件中更难嘲笑。
-
使提交的差异更难阅读。
有关更多信息,您可以阅读 Marco Picetta 撰写的有关此主题的完整博客文章。
坏:
class Car
{private $make = 'Honda';private $model = 'Accord';private $color = 'white';public function setMake(string $make): self{$this->make = $make;// NOTE: Returning this for chainingreturn $this;}public function setModel(string $model): self{$this->model = $model;// NOTE: Returning this for chainingreturn $this;}public function setColor(string $color): self{$this->color = $color;// NOTE: Returning this for chainingreturn $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();
首选期末课程
应尽可能使用关键字:final
-
它可以防止不受控制的继承链。
-
它鼓励作文。
-
它鼓励单一责任原则。
-
它鼓励开发人员使用您的公共方法,而不是扩展类来访问受保护的方法。
-
它允许您更改代码,而不会破坏使用您的类的应用程序。
唯一的条件是你的类应该实现一个接口,并且没有定义其他公共方法。
有关更多信息,您可以阅读 Marco Pivetta (Ocramius) 撰写的有关此主题的博客文章。
坏:
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;}
}
好:
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 是 Michael Feathers 为罗伯特·马丁 (Robert Martin) 命名的前五个原则引入的首字母缩写词,意思是面向对象编程和设计的五个基本原则。
-
S:单一责任原则 (SRP)
-
O:开/闭原理 (OCP)
-
L:李氏替代原理(LSP)
-
I:接口隔离原则(ISP)
-
D:依赖反转原则 (DIP)
单一责任原则 (SRP)
正如 Clean Code 中所述,“一个类不应该有多个原因 改变”。将一个具有许多功能的类塞进包装是很诱人的,例如 当您在航班上只能携带一个行李箱时。这样做的问题是 你的班级在概念上不会有凝聚力,它会给出很多理由 来改变。尽量减少更改类所需的次数非常重要。 这很重要,因为如果一个类中有太多功能,并且您修改了其中的一部分, 可能很难理解这将如何影响其他依赖模块 您的代码库。
坏:
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{// ...}
}
好:
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 所说,“软件实体(类、模块、函数、 等)应该开放以进行扩展,但关闭以进行修改。那有什么作用 但意思是?这个原则基本上表明你应该允许用户 在不更改现有代码的情况下添加新功能。
坏:
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}
}
好:
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);}
}
Liskov 替代原理 (LSP)
对于一个非常简单的概念来说,这是一个可怕的术语。它的正式定义是“如果 S 是 T 的子类型,则 T 类型的对象可以替换为 S 类型的对象 (即,S 类型的对象可以替换 T 类型的对象),而不改变任何 该程序的理想属性(正确性、执行的任务、 等等)。这是一个更可怕的定义。
对此最好的解释是,如果你有一个父类和一个子类, 那么基类和子类可以互换使用,而不会得到 结果不正确。这可能仍然令人困惑,所以让我们来看看 经典的 Square-Rectangle 示例。在数学上,正方形是一个矩形,但是 如果通过继承使用“is-a”关系对其进行建模,则很快 惹上麻烦了。
坏:
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);
}
好:
最好的方法是将四边形分开,并为两种形状分配一个更通用的子类型。
尽管正方形和矩形表面上相似,但它们是不同的。 正方形与菱形有很多共同之处,矩形与平行四边形有很多共同之处,但它们不是亚型。 正方形、矩形、菱形和平行四边形是独立的形状,具有各自的属性,尽管相似。
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 指出,“不应强迫客户端依赖于 他们不使用。
一个很好的例子可以证明这个原则是 需要大型设置对象的类。不需要客户端设置 大量的选择是有益的,因为大多数时候他们不需要 所有设置。使它们成为可选有助于防止出现“胖接口”。
坏:
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}
}
好:
不是每个工人都是雇员,但每个雇员都是工人。
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)
该原则规定了两件基本的事情:
-
高级模块不应依赖于低级模块。两者都应该 依赖于抽象。
-
抽象不应依赖于细节。细节应取决于 抽象。
一开始可能很难理解,但如果你使用过PHP框架(如Symfony),你就会看到这个原则以依赖的形式实现 注射 (DI)。虽然它们不是相同的概念,但 DIP 保持了高层次 模块,了解其低级模块的详细信息并设置它们。 它可以通过 DI 来实现这一点。这样做的一个巨大好处是它减少了 模块之间的耦合。耦合是一种非常糟糕的开发模式,因为 它使你的代码难以重构。
坏:
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();}
}
好:
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 原则。
尽最大努力避免重复代码。重复代码很糟糕,因为 这意味着如果需要,有不止一个地方可以更改某些内容 改变一些逻辑。
想象一下,如果你经营一家餐馆,你跟踪你的库存:所有的 西红柿、洋葱、大蒜、香料等。如果您有多个列表 你保持这个状态,那么当你端上一道菜时,一切都必须更新 西红柿在里面。如果您只有一个列表,则只有一个位置可以更新!
通常,您有重复的代码,因为您有两个或更多代码 不同的东西,有很多共同点,但它们的差异迫使你 具有两个或多个单独的函数,它们执行许多相同的操作。删除 重复代码意味着创建一个抽象,可以处理这组不同的 只有一个函数/模块/类的东西。
获得正确的抽象是至关重要的,这就是为什么你应该遵循 “类”部分中列出的 SOLID 原则。糟糕的抽象可能是 比重复代码更糟糕,所以要小心!话虽如此,如果你能做到 一个好的抽象,去做吧!不要重复自己,否则你会发现自己 随时更新多个位置,想要更改一件事。
坏:
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);}
}
好:
function showList(array $employees): void
{foreach ($employees as $employee) {$expectedSalary = $employee->calculateExpectedSalary();$experience = $employee->getExperience();$githubLink = $employee->getGithubLink();$data = [$expectedSalary, $experience, $githubLink];render($data);}
}
非常好:
最好使用代码的紧凑版本。
function showList(array $employees): void
{foreach ($employees as $employee) {render([$employee->calculateExpectedSalary(), $employee->getExperience(), $employee->getGithubLink()]);}
}