导语
本文主要是对github上的clean-code-php的翻译。翻译水平有限,有错误的地方,多多指正。
介绍
软件工程原理,来自于Robert C.Martin's 的书——《Clean Code》,适用于PHP。这本书不是一个风格指导书。它是让PHP可读,可重用,可重构的指南。
不是每一个准则必须去遵守,且甚至更少的会被同意。这本书仅仅只是指导方针,是《Clean Code》作者根据多年集体经验来撰写的。
尽管有一些开发者还在用PHP5,但是本文的大多数例子仅适用于PHP7.1+。
变量(Variables)
用有意义和可声明的变量名
Bad:
$ymdstr = $moment->format('y-m-d');
Good:
$currentDate = $moment->format('y-m-d');
对同类型的变量用相同的词汇
Bad:
getUserInfo();
getUserData();
getUserRecord();
getUserProfile();
Good:
getUser();
用可搜索的名字(第一部分)
我们将读的代码比我们写的代码更多,所以我们的代码的可读性、可搜索性是相当重要的。不把变量命名为对我们程序有意义的名字,这是对读者的一种伤害。让你的命名变得可搜索吧。
Bad:
// What the heck is 448 for?
$result = $serializer->serialize($data, 448);
Good:
$json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
用可搜索的名字(第二部分)
少用硬编码
Bad:
// What the heck is 4 for?
if ($user->access & 4) {
// ...
}
Good:
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 ...
}
用可解释的变量
Bad:
$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/';
preg_match($cityZipCodeRegex, $address, $matches);
saveCityZipCode($matches[1], $matches[2]);
Not bad:
好一些,但是我们还是重度依赖于正则表达式
$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/';
preg_match($cityZipCodeRegex, $address, $matches);
[, $city, $zipCode] = $matches;
saveCityZipCode($city, $zipCode);
Good:
通过子模式减小对正则表达式的依赖
$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,\\]+[,\\\s]+(?<city>.+?)\s*(?<zipCode>\d{5})?$/';
preg_match($cityZipCodeRegex, $address, $matches);
saveCityZipCode($matches['city'], $matches['zipCode']);
避免嵌套的太深,早点返回(第一部分)
太多的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;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
}
Good:
function isShopOpen(string $day): bool
{
if (empty($day)) {
return false;
}
$openingDays = [
'friday', 'saturday', 'sunday'
];
return in_array(strtolower($day), $openingDays, true);
}
避免嵌套的太深,早点返回(第二部分)
Bad:
function fibonacci(int $n)
{
if ($n < 50) {
if ($n !== 0) {
if ($n !== 1) {
return fibonacci($n - 1) + fibonacci($n - 2);
} else {
return 1;
}
} else {
return 0;
}
} else {
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
可能为空
function createMicrobrewery($breweryName = 'Hipster Brew Co.'): void
{
// ...
}
Not bad:
这种方法比上面的更好理解,且它更好控制变量的值
function createMicrobrewery($name = null): void
{
$breweryName = $name ?: 'Hipster Brew Co.';
// ...
}
Good:
如果你使用的是PHP7+,你能用类型提醒(type hinting)确定
$breweryName
是否为空了。
function createMicrobrewery(string $breweryName = 'Hipster Brew Co.'): void
{
// ...
}
函数
函数参数(2个或者更少)
限制函数的参数是非常重要的,因为它使得测试函数更容易。超过三个参数会导致组合爆炸,你不得不去测试每个参数大量不同的用例。
没有参数是最理想的情况,一个或者两个参数也是可以的,应该避免三个参数。任何超过三个参数的都应该被合并。通常,如果你有两个以上的参数,那么你的函数就是去做很多事。如果不是这样,大多数时候一个高级对象就足够作为一个参数了。
Bad:
function createMenu(string $title, string $body, string $buttonText, bool $cancellable): void
{
// ...
}
Good:
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): void
{
// ...
}
函数只做一件事
在软件工程中,这是最重要的准则。函数不止做一件事,会让它们很难去编写、测试和理解。当你可以将函数限制只做一件事,它们将会更简单的被重构,代码也更加清晰。如果你从这份指南中只学会这一点,你也会比其他开发者优秀。
Bad:
function emailClients(array $clients): void
{
foreach ($clients as $client) {
$clientRecord = $db->find($client);
if ($clientRecord->isActive()) {
email($client);
}
}
}
Good:
function emailClients(array $clients): void
{
$activeClients = activeClients($clients);
array_walk($activeClients, 'email');
}
function activeClients(array $clients): array
{
return array_filter($clients, 'isClientActive');
}
function isClientActive(int $client): bool
{
$clientRecord = $db->find($client);
return $clientRecord->isActive();
}
方法名需要明确该方法的作用
Bad:
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();
Good:
class Email
{
//...
public function send(): void
{
mail($this->to, $this->subject, $this->body);
}
}
$message = new Email(...);
// Clear and obvious
$message->send();
函数应该是一个抽象层次
当您有多个抽象级别时,您的函数通常做得太多。拆分函数会有可重用性和更容易的测试。
Bad:
function parseBetterJSAlternative(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
我们已经执行了一些功能,但是
parseBetterJSAlternative()
函数仍然非常复杂,而且不可以测试。
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 parseBetterJSAlternative(string $code): void
{
$tokens = tokenize($code);
$ast = lexer($tokens);
foreach ($ast as $node) {
// parse...
}
}
Good
最好的办法是移出
parseBetterJSAlternative()
函数
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 BetterJSAlternative
{
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...
}
}
}
不要用flags
作为函数参数
flags
是告诉你这个函数不只做一件事情。函数就应该只做一件事。如果函数遵循布尔值的不同代码路径,拆分你的函数。
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
{
return isset($this->configuration[$key]) ? $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(): DBConnection
{
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();
}
}
避免类型检查(第一部分)
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(Traveler $vehicle): void
{
$vehicle->travelTo(new Location('texas'));
}
避免类型检查(第二部分)
如果您正在使用基本的类型,如字符串、整数和数组,并且使用PHP7+,并且不能使用多态性,但仍然觉得需要键入检查,则应考虑类型声明或严格模式。它为你提供了静态类型上标准的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 withdrawBalance(int $amount): void
{
if ($amount > $this->balance) {
throw new \Exception('Amount greater than available balance.');
}
$this->balance -= $amount;
}
public function depositBalance(int $amount): void
{
$this->balance += $amount;
}
public function getBalance(): int
{
return $this->balance;
}
}
$bankAccount = new BankAccount();
// Buy shoes...
$bankAccount->withdrawBalance($shoesPrice);
// Get balance
$balance = $bankAccount->getBalance();
让对象有private/protected
成员
public
方法和属性的更改最为危险,因为一些外部代码可能很容易依赖它们,而您无法控制哪些代码依赖它们。修改类的类的所有用户都是危险的。protected
修饰符与public
一样危险,因为它们可在任何子类的范围内使用。这实际上意味着公共和受保护之间的差异仅限于访问机制,但封装保证保持不变。修改类的所有子类都是危险的。private
修饰符保证代码仅在单一的类中修改是危险的(你是安全的,你不必修改边效果)。因此,使用
private
作为默认,当您需要提供外部类访问用public/protected
,
更多信息,你可以阅读这个话题,Fabien Potencier写的博客。
Bad:
class Employee
{
public $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
$employee = new Employee('John Doe');
echo 'Employee name: '.$employee->name; // Employee name: John Doe
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');
echo 'Employee name: '.$employee->getName(); // Employee name: John Doe
类
基于继承的编写
正如Gang of Four在《Design Patterns》上所说的那样,你应该更喜欢基于继承的构思。这里有很多好的理由去用基于继承的编写,如果你的头脑本能地追求继承,那么试着想想,在哪些情况下构图能更好地为你的问题建模。
你可能会想,“我应该何时使用继承?”这取决于你手头上的问题,但这是一个很好的列表,说明什么时候继承比组成更有意义:
- 你继承的是一个“is-a”关系而不是“has-a”关系(人->动物 vs 用户->用户详情)。
- 你可以重用基类的代码(人类可以像所有的动物运动)。
- 您希望通过更改基类来对派生类进行全局更改。(改变所有动物的热量消耗移动时)。
Bad:
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 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(string $ssn, string $salary)
{
$this->taxData = new EmployeeTaxData($ssn, $salary);
}
// ...
}
避免频繁的接口
一个流畅的接口是一个面向对象的API,目的是通过使用方法链来提高源代码的可读性。
虽然会有一些情况下,频繁生成对象,降低了代码的冗余,但是更多的时候会带来开销:
- 打破封装
- 打破装饰
- 在测试套件中更难模拟
- 使
commit
的差别难以阅读
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();
SOLID
SOLID
是Robert Martin命名面向对象编程和设计的五个基本原则的首字母简写。
- S: Single Responsibility Principle (SRP)
- O: Open/Closed Principle (OCP)
- L: Liskov Substitution Principle (LSP)
- I: Interface Segregation Principle (ISP)
- D: Dependency Inversion Principle (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:
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的对象)。
最好的解释是,如果您有父类和子类,那么基类和子类可以互换使用而不会得到不正确的结果。这可能仍然是混乱的,所以让我们在经典的矩形的例子看看。在数学上,正方形是长方形的,但是如果你的模型使用“is-a”关系通过继承,你很快就会陷入麻烦。
Bad:
class Rectangle
{
protected $width = 0;
protected $height = 0;
public function render(int $area): void
{
// ...
}
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;
}
}
/**
* @param Rectangle[] $rectangles
*/
function renderLargeRectangles(array $rectangles): void
{
foreach ($rectangles as $rectangle) {
$rectangle->setWidth(4);
$rectangle->setHeight(5);
$area = $rectangle->getArea(); // BAD: Will return 25 for Square. Should be 20.
$rectangle->render($area);
}
}
$rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles($rectangles);
Good:
abstract class Shape
{
protected $width = 0;
protected $height = 0;
abstract public function getArea(): int;
public function render(int $area): void
{
// ...
}
}
class Rectangle extends Shape
{
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 Shape
{
private $length = 0;
public function setLength(int $length): void
{
$this->length = $length;
}
public function getArea(): int
{
return pow($this->length, 2);
}
}
/**
* @param Rectangle[] $rectangles
*/
function renderLargeRectangles(array $rectangles): void
{
foreach ($rectangles as $rectangle) {
if ($rectangle instanceof Square) {
$rectangle->setLength(5);
} elseif ($rectangle instanceof Rectangle) {
$rectangle->setWidth(4);
$rectangle->setHeight(5);
}
$area = $rectangle->getArea();
$rectangle->render($area);
}
}
$shapes = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles($shapes);
接口隔离原则(ISP)
ISP指出,“客户不应该依赖于那些他们不需要的接口。”
有一个清晰的例子来说明示范这条原则。当一个类需要一个大量的设置项, 为了方便不会要求调用方去设置大量的选项,因为在通常他们不需要所有的 设置项。使设置项可选有助于我们避免产生"胖接口
Bad:
interface Employee
{
public function work(): void;
public function eat(): void;
}
class Human implements Employee
{
public function work(): void
{
// ....working
}
public function eat(): void
{
// ...... eating in lunch break
}
}
class Robot 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 Human implements Employee
{
public function work(): void
{
// ....working
}
public function eat(): void
{
//.... eating in lunch break
}
}
// robot can only work
class Robot implements Workable
{
public function work(): void
{
// ....working
}
}
依赖反转原则(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)
试着去遵循DRY 原则.
尽你最大的努力去避免复制代码,它是一种非常糟糕的行为,复制代码 通常意味着当你需要变更一些逻辑时,你需要修改不止一处。
通常情况下你复制代码是应该有两个或者多个略微不同的逻辑,它们大多数 都是一样的,但是由于它们的区别致使你必须有两个或者多个隔离的但大部 分相同的方法,移除重复的代码意味着用一个function/module/class创 建一个能处理差异的抽象。
正确的抽象是非常关键的,这正是为什么你必须学习遵守在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()
]);
}
}