PHP 的代码简洁之道(Clean Code PHP)
介绍
Robert C.Martin’s 的 软件工程师准则 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(); 使用可搜索的名称(第一部分)
我们阅读的代码超过我们写的代码。所以我们写出的代码需要具备可读性、可搜索性,这一点非常重要。要我们去理解程序中没有名字的变量是非常头疼的。让你的变量可搜索吧!
不具备可读性的代码:
// 见鬼的 448 是什么意思?
$result = $serializer->serialize($data, 448); 具备可读性的:
$json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); 使用可搜索的名称(第二部分)
不好的:
// 见鬼的 4 又是什么意思?
if ($user->access & 4) {// ...
} 好的方式:
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 ...
} 使用解释性变量
不好:
$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']); 避免嵌套太深和提前返回 (第一部分)
使用太多 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;} else {return false;}} else {return false;}} else {return false;}
} 很棒:
function isShopOpen(string $day): bool
{if (empty($day)) {return false;}$openingDays = ['friday', 'saturday', 'sunday'];return in_array(strtolower($day), $openingDays, true);
} 避免嵌套太深和提前返回 (第二部分)
不好:
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';}
} 很棒:
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;//...
} 使用默认参数而不是使用短路运算或者是条件判断
不好的做法:
这是不太好的因为 $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
{// ...
} 对比
使用 相等运算符
不好的做法:
$a = '42';
$b = 42; 使用简单的相等运算符会把字符串类型转换成数字类型
if( $a != $b ) {//这个条件表达式总是会通过
} 表达式 $a != $b 会返回 false 但实际上它应该是 true !
字符串类型 ‘42’是不同于数字类型的 42
好的做法:
使用全等运算符会对比类型和值
if( $a !== $b ) {//这个条件是通过的
} 表达式 $a !== $b 会返回 true。
函数
函数参数(2 个或更少)
限制函数参数个数极其重要
这样测试你的函数容易点。有超过 3 个可选参数会导致一个爆炸式组合增长,你会有成吨独立参数情形要测试。
无参数是理想情况。1 个或 2 个都可以,最好避免 3 个。
再多就需要加固了。通常如果你的函数有超过两个参数,说明他要处理的事太多了。 如果必须要传入很多数据,建议封装一个高级别对象作为参数。
不友好的:
function createMenu(string $title, string $body, string $buttonText, bool $cancellable): void
{// ...
} 友好的:
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
{// ...
} 函数应该只做一件事情
这是迄今为止软件工程最重要的原则。函数做了超过一件事情时,它们将变得难以编写、测试、推导。 而函数只做一件事情时,重构起来则非常简单,同时代码阅读起来也非常清晰。掌握了这个原则,你就会领先许多其他的开发者。
不好的:
function emailClients(array $clients): void
{foreach ($clients as $client) {$clientRecord = $db->find($client);if ($clientRecord->isActive()) {email($client);}}
} 好的:
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();
} 函数的名称要说清楚它做什么
不好的例子:
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 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...}
} 同样不是很好:
我们已经完成了一些功能,但是 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...}
} 很好的:
最好的解决方案是取出 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...}}
} 不要用标示作为函数的参数
标示就是在告诉大家,这个方法里处理很多事。前面刚说过,一个函数应当只做一件事。 把不同标示的代码拆分到多个函数里。
不友好的:
function createFile(string $name, bool $temp = false): void
{if ($temp) {touch('./temp/'.$name);} else {touch($name);}
} 友好的:
function createFile(string $name): void
{touch($name);
}function createTempFile(string $name): void
{touch('./temp/'.$name);
} 避免副作用
一个函数应该只获取数值,然后返回另外的数值,如果在这个过程中还做了其他的事情,我们就称为副作用。副作用可能是写入一个文件,修改某些全局变量,或者意外的把你全部的钱给了陌生人。
现在,你的确需要在一个程序或者场合里要有副作用,像之前的例子,你也许需要写一个文件。你需要做的是把你做这些的地方集中起来。不要用几个函数和类来写入一个特定的文件。只允许使用一个服务来单独实现。
重点是避免常见陷阱比如对象间共享无结构的数据、使用可以写入任何的可变数据类型、不集中去处理这些副作用。如果你做了这些你就会比大多数程序员快乐。
不好的:
// 这个全局变量在函数中被使用
// 如果我们在别的方法中使用这个全局变量,有可能我们会不小心将其修改为数组类型
$name = 'Ryan McDermott';
function splitIntoFirstAndLastName(): void
{global $name;$name = explode(' ', $name);
}
splitIntoFirstAndLastName();
var_dump($name); // ['Ryan', 'McDermott']; 推荐的:
function splitIntoFirstAndLastName(string $name): array
{return explode(' ', $name);
}$name = 'Ryan McDermott';
$newName = splitIntoFirstAndLastName($name);var_dump($name); // 'Ryan McDermott';
var_dump($newName); // ['Ryan', 'McDermott']; 不要定义全局函数
在很多语言中定义全局函数是一个坏习惯,因为你定义的全局函数可能与其他人的函数库冲突,并且,除非在实际运用中遇到异常,否则你的 API 的使用者将无法觉察到这一点。接下来我们来看看一个例子:当你想有一个配置数组,你可能会写一个 config() 的全局函数,但是这样会与其他人定义的库冲突。
不好的:
function config(): array
{return ['foo' => 'bar',]
} 好的:
class Configuration
{private $configuration = [];public function __construct(array $configuration){$this->configuration = $configuration;}public function get(string $key): ?string{return isset($this->configuration[$key]) ? $this->configuration[$key] : null;}
} 获取配置需要先创建 Configuration 类的实例,如下:
$configuration = new Configuration(['foo' => 'bar',
]); 现在,在你的应用中必须使用 Configuration 的实例了。
不要使用单例模式
单例模式是个 反模式。 以下转述 Brian Button 的观点:
单例模式常用于 全局实例, 这么做为什么不好呢? 因为在你的代码里 你隐藏了应用的依赖关系,而没有通过接口公开依赖关系 。避免全局的东西扩散使用是一种 代码味道.
单例模式违反了 单一责任原则: 依据的事实就是 单例模式自己控制自身的创建和生命周期.
单例模式天生就导致代码紧 耦合。这使得在许多情况下用伪造的数据 难于测试。
单例模式的状态会留存于应用的整个生命周期。 这会对测试产生第二次打击, 你只能让被严令需要测试的代码运行不了收场,根本不能进行单元测试。为何?因为每一个单元测试应该彼此独立。
不好的示范:
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(); 好的示范:
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(Traveler $vehicle): void
{$vehicle->travelTo(new Location('texas'));
} 避免类型检查(第 2 部分)
如果你正在使用像 字符串、数值、或数组这样的基础类型,你使用的是 PHP 版本是 PHP 7+,并且你不能使用多态,但仍然觉得需要使用类型检测,这时,你应该考虑 类型定义 或 严格模式。它为您提供了标准 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 and private。
使用它们,你可以任意的控制、修改对象的属性。
当你除获取对象属性外还想做更多的操作时,你不需要修改你的代码
当 set 属性时,易于增加参数验证。
封装的内部表示。
容易在获取和设置属性时添加日志和错误处理。
继承这个类,你可以重写默认信息。
你可以延迟加载对象的属性,比如从服务器获取数据。
此外,这样的方式也符合 OOP 开发中的 开闭原则
不好的:
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(); 让对象拥有 private/protected 属性的成员
public 公有方法和属性对于变化来说是最危险的,因为一些外部的代码可能会轻易的依赖他们,但是你没法控制那些依赖他们的代码, 类的变化对于类的所有使用者来说都是危险的。
protected 受保护的属性变化和 public 公有的同样危险,因为他们在子类范围内是可用的。也就是说 public 和 protected 之间的区别仅仅在于访问机制,只有封装才能保证属性是一致的。 任何在类内的变化对于所有继承子类来说都是危险的 。
private 私有属性的变化可以保证代码 只对单个类范围内的危险 (对于修改你是安全的,并且你不会有其他类似堆积木的影响 Jenga effect).
因此,请默认使用 private 属性,只有当需要对外部类提供访问属性的时候才采用 public/protected 属性。
不好的:
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 好的:
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 类 " class="reference-link"> 类
组合优于继承
正如 the Gang of Four 所著的 设计模式 中所说,
我们应该尽量优先选择组合而不是继承的方式。使用继承和组合都有很多好处。
这个准则的主要意义在于当你本能的使用继承时,试着思考一下组合是否能更好对你的需求建模。
在一些情况下,是这样的。
接下来你或许会想,“那我应该在什么时候使用继承?”
答案依赖于你的问题,当然下面有一些何时继承比组合更好的说明:
你的继承表达了 “是一个” 而不是 “有一个” 的关系(例如人类 “是” 动物,而用户 “有” 用户详情)。
你可以复用基类的代码(人类可以像动物一样移动)。
你想通过修改基类对所有派生类做全局的修改(当动物移动时,修改它们的能量消耗)
不好的:
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;}// ...
} 优美的:
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 的方法,旨在通过方法链 Method chaining 来提高源代码的可阅读性.
流式接口虽然需要一些上下文,需要经常构建对象,但是这种模式减少了代码的冗余度 (例如:PHPUnit Mock Builder或 Doctrine Query Builder),但是同样它也带来了很多麻烦:
1.破坏了封装 Encapsulation
2.破坏了原型 Decorators
3.难以模拟测试 mock
4.使得多次提交的代码难以理解
不好的:
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(); SOLID
SOLID 是 Michael Feathers 推荐的便于记忆的首字母简写,它代表了 Robert Martin 命名的最重要的五个面向对象编程设计原则:
S: 职责单一原则 (SRP)
O: 开闭原则 (OCP)
L: 里氏替换原则 (LSP)
I: 接口隔离原则 (ISP)
D: 依赖反转原则 (DIP)
职责单一原则 Single Responsibility Principle (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);}
} 里氏代换原则 (LSP)
这是一个简单概念的可怕术语。它通常被定义为 “如果 S 是 T 的一个子类型,则 T 型对象可以替换为 S 型对象”
(i.e., S 类型的对象可以替换 T 型对象) 在不改变程序的任何理想属性的情况下 (正确性,任务完成度,etc.).” 这是一个更可怕的定义.
这个的最佳解释是,如果你有个父类和一个子类,然后父类和子类可以互换使用而不会得到不正确的结果。这或许依然令人疑惑,所以我们来看下经典的正方形 - 矩形例子。几何定义,正方形是矩形,但是,如果你通过继承建立了 “IS-a” 关系的模型,你很快就会陷入麻烦。
不好的:
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); 优秀的:
abstract class Shape
{abstract public function getArea(): int;public function render(int $area): void{// ...}
}class Rectangle extends Shape
{private $width;private $height;public function __construct(int $width, int $height){$this->width = $width;$this->height = $height;}public function getArea(): int{return $this->width * $this->height;}
}class Square extends Shape
{private $length;public function __construct(int $length){$this->length = $length;}public function getArea(): int{return pow($this->length, 2);}
}/*** @param Rectangle[] $rectangles*/
function renderLargeRectangles(array $rectangles): void
{foreach ($rectangles as $rectangle) {$area = $rectangle->getArea(); $rectangle->render($area);}
}$shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeRectangles($shapes); 接口隔离原则 (ISP)
ISP 指出 “客户不应该被强制依赖于他们用不到的接口.”
一个好的例子来观察证实此原则的是针对需要大量设置对象的类,不要求客户端设置大量的选项是有益的,因为多数情况下他们不需要所有的设置。使他们可选来避免产生一个 “臃肿的接口”
坏的:
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}
} 好的:
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)
这一原则规定了两项基本内容:
1.高级模块不应依赖于低级模块。两者都应该依赖于抽象.
2.抽象类不应依赖于实例。实例应该依赖于抽象.
一开始可能很难去理解,但是你如果工作中使用过 php 框架(如 Symfony), 你应该见过以依赖的形式执行这一原则
依赖注入 (DI). 虽然他们不是相同的概念,DIP 可以让高级模块不需要了解其低级模块的详细信息而安装它们.
通过依赖注入可以做到。这样做的一个巨大好处是减少了模块之间的耦合。耦合是一种非常糟糕的开发模式,因为它使您的代码难以重构.
不好的:
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 原则。
尽你最大的努力去避免复制代码,它是一种非常糟糕的行为,复制代码通常意味着当你需要变更一些逻辑时,你需要修改不止一处。
试想一下,如果你在经营一家餐厅,并且你需要记录你仓库的进销记录:包括所有的土豆,洋葱,大蒜,辣椒,等等。如果你使用多个表格来管理进销记录,当你用其中一些土豆做菜时,你需要更新所有的表格。如果你只有一个列表的话就只需要更新一个地方。
通常情况下你复制代码的原因可能是它们大多数都是一样的,只不过有两个或者多个略微不同的逻辑,但是由于这些区别,最终导致你写出了两个或者多个隔离的但大部分相同的方法,移除重复的代码意味着用一个 function/module/class 创建一个能处理差异的抽象。
正确的抽象是非常关键的,这正是为什么你必须学习遵守在 Classes 章节展开讨论的的 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()]);}
} 译文地址:https://github.com/piotrplenik/clean-code-php
相关文章:
PHP 的代码简洁之道(Clean Code PHP)
介绍 Robert C.Martin’s 的 软件工程师准则 Clean Code 同样适用于 PHP。它并不是一个编码风格指南,它指导我们用 PHP 写出具有可读性,可复用性且可分解的代码。 并非所有的准则都必须严格遵守,甚至一些已经成为普遍的约定。这仅仅作为指导方…...
delphi在两个窗口间用消息通讯
用SendMessage在窗口间通讯: 发送方 var HWD: THandle; str1,str2:string; sData: TCopyDataStruct; begin HWD:FindWindow(nil,pchar(aaaaaa)); // Integer(pchar(self.Edit2.Text)) str2:我来了中玉人; str1:我来了中玉人; sData.cbDa…...
如何高效提高倾斜摄影三维模型顶层合并的技术方法分析
如何高效提高倾斜摄影三维模型顶层合并的技术方法分析 1、倾斜摄影三维模型顶层合并 1.1倾斜摄影三维模型是一种基于倾斜摄影技术,通过多个角度拍摄同一区域的影像,利用计算机图像处理和三维重建技术生成的三维地理信息数据。由于一个大区域可能需要多块…...
【科普】PCB为什么常用50Ω阻抗?6大原因
在PCB设计中,阻抗通常是指传输线的特性阻抗,这是电磁波在导线中传输时的特性阻抗,与导线的几何形状、介质材料和导线周围环境等因素有关。 对于一般的高速数字信号传输和RF电路,50Ω是一个常用的阻抗值。 为什么是50Ω?…...
Linux嵌入式uboot使用tftp网络启动加载zImage、设备树
文章目录 一、前言二、Linux U-boot 相关命令(1)help 命令(2)printenv 命令(3)setenv 函数(4)saveenv 函数 三、tftp启动linux内核步骤(1)进入u-boot模式&…...
使用Serv-U搭建FTP服务器并公网访问【内网穿透】
文章目录 1. 前言2. 本地FTP搭建2.1 Serv-U下载和安装2.2 Serv-U共享网页测试2.3 Cpolar下载和安装 3. 本地FTP发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 1. 前言 科技日益发展的今天,移动电子设备似乎成了我们生活的主角,智能…...
后端大厂面试总结大全六
目录: 1、Transactional注解控制事务有哪些不生效的场景2、MySQL的优化 1、Transactional注解控制事务有哪些不生效的场景 数据库引擎不支持事务数据源没有配置事务管理器没有被spring管理方法不是public的同一个类中方法调用,导致Transactional失效 举…...
2023五一数学建模A题B题C题思路模型代码
占个位置吧,开始在本帖实时更新五一数学建模赛题思路代码,文章末尾获取! 持续为更新参考思路 赛题思路 会持续进行思路模型分析,下自行获取。 A题思路: (比赛开始后第一时间更新) B题思路…...
Redis --- 入门、数据类型
一、前言 1.1、什么是Redis Redis是一个基于内存的key-value结构数据库。Redis 是互联网技术领域使用最为广泛的存储中间件,它是「Remote Dictionary Service」的首字母缩写,也就是「远程字典服务」。 基于内存存储,读写性能高适合存储热点…...
超级详解MySQL执行计划explain
1、什么是MySQL执行计划 要对执行计划有个比较好的理解,需要先对MySQL的基础结构及查询基本原理有简单的了解。 MySQL本身的功能架构分为三个部分,分别是 应用层、逻辑层、物理层,不只是MySQL ,其他大多数数据库产品都是按这种架构…...
C++流操作
C引入了ostringstream、istringstream、stringstream这三个类,要使用他们创建对象就必须包含<sstream>这个头文件。 在C中标准库提供三个类用于文件操作,统称为文件流类: ifstream:专用于从文件中读取数据;ofs…...
Django连接Redis、数据库、mongodb密码明文存储问题以及方案
文章目录 DDjango连接Redis、数据库、mongodb密码明文存储问题以及方案需求背景常见解决方案Django中redis、数据库密码明文加密存储方案 DDjango连接Redis、数据库、mongodb密码明文存储问题以及方案 需求背景 密码不应以明文出现在代码或配置文件中。 常见解决方案 在Dja…...
Qt实现仿微信在线聊天工具(服务器、客户端)V1_ 02
上一篇设计了登录和聊天窗口,并实现了窗口切换,本章将继续实现其他模块 本章内容 完善登录验证实现自定义窗口的拖动效果 完善登录验证 对登录的输入框进行输入限定,这里我限制登录id为6位纯数字组合,密码长度不超过16位 设置…...
Direct local .aar file dependencies are not supported when building an AAR.
前言 起因:项目中含有视频播放功能,使用的是GSYVideoPlayer,因为公司网络问题经常依赖添加不了,所以将关于它的aar包全部下载下来直接本地依赖。 因为多个业务都可能涉及视频播放功能,为了复用,就想着将视频…...
【Java基础】day16
day16 一、switch-case 和 if-else 谁更快? switch-case 在 switch-case 中,case 的值是连续的话,会生成一个 TableSwitch 来进行优化,这样的情况下,只需要在表中进行判断即可。 这里使用 0-4 的连续值来进行测试 如…...
Neo4j | 一文入门Neo4j!
下面是一些基本的Cypher查询语句: 创建节点 CREATE (n:Person {name:Alice})这会创建一个标签为Person、属性name值为Alice的节点。 创建节点之间的关系 MATCH (a:Person {name:Alice}), (b:Person {name:Bob}) CREATE (a)-[:FRIEND]->(b)这会创建Alice和Bob…...
Python科研数据可视化
在过去的20 年中,随着社会产生数据的大量增加,对数据的理解、解释与决策的需求也随之增加。而固定不变是人类本身,所以我们的大脑必须学会理解这些日益增加的数据信息。所谓“一图胜千言”,对于数量、规模与复杂性不断增加的数据&…...
叫板IT部门和专业软件公司,低代码成为企业数字化的新选择
从2017年政府将“数字经济”写入工作报告,到今年两会将企业数字化转型列为重点议题,数字化的口号已喊了6年。政策对于数字化的支持越来越坚定,令人欣喜的是,越来越多具有远见卓识的企业已将数字化建设作为工作重心。 然而…...
leetcode 541. 反转字符串 II
题目描述解题思路执行结果 leetcode 541. 反转字符串 II 题目描述 反转字符串 II 给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。 如果剩余字符少于 k 个,则将剩余字符全…...
java数据类型的转换以及精度丢失
java数据类型的转换以及精度丢失_long转double会丢失精度吗_ღLiJia的博客-CSDN博客 一.浮点类型在计算机当中的存储 float存储需求是4字节(32位), 其中1位最高位是符号位,中间8位表示阶位,后32位表示值 float的范围: -2^128 ~ …...
深度学习在微纳光子学中的应用
深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向: 逆向设计 通过神经网络快速预测微纳结构的光学响应,替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...
在软件开发中正确使用MySQL日期时间类型的深度解析
在日常软件开发场景中,时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志,到供应链系统的物流节点时间戳,时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库,其日期时间类型的…...
超短脉冲激光自聚焦效应
前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应,这是一种非线性光学现象,主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场,对材料产生非线性响应,可能…...
C++.OpenGL (10/64)基础光照(Basic Lighting)
基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...
在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理
引言 Bitmap(位图)是Android应用内存占用的“头号杀手”。一张1080P(1920x1080)的图片以ARGB_8888格式加载时,内存占用高达8MB(192010804字节)。据统计,超过60%的应用OOM崩溃与Bitm…...
JVM暂停(Stop-The-World,STW)的原因分类及对应排查方案
JVM暂停(Stop-The-World,STW)的完整原因分类及对应排查方案,结合JVM运行机制和常见故障场景整理而成: 一、GC相关暂停 1. 安全点(Safepoint)阻塞 现象:JVM暂停但无GC日志,日志显示No GCs detected。原因:JVM等待所有线程进入安全点(如…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?
uni-app 中 Web-view 与 Vue 页面的通讯机制详解 一、Web-view 简介 Web-view 是 uni-app 提供的一个重要组件,用于在原生应用中加载 HTML 页面: 支持加载本地 HTML 文件支持加载远程 HTML 页面实现 Web 与原生的双向通讯可用于嵌入第三方网页或 H5 应…...
