├── .gitattributes ├── .github └── workflows │ └── coding_standard.yaml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json └── ecs.php /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md linguist-documentation=false 2 | *.md linguist-language=PHP 3 | -------------------------------------------------------------------------------- /.github/workflows/coding_standard.yaml: -------------------------------------------------------------------------------- 1 | name: Coding Standard 2 | 3 | on: 4 | pull_request: null 5 | push: null 6 | 7 | jobs: 8 | coding_standard: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | # see https://github.com/shivammathur/setup-php 14 | - uses: shivammathur/setup-php@v2 15 | with: 16 | php-version: 8.0 17 | coverage: none 18 | 19 | - run: composer install --no-progress --ansi 20 | 21 | - run: vendor/bin/ecs check-markdown README.md --ansi 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | /vendor -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ryan McDermott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean Code PHP 2 | 3 | ## 目录 4 | 5 | 1. [介绍](#介绍) 6 | 2. [变量](#变量) 7 | * [使用见字知意的变量名](#使用见字知意的变量名) 8 | * [同一个实体要用相同的变量名](#同一个实体要用相同的变量名) 9 | * [使用便于搜索的名称 (part 1)](#使用便于搜索的名称-part-1) 10 | * [使用便于搜索的名称 (part 2)](#使用便于搜索的名称-part-2) 11 | * [使用自解释型变量](#使用自解释型变量) 12 | * [避免深层嵌套,尽早返回 (part 1)](#避免深层嵌套尽早返回-part-1) 13 | * [避免深层嵌套,尽早返回 (part 2)](#避免深层嵌套尽早返回-part-2) 14 | * [少用无意义的变量名](#少用无意义的变量名) 15 | * [不要添加不必要上下文](#不要添加不必要上下文) 3. [表达式](#表达式) 16 | * [使用恒等式](#使用恒等式) 17 | * [Null合并运算符](#null合并运算符) 18 | 4. [函数](#函数) 19 | * [合理使用参数默认值,没必要在方法里再做默认值检测](#合理使用参数默认值没必要在方法里再做默认值检测) 20 | * [函数参数(最好少于2个)](#函数参数-最好少于2个) 21 | * [函数应该只做一件事](#函数应该只做一件事) 22 | * [函数名应体现他做了什么事](#函数名应体现他做了什么事) 23 | * [函数里应当只有一层抽象abstraction](#函数里应当只有一层抽象abstraction) 24 | * [不要用flag作为函数的参数](#不要用flag作为函数的参数) 25 | * [避免副作用](#避免副作用) 26 | * [不要写全局函数](#不要写全局函数) 27 | * [不要使用单例模式](#不要使用单例模式) 28 | * [封装条件语句](#封装条件语句) 29 | * [避免用反义条件判断](#避免用反义条件判断) 30 | * [避免条件判断](#避免条件判断) 31 | * [避免类型检查 (part 1)](#避免类型检查-part-1) 32 | * [避免类型检查 (part 2)](#避免类型检查-part-2) 33 | * [移除僵尸代码](#移除僵尸代码) 34 | 5. [对象和数据结构 Objects and Data Structures](#对象和数据结构) 35 | * [使用 getters 和 setters Use object encapsulation](#使用-getters-和-setters) 36 | * [给对象使用私有或受保护的成员变量](#给对象使用私有或受保护的成员变量) 37 | 6. [类](#类) 38 | * [少用继承多用组合](#少用继承多用组合) 39 | * [避免连贯接口](#避免连贯接口) 40 | * [推荐使用 final 类](#推荐使用-final-类) 41 | 7. [类的SOLID原则 SOLID](#solid) 42 | * [S: 单一职责原则 Single Responsibility Principle (SRP)](#单一职责原则) 43 | * [O: 开闭原则 Open/Closed Principle (OCP)](#开闭原则) 44 | * [L: 里氏替换原则 Liskov Substitution Principle (LSP)](#里氏替换原则) 45 | * [I: 接口隔离原则 Interface Segregation Principle (ISP)](#接口隔离原则) 46 | * [D: 依赖倒置原则 Dependency Inversion Principle (DIP)](#依赖倒置原则) 47 | 8. [别写重复代码 (DRY)](#别写重复代码-dry) 48 | 9. [翻译](#翻译) 49 | 50 | ## 介绍 51 | 52 | 53 | 本文参考自 Robert C. Martin的[*Clean Code*](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) 书中的软件工程师的原则 54 | ,适用于PHP。 这不是风格指南。 这是一个关于开发可读、可复用并且可重构的PHP软件指南。 55 | 56 | 并不是这里所有的原则都得遵循,甚至很少的能被普遍接受。 这些虽然只是指导,但是都是*Clean Code*作者多年总结出来的。 57 | 58 | 本文受到 [clean-code-javascript](https://github.com/ryanmcdermott/clean-code-javascript) 的启发 59 | 60 | 虽然很多开发者还在使用PHP5,但是本文中的大部分示例的运行环境需要PHP 7.1+。 61 | 62 | ## 翻译说明 63 | 64 | 翻译完成度100%,最后更新时间2020-10-26。本文由 php-cpm 基于 [yangweijie版本](https://github.com/yangweijie/clean-code-php) 的[clean-code-php](https://github.com/jupeter/clean-code-php)翻译并同步大量原文内容。 65 | 66 | 原文更新频率较高,我的翻译方法是直接用文本比较工具逐行对比。优先保证文字内容是最新的,再逐步提升翻译质量。 67 | 68 | 阅读过程中如果遇到各种链接失效、内容老旧、术语使用错误和其他翻译错误等问题,欢迎大家积极提交PR。 69 | 70 | ## **变量** 71 | 72 | ### 使用见字知意的变量名 73 | 74 | **坏:** 75 | 76 | ```php 77 | $ymdstr = $moment->format('y-m-d'); 78 | ``` 79 | 80 | **好:** 81 | 82 | ```php 83 | $currentDate = $moment->format('y-m-d'); 84 | ``` 85 | 86 | **[⬆ 返回顶部](#目录)** 87 | 88 | ### 同一个实体要用相同的变量名 89 | 90 | **坏:** 91 | 92 | ```php 93 | getUserInfo(); 94 | getUserData(); 95 | getUserRecord(); 96 | getUserProfile(); 97 | ``` 98 | 99 | **好:** 100 | 101 | ```php 102 | getUser(); 103 | ``` 104 | 105 | **[⬆ 返回顶部](#目录)** 106 | 107 | ### 使用便于搜索的名称 (part 1) 108 | 109 | 写代码是用来读的。所以写出可读性高、便于搜索的代码至关重要。 110 | 命名变量时如果没有有意义、不好理解,那就是在伤害读者。 111 | 请让你的代码便于搜索。 112 | 113 | **坏:** 114 | ```php 115 | // 448 ™ 干啥的? 116 | $result = $serializer->serialize($data, 448); 117 | ``` 118 | 119 | **好:** 120 | 121 | ```php 122 | $json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); 123 | ``` 124 | 125 | ### 使用便于搜索的名称 (part 2) 126 | 127 | **坏:** 128 | 129 | ```php 130 | class User 131 | { 132 | // 7 ™ 干啥的? 133 | public $access = 7; 134 | } 135 | 136 | // 4 ™ 干啥的? 137 | if ($user->access & 4) { 138 | // ... 139 | } 140 | 141 | // 这里会发生什么? 142 | $user->access ^= 2; 143 | ``` 144 | 145 | **好:** 146 | 147 | ```php 148 | class User 149 | { 150 | public const ACCESS_READ = 1; 151 | 152 | public const ACCESS_CREATE = 2; 153 | 154 | public const ACCESS_UPDATE = 4; 155 | 156 | public const ACCESS_DELETE = 8; 157 | 158 | // 默认情况下用户 具有读、写和更新权限 159 | public $access = self::ACCESS_READ | self::ACCESS_CREATE | self::ACCESS_UPDATE; 160 | } 161 | 162 | if ($user->access & User::ACCESS_UPDATE) { 163 | // do edit ... 164 | } 165 | 166 | // 禁用创建权限 167 | $user->access ^= User::ACCESS_CREATE; 168 | ``` 169 | 170 | **[⬆ 返回顶部](#目录)** 171 | 172 | ### 使用自解释型变量 173 | 174 | **坏:** 175 | 176 | ```php 177 | $address = 'One Infinite Loop, Cupertino 95014'; 178 | $cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/'; 179 | preg_match($cityZipCodeRegex, $address, $matches); 180 | 181 | saveCityZipCode($matches[1], $matches[2]); 182 | ``` 183 | 184 | **不错:** 185 | 186 | 好一些,但强依赖于正则表达式的熟悉程度 187 | 188 | ```php 189 | $address = 'One Infinite Loop, Cupertino 95014'; 190 | $cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/'; 191 | preg_match($cityZipCodeRegex, $address, $matches); 192 | 193 | [, $city, $zipCode] = $matches; 194 | saveCityZipCode($city, $zipCode); 195 | ``` 196 | 197 | **好:** 198 | 199 | 使用带名字的子规则,不用懂正则也能看的懂 200 | 201 | ```php 202 | $address = 'One Infinite Loop, Cupertino 95014'; 203 | $cityZipCodeRegex = '/^[^,]+,\s*(?.+?)\s*(?\d{5})$/'; 204 | preg_match($cityZipCodeRegex, $address, $matches); 205 | 206 | saveCityZipCode($matches['city'], $matches['zipCode']); 207 | ``` 208 | 209 | **[⬆ 返回顶部](#目录)** 210 | 211 | ### 避免深层嵌套,尽早返回 (part 1) 212 | 213 | 太多的if else语句通常会导致你的代码难以阅读,直白优于隐晦 214 | 215 | **糟糕:** 216 | 217 | ```php 218 | function isShopOpen($day): bool 219 | { 220 | if ($day) { 221 | if (is_string($day)) { 222 | $day = strtolower($day); 223 | if ($day === 'friday') { 224 | return true; 225 | } elseif ($day === 'saturday') { 226 | return true; 227 | } elseif ($day === 'sunday') { 228 | return true; 229 | } 230 | return false; 231 | } 232 | return false; 233 | } 234 | return false; 235 | } 236 | ``` 237 | 238 | **好:** 239 | 240 | ```php 241 | function isShopOpen(string $day): bool 242 | { 243 | if (empty($day)) { 244 | return false; 245 | } 246 | 247 | $openingDays = ['friday', 'saturday', 'sunday']; 248 | 249 | return in_array(strtolower($day), $openingDays, true); 250 | } 251 | ``` 252 | 253 | **[⬆ 返回顶部](#目录)** 254 | 255 | ### 避免深层嵌套,尽早返回 (part 2) 256 | 257 | **糟糕的:** 258 | 259 | ```php 260 | function fibonacci(int $n) 261 | { 262 | if ($n < 50) { 263 | if ($n !== 0) { 264 | if ($n !== 1) { 265 | return fibonacci($n - 1) + fibonacci($n - 2); 266 | } 267 | return 1; 268 | } 269 | return 0; 270 | } 271 | return 'Not supported'; 272 | } 273 | ``` 274 | 275 | **好:** 276 | 277 | ```php 278 | function fibonacci(int $n): int 279 | { 280 | if ($n === 0 || $n === 1) { 281 | return $n; 282 | } 283 | 284 | if ($n >= 50) { 285 | throw new Exception('Not supported'); 286 | } 287 | 288 | return fibonacci($n - 1) + fibonacci($n - 2); 289 | } 290 | ``` 291 | 292 | **[⬆ 返回顶部](#目录)** 293 | 294 | ### 少用无意义的变量名 295 | 296 | 别让读你的代码的人猜你写的变量是什么意思。 297 | 写清楚好过模糊不清。 298 | 299 | **坏:** 300 | 301 | ```php 302 | $l = ['Austin', 'New York', 'San Francisco']; 303 | 304 | for ($i = 0; $i < count($l); $i++) { 305 | $li = $l[$i]; 306 | doStuff(); 307 | doSomeOtherStuff(); 308 | // ... 309 | // ... 310 | // ... 311 | // 等等, `$li` 又代表什么? 312 | dispatch($li); 313 | } 314 | ``` 315 | 316 | **好:** 317 | 318 | ```php 319 | $locations = ['Austin', 'New York', 'San Francisco']; 320 | 321 | foreach ($locations as $location) { 322 | doStuff(); 323 | doSomeOtherStuff(); 324 | // ... 325 | // ... 326 | // ... 327 | dispatch($location); 328 | } 329 | ``` 330 | 331 | **[⬆ 返回顶部](#目录)** 332 | 333 | ### 不要添加不必要上下文 334 | 335 | 如果从你的类名、对象名已经可以得知一些信息,就别再在变量名里重复。 336 | 337 | **坏:** 338 | 339 | ```php 340 | class Car 341 | { 342 | public $carMake; 343 | 344 | public $carModel; 345 | 346 | public $carColor; 347 | 348 | //... 349 | } 350 | ``` 351 | 352 | **好:** 353 | 354 | ```php 355 | class Car 356 | { 357 | public $make; 358 | 359 | public $model; 360 | 361 | public $color; 362 | 363 | //... 364 | } 365 | ``` 366 | 367 | **[⬆ 返回顶部](#目录)** 368 | 369 | 370 | ## 表达式 371 | 372 | ### [使用恒等式](http://php.net/manual/en/language.operators.comparison.php) 373 | 374 | **不好:** 375 | 376 | 简易对比会将字符串转为整形 377 | 378 | ```php 379 | $a = '42'; 380 | $b = 42; 381 | 382 | if ($a != $b) { 383 | //这里始终执行不到 384 | } 385 | ``` 386 | 387 | 对比 $a != $b 返回了 `FALSE` 但应该返回 `TRUE` ! 388 | 字符串 '42' 跟整数 42 不相等 389 | 390 | **好:** 391 | 392 | 使用恒等判断检查类型和数据 393 | 394 | ```php 395 | $a = '42'; 396 | $b = 42; 397 | 398 | if ($a !== $b) { 399 | // The expression is verified 400 | } 401 | ``` 402 | 403 | The comparison `$a !== $b` returns `TRUE`. 404 | 405 | **[⬆ 返回顶部](#目录)** 406 | 407 | ### Null合并运算符 408 | 409 | Null合并运算符是 [PHP 7新特性](https://www.php.net/manual/en/migration70.new-features.php). Null合并运算符 `??` 是用来简化判断`isset()`的语法糖。如果第一个操作数存在且不为`null`则返回;否则返回第二个操作数。 410 | 411 | **不好:** 412 | 413 | ```php 414 | if (isset($_GET['name'])) { 415 | $name = $_GET['name']; 416 | } elseif (isset($_POST['name'])) { 417 | $name = $_POST['name']; 418 | } else { 419 | $name = 'nobody'; 420 | } 421 | ``` 422 | 423 | **好:** 424 | ```php 425 | $name = $_GET['name'] ?? $_POST['name'] ?? 'nobody'; 426 | ``` 427 | 428 | **[⬆ 返回顶部](#目录)** 429 | 430 | 431 | ## 函数 432 | 433 | ### 合理使用参数默认值,没必要在方法里再做默认值检测 434 | 435 | **不好:** 436 | 437 | 不好,`$breweryName` 可能为 `NULL`. 438 | 439 | ```php 440 | function createMicrobrewery($breweryName = 'Hipster Brew Co.'): void 441 | { 442 |    // ... 443 | } 444 | ``` 445 | 446 | **还行:** 447 | 448 | 比上一个好理解一些,但最好能控制变量的值 449 | 450 | ```php 451 | function createMicrobrewery($name = null): void 452 | { 453 | $breweryName = $name ?: 'Hipster Brew Co.'; 454 | // ... 455 | } 456 | ``` 457 | 458 | **好:** 459 | 460 | 如果你的程序只支持 PHP 7+, 那你可以用 [type hinting](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration) 保证变量 `$breweryName` 不是 `NULL`. 461 | 462 | ```php 463 | function createMicrobrewery(string $breweryName = 'Hipster Brew Co.'): void 464 | { 465 | // ... 466 | } 467 | ``` 468 | 469 | **[⬆ 返回顶部](#目录)** 470 | 471 | ### 函数参数(最好少于2个) 472 | 473 | 限制函数参数个数极其重要,这样测试你的函数容易点。有超过3个可选参数参数导致一个爆炸式组合增长,你会有成吨独立参数情形要测试。 474 | 475 | 无参数是理想情况。1个或2个都可以,最好避免3个。再多就需要加固了。通常如果你的函数有超过两个参数,说明他要处理的事太多了。 如果必须要传入很多数据,建议封装一个高级别对象作为参数。 476 | 477 | **坏:** 478 | 479 | ```php 480 | class Questionnaire 481 | { 482 | public function __construct( 483 | string $firstname, 484 | string $lastname, 485 | string $patronymic, 486 | string $region, 487 | string $district, 488 | string $city, 489 | string $phone, 490 | string $email 491 | ) { 492 | // ... 493 | } 494 | } 495 | ``` 496 | 497 | **好:** 498 | 499 | ```php 500 | class Name 501 | { 502 | private $firstname; 503 | 504 | private $lastname; 505 | 506 | private $patronymic; 507 | 508 | public function __construct(string $firstname, string $lastname, string $patronymic) 509 | { 510 | $this->firstname = $firstname; 511 | $this->lastname = $lastname; 512 | $this->patronymic = $patronymic; 513 | } 514 | 515 | // getters ... 516 | } 517 | 518 | class City 519 | { 520 | private $region; 521 | 522 | private $district; 523 | 524 | private $city; 525 | 526 | public function __construct(string $region, string $district, string $city) 527 | { 528 | $this->region = $region; 529 | $this->district = $district; 530 | $this->city = $city; 531 | } 532 | 533 | // getters ... 534 | } 535 | 536 | class Contact 537 | { 538 | private $phone; 539 | 540 | private $email; 541 | 542 | public function __construct(string $phone, string $email) 543 | { 544 | $this->phone = $phone; 545 | $this->email = $email; 546 | } 547 | 548 | // getters ... 549 | } 550 | 551 | class Questionnaire 552 | { 553 | public function __construct(Name $name, City $city, Contact $contact) 554 | { 555 | // ... 556 | } 557 | } 558 | ``` 559 | 560 | **[⬆ 返回顶部](#目录)** 561 | 562 | ### 函数名应体现他做了什么事 563 | 564 | **坏:** 565 | 566 | ```php 567 | class Email 568 | { 569 | //... 570 | 571 | public function handle(): void 572 | { 573 | mail($this->to, $this->subject, $this->body); 574 | } 575 | } 576 | 577 | $message = new Email(...); 578 | // 啥?handle处理一个消息干嘛了?是往一个文件里写吗? 579 | $message->handle(); 580 | ``` 581 | 582 | **好:** 583 | 584 | ```php 585 | class Email 586 | { 587 | //... 588 | 589 | public function send(): void 590 | { 591 | mail($this->to, $this->subject, $this->body); 592 | } 593 | } 594 | 595 | $message = new Email(...); 596 | // 简单明了 597 | $message->send(); 598 | ``` 599 | 600 | **[⬆ 返回顶部](#目录)** 601 | 602 | ### 函数里应当只有一层抽象abstraction 603 | 604 | 当你抽象层次过多时时,函数处理的事情太多了。需要拆分功能来提高可重用性和易用性,以便简化测试。 605 | (译者注:这里从示例代码看应该是指嵌套过多) 606 | 607 | **坏:** 608 | 609 | ```php 610 | function parseBetterPHPAlternative(string $code): void 611 | { 612 | $regexes = [ 613 | // ... 614 | ]; 615 | 616 | $statements = explode(' ', $code); 617 | $tokens = []; 618 | foreach ($regexes as $regex) { 619 | foreach ($statements as $statement) { 620 | // ... 621 | } 622 | } 623 | 624 | $ast = []; 625 | foreach ($tokens as $token) { 626 | // lex... 627 | } 628 | 629 | foreach ($ast as $node) { 630 | // parse... 631 | } 632 | } 633 | ``` 634 | 635 | **坏:** 636 | 637 | 我们把一些方法从循环中提取出来,但是`parseBetterJSAlternative()`方法还是很复杂,而且不利于测试。 638 | 639 | ```php 640 | function tokenize(string $code): array 641 | { 642 | $regexes = [ 643 | // ... 644 | ]; 645 | 646 | $statements = explode(' ', $code); 647 | $tokens = []; 648 | foreach ($regexes as $regex) { 649 | foreach ($statements as $statement) { 650 | $tokens[] = /* ... */; 651 | } 652 | } 653 | 654 | return $tokens; 655 | } 656 | 657 | function lexer(array $tokens): array 658 | { 659 | $ast = []; 660 | foreach ($tokens as $token) { 661 | $ast[] = /* ... */; 662 | } 663 | 664 | return $ast; 665 | } 666 | 667 | function parseBetterPHPAlternative(string $code): void 668 | { 669 | $tokens = tokenize($code); 670 | $ast = lexer($tokens); 671 | foreach ($ast as $node) { 672 | // 解析逻辑... 673 | } 674 | } 675 | ``` 676 | 677 | **好:** 678 | 679 | 最好的解决方案是把 `parseBetterPHPAlternative()`方法的依赖移除。 680 | 681 | ```php 682 | class Tokenizer 683 | { 684 | public function tokenize(string $code): array 685 | { 686 | $regexes = [ 687 | // ... 688 | ]; 689 | 690 | $statements = explode(' ', $code); 691 | $tokens = []; 692 | foreach ($regexes as $regex) { 693 | foreach ($statements as $statement) { 694 | $tokens[] = /* ... */; 695 | } 696 | } 697 | 698 | return $tokens; 699 | } 700 | } 701 | 702 | class Lexer 703 | { 704 | public function lexify(array $tokens): array 705 | { 706 | $ast = []; 707 | foreach ($tokens as $token) { 708 | $ast[] = /* ... */; 709 | } 710 | 711 | return $ast; 712 | } 713 | } 714 | 715 | class BetterPHPAlternative 716 | { 717 | private $tokenizer; 718 | private $lexer; 719 | 720 | public function __construct(Tokenizer $tokenizer, Lexer $lexer) 721 | { 722 | $this->tokenizer = $tokenizer; 723 | $this->lexer = $lexer; 724 | } 725 | 726 | public function parse(string $code): void 727 | { 728 | $tokens = $this->tokenizer->tokenize($code); 729 | $ast = $this->lexer->lexify($tokens); 730 | foreach ($ast as $node) { 731 | // 解析逻辑... 732 | } 733 | } 734 | } 735 | ``` 736 | 737 | **[⬆ 返回顶部](#目录)** 738 | 739 | ### 不要用flag作为函数的参数 740 | 741 | flag就是在告诉大家,这个方法里处理很多事。前面刚说过,一个函数应当只做一件事。 把不同flag的代码拆分到多个函数里。 742 | 743 | **坏:** 744 | ```php 745 | function createFile(string $name, bool $temp = false): void 746 | { 747 | if ($temp) { 748 | touch('./temp/' . $name); 749 | } else { 750 | touch($name); 751 | } 752 | } 753 | ``` 754 | 755 | **好:** 756 | 757 | ```php 758 | function createFile(string $name): void 759 | { 760 | touch($name); 761 | } 762 | 763 | function createTempFile(string $name): void 764 | { 765 | touch('./temp/' . $name); 766 | } 767 | ``` 768 | **[⬆ 返回顶部](#目录)** 769 | 770 | ### 避免副作用 771 | 772 | 一个函数做了比获取一个值然后返回另外一个值或值们会产生副作用如果。副作用可能是写入一个文件,修改某些全局变量或者偶然的把你全部的钱给了陌生人。 773 | 774 | 现在,你的确需要在一个程序或者场合里要有副作用,像之前的例子,你也许需要写一个文件。你想要做的是把你做这些的地方集中起来。不要用几个函数和类来写入一个特定的文件。用一个服务来做它,一个只有一个。 775 | 776 | 重点是避免常见陷阱比如对象间共享无结构的数据,使用可以写入任何的可变数据类型,不集中处理副作用发生的地方。如果你做了这些你就会比大多数程序员快乐。 777 | 778 | **坏:** 779 | 780 | ```php 781 | // Global variable referenced by following function. 782 | // If we had another function that used this name, now it'd be an array and it could break it. 783 | $name = 'Ryan McDermott'; 784 | 785 | function splitIntoFirstAndLastName(): void 786 | { 787 | global $name; 788 | 789 | $name = explode(' ', $name); 790 | } 791 | 792 | splitIntoFirstAndLastName(); 793 | 794 | var_dump($name); 795 | // ['Ryan', 'McDermott']; 796 | ``` 797 | 798 | **好:** 799 | 800 | ```php 801 | function splitIntoFirstAndLastName(string $name): array 802 | { 803 | return explode(' ', $name); 804 | } 805 | 806 | $name = 'Ryan McDermott'; 807 | $newName = splitIntoFirstAndLastName($name); 808 | 809 | var_dump($name); 810 | // 'Ryan McDermott'; 811 | 812 | var_dump($newName); 813 | // ['Ryan', 'McDermott']; 814 | ``` 815 | 816 | **[⬆ 返回顶部](#目录)** 817 | 818 | ### 不要写全局函数 819 | 在大多数语言中污染全局变量是一个坏的实践,因为你可能和其他类库冲突 820 | 并且调用你api的人直到他们捕获异常才知道踩坑了。让我们思考一种场景: 821 | 如果你想配置一个数组,你可能会写一个全局函数`config()`,但是他可能 822 | 和试着做同样事的其他类库冲突。 823 | 824 | **坏:** 825 | 826 | ```php 827 | function config(): array 828 | { 829 | return [ 830 | 'foo' => 'bar', 831 | ]; 832 | } 833 | ``` 834 | 835 | **好:** 836 | 837 | ```php 838 | class Configuration 839 | { 840 | private $configuration = []; 841 | 842 | public function __construct(array $configuration) 843 | { 844 | $this->configuration = $configuration; 845 | } 846 | 847 | public function get(string $key): ?string 848 | { 849 | // null coalescing operator 850 | return $this->configuration[$key] ?? null; 851 | } 852 | } 853 | ``` 854 | 855 | 加载配置并创建 `Configuration` 类的实例 856 | 857 | ```php 858 | $configuration = new Configuration([ 859 | 'foo' => 'bar', 860 | ]); 861 | ``` 862 | 863 | 现在你必须在程序中用 `Configuration` 的实例了 864 | 865 | **[⬆ 返回顶部](#目录)** 866 | 867 | ### 不要使用单例模式 868 | 869 | 单例是一种 [反模式](https://en.wikipedia.org/wiki/Singleton_pattern). 以下是解释:Paraphrased from Brian Button: 870 | 1. 总是被用成全局实例。They are generally used as a **global instance**, why is that so bad? Because **you hide the dependencies** of your application in your code, instead of exposing them through the interfaces. Making something global to avoid passing it around is a [code smell](https://en.wikipedia.org/wiki/Code_smell). 871 | 2. 违反了[单一响应原则]()They violate the [single responsibility principle](#single-responsibility-principle-srp): by virtue of the fact that **they control their own creation and lifecycle**. 872 | 3. 导致代码强耦合They inherently cause code to be tightly [coupled](https://en.wikipedia.org/wiki/Coupling_%28computer_programming%29). This makes faking them out under **test rather difficult** in many cases. 873 | 4. 在整个程序的生命周期中始终携带状态。They carry state around for the lifetime of the application. Another hit to testing since **you can end up with a situation where tests need to be ordered** which is a big no for unit tests. Why? Because each unit test should be independent from the other. 874 | 875 | 这里有一篇非常好的讨论单例模式的[根本问题((http://misko.hevery.com/2008/08/25/root-cause-of-singletons/)的文章,是[Misko Hevery](http://misko.hevery.com/about/) 写的。 876 | 877 | **坏:** 878 | 879 | ```php 880 | class DBConnection 881 | { 882 | private static $instance; 883 | 884 | private function __construct(string $dsn) 885 | { 886 | // ... 887 | } 888 | 889 | public static function getInstance(): self 890 | { 891 | if (self::$instance === null) { 892 | self::$instance = new self(); 893 | } 894 | 895 | return self::$instance; 896 | } 897 | 898 | // ... 899 | } 900 | 901 | $singleton = DBConnection::getInstance(); 902 | ``` 903 | 904 | **好:** 905 | 906 | ```php 907 | class DBConnection 908 | { 909 | public function __construct(string $dsn) 910 | { 911 | // ... 912 | } 913 | 914 | // ... 915 | } 916 | ``` 917 | 918 | 创建 `DBConnection` 类的实例并通过 [DSN](http://php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters) 配置. 919 | 920 | ```php 921 | $connection = new DBConnection($dsn); 922 | ``` 923 | 924 | 现在你必须在程序中 使用 `DBConnection` 的实例了 925 | 926 | **[⬆ 返回顶部](#目录)** 927 | 928 | ### 封装条件语句 929 | 930 | **坏:** 931 | 932 | ```php 933 | if ($article->state === 'published') { 934 | // ... 935 | } 936 | ``` 937 | 938 | **好:** 939 | 940 | ```php 941 | if ($article->isPublished()) { 942 | // ... 943 | } 944 | ``` 945 | 946 | **[⬆ 返回顶部](#目录)** 947 | 948 | ### 避免用反义条件判断 949 | 950 | **坏:** 951 | 952 | ```php 953 | function isDOMNodeNotPresent(DOMNode $node): bool 954 | { 955 | // ... 956 | } 957 | 958 | if (! isDOMNodeNotPresent($node)) { 959 | // ... 960 | } 961 | ``` 962 | 963 | **好:** 964 | 965 | ```php 966 | function isDOMNodePresent(DOMNode $node): bool 967 | { 968 | // ... 969 | } 970 | 971 | if (isDOMNodePresent($node)) { 972 | // ... 973 | } 974 | ``` 975 | 976 | **[⬆ 返回顶部](#目录)** 977 | 978 | ### 避免条件判断 979 | 980 | 这看起来像一个不可能任务。当人们第一次听到这句话是都会这么说。 981 | "没有`if语句`我还能做啥?" 答案是你可以使用多态来实现多种场景 982 | 的相同任务。第二个问题很常见, “这么做可以,但为什么我要这么做?” 983 | 答案是前面我们学过的一个Clean Code原则:一个函数应当只做一件事。 984 | 当你有很多含有`if`语句的类和函数时,你的函数做了不止一件事。 985 | 记住,只做一件事。 986 | 987 | **坏:** 988 | 989 | ```php 990 | class Airplane 991 | { 992 | // ... 993 | 994 | public function getCruisingAltitude(): int 995 | { 996 | switch ($this->type) { 997 | case '777': 998 | return $this->getMaxAltitude() - $this->getPassengerCount(); 999 | case 'Air Force One': 1000 | return $this->getMaxAltitude(); 1001 | case 'Cessna': 1002 | return $this->getMaxAltitude() - $this->getFuelExpenditure(); 1003 | } 1004 | } 1005 | } 1006 | ``` 1007 | 1008 | **好:** 1009 | 1010 | ```php 1011 | interface Airplane 1012 | { 1013 | // ... 1014 | 1015 | public function getCruisingAltitude(): int; 1016 | } 1017 | 1018 | class Boeing777 implements Airplane 1019 | { 1020 | // ... 1021 | 1022 | public function getCruisingAltitude(): int 1023 | { 1024 | return $this->getMaxAltitude() - $this->getPassengerCount(); 1025 | } 1026 | } 1027 | 1028 | class AirForceOne implements Airplane 1029 | { 1030 | // ... 1031 | 1032 | public function getCruisingAltitude(): int 1033 | { 1034 | return $this->getMaxAltitude(); 1035 | } 1036 | } 1037 | 1038 | class Cessna implements Airplane 1039 | { 1040 | // ... 1041 | 1042 | public function getCruisingAltitude(): int 1043 | { 1044 | return $this->getMaxAltitude() - $this->getFuelExpenditure(); 1045 | } 1046 | } 1047 | ``` 1048 | 1049 | **[⬆ 返回顶部](#目录)** 1050 | 1051 | ### 避免类型检查 (part 1) 1052 | 1053 | PHP是弱类型的,这意味着你的函数可以接收任何类型的参数。 1054 | 有时候你为这自由所痛苦并且在你的函数渐渐尝试类型检查。 1055 | 有很多方法去避免这么做。第一种是统一API。 1056 | 1057 | **坏:** 1058 | 1059 | ```php 1060 | function travelToTexas($vehicle): void 1061 | { 1062 | if ($vehicle instanceof Bicycle) { 1063 | $vehicle->pedalTo(new Location('texas')); 1064 | } elseif ($vehicle instanceof Car) { 1065 | $vehicle->driveTo(new Location('texas')); 1066 | } 1067 | } 1068 | ``` 1069 | 1070 | **好:** 1071 | 1072 | ```php 1073 | function travelToTexas(Vehicle $vehicle): void 1074 | { 1075 | $vehicle->travelTo(new Location('texas')); 1076 | } 1077 | ``` 1078 | 1079 | **[⬆ 返回顶部](#目录)** 1080 | 1081 | ### 避免类型检查 (part 2) 1082 | 1083 | 如果你正使用基本原始值比如字符串、整形和数组,要求版本是PHP 7+,不用多态,需要类型检测, 1084 | 那你应当考虑[类型声明](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration)或者严格模式。 1085 | 提供了基于标准PHP语法的静态类型。 手动检查类型的问题是做好了需要好多的废话,好像为了安全就可以不顾损失可读性。 1086 | 保持你的PHP 整洁,写好测试,做好代码回顾。做不到就用PHP严格类型声明和严格模式来确保安全。 1087 | 1088 | **坏:** 1089 | 1090 | ```php 1091 | function combine($val1, $val2): int 1092 | { 1093 | if (! is_numeric($val1) || ! is_numeric($val2)) { 1094 | throw new Exception('Must be of type Number'); 1095 | } 1096 | 1097 | return $val1 + $val2; 1098 | } 1099 | ``` 1100 | 1101 | **好:** 1102 | 1103 | ```php 1104 | function combine(int $val1, int $val2): int 1105 | { 1106 | return $val1 + $val2; 1107 | } 1108 | ``` 1109 | 1110 | **[⬆ 返回顶部](#目录)** 1111 | 1112 | ### 移除僵尸代码 1113 | 1114 | 僵尸代码和重复代码一样坏。没有理由保留在你的代码库中。如果从来没被调用过,就删掉! 1115 | 因为还在代码版本库里,因此很安全。 1116 | 1117 | **坏:** 1118 | ```php 1119 | function oldRequestModule(string $url): void 1120 | { 1121 | // ... 1122 | } 1123 | 1124 | function newRequestModule(string $url): void 1125 | { 1126 | // ... 1127 | } 1128 | 1129 | $request = newRequestModule($requestUrl); 1130 | inventoryTracker('apples', $request, 'www.inventory-awesome.io'); 1131 | ``` 1132 | 1133 | **好:** 1134 | 1135 | ```php 1136 | function requestModule(string $url): void 1137 | { 1138 | // ... 1139 | } 1140 | 1141 | $request = requestModule($requestUrl); 1142 | inventoryTracker('apples', $request, 'www.inventory-awesome.io'); 1143 | ``` 1144 | 1145 | **[⬆ 返回顶部](#目录)** 1146 | 1147 | 1148 | ## 对象和数据结构 1149 | 1150 | ### 使用 getters 和 setters 1151 | 1152 | 在PHP中你可以对方法使用`public`, `protected`, `private` 来控制对象属性的变更。 1153 | 1154 | * 当你想对对象属性做获取之外的操作时,你不需要在代码中去寻找并修改每一个该属性访问方法 1155 | * 当有`set`对应的属性方法时,易于增加参数的验证 1156 | * 封装内部的表示 1157 | * 使用set*和get*时,易于增加日志和错误控制 1158 | * 继承当前类时,可以复写默认的方法功能 1159 | * 当对象属性是从远端服务器获取时,get*,set*易于使用延迟加载 1160 | 1161 | 此外,这样的方式也符合OOP开发中的[开闭原则](#开闭原则) 1162 | 1163 | **坏:** 1164 | 1165 | ```php 1166 | class BankAccount 1167 | { 1168 | public $balance = 1000; 1169 | } 1170 | 1171 | $bankAccount = new BankAccount(); 1172 | 1173 | // Buy shoes... 1174 | $bankAccount->balance -= 100; 1175 | ``` 1176 | 1177 | **好:** 1178 | 1179 | ```php 1180 | class BankAccount 1181 | { 1182 | private $balance; 1183 | 1184 | public function __construct(int $balance = 1000) 1185 | { 1186 | $this->balance = $balance; 1187 | } 1188 | 1189 | public function withdraw(int $amount): void 1190 | { 1191 | if ($amount > $this->balance) { 1192 | throw new \Exception('Amount greater than available balance.'); 1193 | } 1194 | 1195 | $this->balance -= $amount; 1196 | } 1197 | 1198 | public function deposit(int $amount): void 1199 | { 1200 | $this->balance += $amount; 1201 | } 1202 | 1203 |    public function getBalance(): int 1204 | { 1205 | return $this->balance; 1206 | } 1207 | } 1208 | 1209 | $bankAccount = new BankAccount(); 1210 | 1211 | // Buy shoes... 1212 | $bankAccount->withdraw($shoesPrice); 1213 | 1214 | // Get balance 1215 | $balance = $bankAccount->getBalance(); 1216 | ``` 1217 | 1218 | **[⬆ 返回顶部](#目录)** 1219 | 1220 | ### 给对象使用私有或受保护的成员变量 1221 | 1222 | * 对`public`方法和属性进行修改非常危险,因为外部代码容易依赖他,而你没办法控制。**对之修改影响所有这个类的使用者。** `public` methods and properties are most dangerous for changes, because some outside code may easily rely on them and you can't control what code relies on them. **Modifications in class are dangerous for all users of class.** 1223 | * 对`protected`的修改跟对`public`修改差不多危险,因为他们对子类可用,他俩的唯一区别就是可调用的位置不一样,**对之修改影响所有集成这个类的地方。** `protected` modifier are as dangerous as public, because they are available in scope of any child class. This effectively means that difference between public and protected is only in access mechanism, but encapsulation guarantee remains the same. **Modifications in class are dangerous for all descendant classes.** 1224 | * 对`private`的修改保证了这部分代码**只会影响当前类**`private` modifier guarantees that code is **dangerous to modify only in boundaries of single class** (you are safe for modifications and you won't have [Jenga effect](http://www.urbandictionary.com/define.php?term=Jengaphobia&defid=2494196)). 1225 | 1226 | 所以,当你需要控制类里的代码可以被访问时才用`public/protected`,其他时候都用`private`。 1227 | 1228 | 可以读一读这篇 [博客文章](http://fabien.potencier.org/pragmatism-over-theory-protected-vs-private.html) ,[Fabien Potencier](https://github.com/fabpot)写的. 1229 | 1230 | **坏:** 1231 | 1232 | ```php 1233 | class Employee 1234 | { 1235 | public $name; 1236 | 1237 | public function __construct(string $name) 1238 | { 1239 | $this->name = $name; 1240 | } 1241 | } 1242 | 1243 | $employee = new Employee('John Doe'); 1244 | // Employee name: John Doe 1245 | echo 'Employee name: ' . $employee->name; 1246 | ``` 1247 | 1248 | **好:** 1249 | 1250 | ```php 1251 | class Employee 1252 | { 1253 | private $name; 1254 | 1255 | public function __construct(string $name) 1256 | { 1257 | $this->name = $name; 1258 | } 1259 | 1260 | public function getName(): string 1261 | { 1262 | return $this->name; 1263 | } 1264 | } 1265 | 1266 | $employee = new Employee('John Doe'); 1267 | // Employee name: John Doe 1268 | echo 'Employee name: ' . $employee->getName(); 1269 | ``` 1270 | 1271 | **[⬆ 返回顶部](#目录)** 1272 | 1273 | ## 类 1274 | 1275 | ### 少用继承多用组合 1276 | 1277 | 正如 the Gang of Four 所著的[*设计模式*](https://en.wikipedia.org/wiki/Design_Patterns)之前所说, 1278 | 我们应该尽量优先选择组合而不是继承的方式。使用继承和组合都有很多好处。 1279 | 这个准则的主要意义在于当你本能的使用继承时,试着思考一下`组合`是否能更好对你的需求建模。 1280 | 在一些情况下,是这样的。 1281 | 1282 | 接下来你或许会想,“那我应该在什么时候使用继承?” 1283 | 答案依赖于你的问题,当然下面有一些何时继承比组合更好的说明: 1284 | 1285 | 1. 你的继承表达了“是一个”而不是“有一个”的关系(人类-》动物,用户-》用户详情) 1286 | 2. 你可以复用基类的代码(人类可以像动物一样移动) 1287 | 3. 你想通过修改基类对所有派生类做全局的修改(当动物移动时,修改她们的能量消耗) 1288 | 1289 | **糟糕的:** 1290 | 1291 | ```php 1292 | class Employee 1293 | { 1294 | private $name; 1295 | 1296 | private $email; 1297 | 1298 | public function __construct(string $name, string $email) 1299 | { 1300 | $this->name = $name; 1301 | $this->email = $email; 1302 | } 1303 | 1304 | // ... 1305 | } 1306 | 1307 | 1308 | // 不好,因为 Employees "有" taxdata 1309 | // 而 EmployeeTaxData 不是 Employee 类型的 1310 | 1311 | 1312 | class EmployeeTaxData extends Employee 1313 | { 1314 | private $ssn; 1315 | 1316 | private $salary; 1317 | 1318 | public function __construct(string $name, string $email, string $ssn, string $salary) 1319 | { 1320 | parent::__construct($name, $email); 1321 | 1322 | $this->ssn = $ssn; 1323 | $this->salary = $salary; 1324 | } 1325 | 1326 | // ... 1327 | } 1328 | ``` 1329 | 1330 | **好:** 1331 | 1332 | ```php 1333 | class EmployeeTaxData 1334 | { 1335 | private $ssn; 1336 | 1337 | private $salary; 1338 | 1339 | public function __construct(string $ssn, string $salary) 1340 | { 1341 | $this->ssn = $ssn; 1342 | $this->salary = $salary; 1343 | } 1344 | 1345 | // ... 1346 | } 1347 | 1348 | class Employee 1349 | { 1350 | private $name; 1351 | 1352 | private $email; 1353 | 1354 | private $taxData; 1355 | 1356 | public function __construct(string $name, string $email) 1357 | { 1358 | $this->name = $name; 1359 | $this->email = $email; 1360 | } 1361 | 1362 | public function setTaxData(EmployeeTaxData $taxData): void 1363 | { 1364 | $this->taxData = $taxData; 1365 | } 1366 | 1367 | // ... 1368 | } 1369 | ``` 1370 | 1371 | **[⬆ 返回顶部](#目录)** 1372 | 1373 | ### 避免连贯接口 1374 | 1375 | [连贯接口Fluent interface](https://en.wikipedia.org/wiki/Fluent_interface)是一种 1376 | 旨在提高面向对象编程时代码可读性的API设计模式,他基于[方法链Method chaining](https://en.wikipedia.org/wiki/Method_chaining) 1377 | 1378 | 有上下文的地方可以降低代码复杂度,例如[PHPUnit Mock Builder](https://phpunit.de/manual/current/en/test-doubles.html) 1379 | 和[Doctrine Query Builder](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/query-builder.html) 1380 | ,更多的情况会带来较大代价: 1381 | 1382 | While there can be some contexts, frequently builder objects, where this 1383 | pattern reduces the verbosity of the code (for example the [PHPUnit Mock Builder](https://phpunit.de/manual/current/en/test-doubles.html) 1384 | or the [Doctrine Query Builder](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/query-builder.html)), 1385 | more often it comes at some costs: 1386 | 1387 | 1. 破坏了 [对象封装](https://en.wikipedia.org/wiki/Encapsulation_%28object-oriented_programming%29) 1388 | 2. 破坏了 [装饰器模式](https://en.wikipedia.org/wiki/Decorator_pattern) 1389 | 3. 在测试组件中不好做[mock](https://en.wikipedia.org/wiki/Mock_object) 1390 | 4. 导致提交的diff不好阅读 1391 | 1392 | 了解更多请阅读 [连贯接口为什么不好](https://ocramius.github.io/blog/fluent-interfaces-are-evil/) 1393 | ,作者 [Marco Pivetta](https://github.com/Ocramius). 1394 | 1395 | **坏:** 1396 | 1397 | ```php 1398 | class Car 1399 | { 1400 | private $make = 'Honda'; 1401 | 1402 | private $model = 'Accord'; 1403 | 1404 | private $color = 'white'; 1405 | 1406 | public function setMake(string $make): self 1407 | { 1408 | $this->make = $make; 1409 | 1410 | // NOTE: Returning this for chaining 1411 | return $this; 1412 | } 1413 | 1414 | public function setModel(string $model): self 1415 | { 1416 | $this->model = $model; 1417 | 1418 | // NOTE: Returning this for chaining 1419 | return $this; 1420 | } 1421 | 1422 | public function setColor(string $color): self 1423 | { 1424 | $this->color = $color; 1425 | 1426 | // NOTE: Returning this for chaining 1427 | return $this; 1428 | } 1429 | 1430 | public function dump(): void 1431 | { 1432 | var_dump($this->make, $this->model, $this->color); 1433 | } 1434 | } 1435 | 1436 | $car = (new Car()) 1437 | ->setColor('pink') 1438 | ->setMake('Ford') 1439 | ->setModel('F-150') 1440 | ->dump(); 1441 | ``` 1442 | 1443 | **好:** 1444 | 1445 | ```php 1446 | class Car 1447 | { 1448 | private $make = 'Honda'; 1449 | 1450 | private $model = 'Accord'; 1451 | 1452 | private $color = 'white'; 1453 | 1454 | public function setMake(string $make): void 1455 | { 1456 | $this->make = $make; 1457 | } 1458 | 1459 | public function setModel(string $model): void 1460 | { 1461 | $this->model = $model; 1462 | } 1463 | 1464 | public function setColor(string $color): void 1465 | { 1466 | $this->color = $color; 1467 | } 1468 | 1469 | public function dump(): void 1470 | { 1471 | var_dump($this->make, $this->model, $this->color); 1472 | } 1473 | } 1474 | 1475 | $car = new Car(); 1476 | $car->setColor('pink'); 1477 | $car->setMake('Ford'); 1478 | $car->setModel('F-150'); 1479 | $car->dump(); 1480 | ``` 1481 | 1482 | **[⬆ 返回顶部](#目录)** 1483 | 1484 | ### 推荐使用 final 类 1485 | 1486 | 能用时尽量使用 `final` 关键字: 1487 | 1488 | 1. 阻止不受控的继承链 1489 | 2. 鼓励 [组合](#少用继承多用组合). 1490 | 3. 鼓励 [单一职责模式](#单一职责模式). 1491 | 4. 鼓励开发者用你的公开方法而非通过继承类获取受保护方法的访问权限. 1492 | 5. 使得在不破坏使用你的类的应用的情况下修改代码成为可能. 1493 | 1494 | The only condition is that your class should implement an interface and no other public methods are defined. 1495 | 1496 | For more informations you can read [the blog post](https://ocramius.github.io/blog/when-to-declare-classes-final/) on this topic written by [Marco Pivetta (Ocramius)](https://ocramius.github.io/). 1497 | 1498 | **Bad:** 1499 | 1500 | ```php 1501 | final class Car 1502 | { 1503 | private $color; 1504 | 1505 | public function __construct($color) 1506 | { 1507 | $this->color = $color; 1508 | } 1509 | 1510 | /** 1511 | * @return string The color of the vehicle 1512 | */ 1513 | public function getColor() 1514 | { 1515 | return $this->color; 1516 | } 1517 | } 1518 | ``` 1519 | 1520 | **Good:** 1521 | 1522 | ```php 1523 | interface Vehicle 1524 | { 1525 | /** 1526 | * @return string The color of the vehicle 1527 | */ 1528 | public function getColor(); 1529 | } 1530 | 1531 | final class Car implements Vehicle 1532 | { 1533 | private $color; 1534 | 1535 | public function __construct($color) 1536 | { 1537 | $this->color = $color; 1538 | } 1539 | 1540 | public function getColor() 1541 | { 1542 | return $this->color; 1543 | } 1544 | } 1545 | ``` 1546 | 1547 | **[⬆ 返回顶部](#目录)** 1548 | 1549 | ## SOLID 1550 | 1551 | **SOLID** 是Michael Feathers推荐的便于记忆的首字母简写,它代表了Robert Martin命名的最重要的五个面对对象编码设计原则 1552 | 1553 | * [S: 单一职责原则 (SRP)](#职责原则) 1554 | * [O: 开闭原则 (OCP)](#开闭原则) 1555 | * [L: 里氏替换原则 (LSP)](#里氏替换原则) 1556 | * [I: 接口隔离原则 (ISP)](#接口隔离原则) 1557 | * [D: 依赖倒置原则 (DIP)](#依赖倒置原则) 1558 | 1559 | 1560 | ### 单一职责原则 1561 | 1562 | Single Responsibility Principle (SRP) 1563 | 1564 | 正如在Clean Code所述,"修改一个类应该只为一个理由"。 1565 | 人们总是易于用一堆方法塞满一个类,如同我们只能在飞机上 1566 | 只能携带一个行李箱(把所有的东西都塞到箱子里)。这样做 1567 | 的问题是:从概念上这样的类不是高内聚的,并且留下了很多 1568 | 理由去修改它。将你需要修改类的次数降低到最小很重要。 1569 | 这是因为,当有很多方法在类中时,修改其中一处,你很难知 1570 | 晓在代码库中哪些依赖的模块会被影响到。 1571 | 1572 | **坏:** 1573 | 1574 | ```php 1575 | class UserSettings 1576 | { 1577 | private $user; 1578 | 1579 | public function __construct(User $user) 1580 | { 1581 | $this->user = $user; 1582 | } 1583 | 1584 | public function changeSettings(array $settings): void 1585 | { 1586 | if ($this->verifyCredentials()) { 1587 | // ... 1588 | } 1589 | } 1590 | 1591 | private function verifyCredentials(): bool 1592 | { 1593 | // ... 1594 | } 1595 | } 1596 | ``` 1597 | 1598 | **好:** 1599 | 1600 | ```php 1601 | class UserAuth 1602 | { 1603 | private $user; 1604 | 1605 | public function __construct(User $user) 1606 | { 1607 | $this->user = $user; 1608 | } 1609 | 1610 | public function verifyCredentials(): bool 1611 | { 1612 | // ... 1613 | } 1614 | } 1615 | 1616 | class UserSettings 1617 | { 1618 | private $user; 1619 | 1620 | private $auth; 1621 | 1622 | public function __construct(User $user) 1623 | { 1624 | $this->user = $user; 1625 | $this->auth = new UserAuth($user); 1626 | } 1627 | 1628 | public function changeSettings(array $settings): void 1629 | { 1630 | if ($this->auth->verifyCredentials()) { 1631 | // ... 1632 | } 1633 | } 1634 | } 1635 | ``` 1636 | 1637 | **[⬆ 返回顶部](#目录)** 1638 | 1639 | ### 开闭原则 1640 | 1641 | Open/Closed Principle (OCP) 1642 | 1643 | 正如Bertrand Meyer所述,"软件的工件( classes, modules, functions 等) 1644 | 应该对扩展开放,对修改关闭。" 然而这句话意味着什么呢?这个原则大体上表示你 1645 | 应该允许在不改变已有代码的情况下增加新的功能 1646 | 1647 | **坏:** 1648 | 1649 | ```php 1650 | abstract class Adapter 1651 | { 1652 | protected $name; 1653 | 1654 | public function getName(): string 1655 | { 1656 | return $this->name; 1657 | } 1658 | } 1659 | 1660 | class AjaxAdapter extends Adapter 1661 | { 1662 | public function __construct() 1663 | { 1664 | parent::__construct(); 1665 | 1666 | $this->name = 'ajaxAdapter'; 1667 | } 1668 | } 1669 | 1670 | class NodeAdapter extends Adapter 1671 | { 1672 | public function __construct() 1673 | { 1674 | parent::__construct(); 1675 | 1676 | $this->name = 'nodeAdapter'; 1677 | } 1678 | } 1679 | 1680 | class HttpRequester 1681 | { 1682 | private $adapter; 1683 | 1684 | public function __construct(Adapter $adapter) 1685 | { 1686 | $this->adapter = $adapter; 1687 | } 1688 | 1689 | public function fetch(string $url): Promise 1690 | { 1691 | $adapterName = $this->adapter->getName(); 1692 | 1693 | if ($adapterName === 'ajaxAdapter') { 1694 | return $this->makeAjaxCall($url); 1695 | } elseif ($adapterName === 'httpNodeAdapter') { 1696 | return $this->makeHttpCall($url); 1697 | } 1698 | } 1699 | 1700 | private function makeAjaxCall(string $url): Promise 1701 | { 1702 | // request and return promise 1703 | } 1704 | 1705 | private function makeHttpCall(string $url): Promise 1706 | { 1707 | // request and return promise 1708 | } 1709 | } 1710 | ``` 1711 | 1712 | **好:** 1713 | 1714 | ```php 1715 | interface Adapter 1716 | { 1717 | public function request(string $url): Promise; 1718 | } 1719 | 1720 | class AjaxAdapter implements Adapter 1721 | { 1722 | public function request(string $url): Promise 1723 | { 1724 | // request and return promise 1725 | } 1726 | } 1727 | 1728 | class NodeAdapter implements Adapter 1729 | { 1730 | public function request(string $url): Promise 1731 | { 1732 | // request and return promise 1733 | } 1734 | } 1735 | 1736 | class HttpRequester 1737 | { 1738 | private $adapter; 1739 | 1740 | public function __construct(Adapter $adapter) 1741 | { 1742 | $this->adapter = $adapter; 1743 | } 1744 | 1745 | public function fetch(string $url): Promise 1746 | { 1747 | return $this->adapter->request($url); 1748 | } 1749 | } 1750 | ``` 1751 | 1752 | **[⬆ 返回顶部](#目录)** 1753 | 1754 | ### 里氏替换原则 1755 | 1756 | Liskov Substitution Principle (LSP) 1757 | 1758 | 这是一个简单的原则,却用了一个不好理解的术语。它的正式定义是 1759 | "如果S是T的子类型,那么在不改变程序原有既定属性(检查、执行 1760 | 任务等)的前提下,任何T类型的对象都可以使用S类型的对象替代 1761 | (例如,使用S的对象可以替代T的对象)" 这个定义更难理解:-)。 1762 | 1763 | 对这个概念最好的解释是:如果你有一个父类和一个子类,在不改变 1764 | 原有结果正确性的前提下父类和子类可以互换。这个听起来依旧让人 1765 | 有些迷惑,所以让我们来看一个经典的正方形-长方形的例子。从数学 1766 | 上讲,正方形是一种长方形,但是当你的模型通过继承使用了"is-a" 1767 | 的关系时,就不对了。 1768 | 1769 | **坏:** 1770 | 1771 | ```php 1772 | class Rectangle 1773 | { 1774 | protected $width = 0; 1775 | 1776 | protected $height = 0; 1777 | 1778 | public function setWidth(int $width): void 1779 | { 1780 | $this->width = $width; 1781 | } 1782 | 1783 | public function setHeight(int $height): void 1784 | { 1785 | $this->height = $height; 1786 | } 1787 | 1788 | public function getArea(): int 1789 | { 1790 | return $this->width * $this->height; 1791 | } 1792 | } 1793 | 1794 | class Square extends Rectangle 1795 | { 1796 | public function setWidth(int $width): void 1797 | { 1798 | $this->width = $this->height = $width; 1799 | } 1800 | 1801 | public function setHeight(int $height): void 1802 | { 1803 | $this->width = $this->height = $height; 1804 | } 1805 | } 1806 | 1807 | function printArea(Rectangle $rectangle): void 1808 | { 1809 | $rectangle->setWidth(4); 1810 | $rectangle->setHeight(5); 1811 | 1812 | // BAD: Will return 25 for Square. Should be 20. 1813 | echo sprintf('%s has area %d.', get_class($rectangle), $rectangle->getArea()) . PHP_EOL; 1814 | } 1815 | 1816 | $rectangles = [new Rectangle(), new Square()]; 1817 | 1818 | foreach ($rectangles as $rectangle) { 1819 | printArea($rectangle); 1820 | } 1821 | ``` 1822 | 1823 | **好:** 1824 | 1825 | 最好是将这两种四边形分别对待,用一个适合两种类型的更通用子类型来代替。 1826 | 1827 | 尽管正方形和长方形看起来很相似,但他们是不同的。 1828 | 正方形更接近菱形,而长方形更接近平行四边形。但他们不是子类型。 1829 | 尽管相似,正方形、长方形、菱形、平行四边形都是有自己属性的不同形状。 1830 | 1831 | ```php 1832 | interface Shape 1833 | { 1834 | public function getArea(): int; 1835 | } 1836 | 1837 | class Rectangle implements Shape 1838 | { 1839 | private $width = 0; 1840 | private $height = 0; 1841 | 1842 | public function __construct(int $width, int $height) 1843 | { 1844 | $this->width = $width; 1845 | $this->height = $height; 1846 | } 1847 | 1848 | public function getArea(): int 1849 | { 1850 | return $this->width * $this->height; 1851 | } 1852 | } 1853 | 1854 | class Square implements Shape 1855 | { 1856 | private $length = 0; 1857 | 1858 | public function __construct(int $length) 1859 | { 1860 | $this->length = $length; 1861 | } 1862 | 1863 | public function getArea(): int 1864 | { 1865 |        return $this->length ** 2; 1866 |    } 1867 | } 1868 | 1869 | function printArea(Shape $shape): void 1870 | { 1871 | echo sprintf('%s has area %d.', get_class($shape), $shape->getArea()).PHP_EOL; 1872 | } 1873 | 1874 | $shapes = [new Rectangle(4, 5), new Square(5)]; 1875 | 1876 | foreach ($shapes as $shape) { 1877 | printArea($shape); 1878 | } 1879 | ``` 1880 | 1881 | **[⬆ 返回顶部](#目录)** 1882 | 1883 | ### 接口隔离原则 1884 | 1885 | Interface Segregation Principle (ISP) 1886 | 1887 | 接口隔离原则表示:"调用方不应该被强制依赖于他不需要的接口" 1888 | 1889 | 有一个清晰的例子来说明示范这条原则。当一个类需要一个大量的设置项, 1890 | 为了方便不会要求调用方去设置大量的选项,因为在通常他们不需要所有的 1891 | 设置项。使设置项可选有助于我们避免产生"胖接口" 1892 | 1893 | **坏:** 1894 | 1895 | ```php 1896 | interface Employee 1897 | { 1898 | public function work(): void; 1899 | 1900 | public function eat(): void; 1901 | } 1902 | 1903 | class HumanEmployee implements Employee 1904 | { 1905 | public function work(): void 1906 | { 1907 | // ....working 1908 | } 1909 | 1910 | public function eat(): void 1911 | { 1912 | // ...... eating in lunch break 1913 | } 1914 | } 1915 | 1916 | class RobotEmployee implements Employee 1917 | { 1918 | public function work(): void 1919 | { 1920 | //.... working much more 1921 | } 1922 | 1923 | public function eat(): void 1924 | { 1925 | //.... robot can't eat, but it must implement this method 1926 | } 1927 | } 1928 | ``` 1929 | 1930 | **好:** 1931 | 1932 | 不是每一个工人都是雇员,但是每一个雇员都是一个工人 1933 | 1934 | ```php 1935 | interface Workable 1936 | { 1937 | public function work(): void; 1938 | } 1939 | 1940 | interface Feedable 1941 | { 1942 | public function eat(): void; 1943 | } 1944 | 1945 | interface Employee extends Feedable, Workable 1946 | { 1947 | } 1948 | 1949 | class HumanEmployee implements Employee 1950 | { 1951 | public function work(): void 1952 | { 1953 | // ....working 1954 | } 1955 | 1956 | public function eat(): void 1957 | { 1958 | //.... eating in lunch break 1959 | } 1960 | } 1961 | 1962 | // robot can only work 1963 | class RobotEmployee implements Workable 1964 | { 1965 | public function work(): void 1966 | { 1967 | // ....working 1968 | } 1969 | } 1970 | ``` 1971 | 1972 | **[⬆ 返回顶部](#目录)** 1973 | 1974 | ### 依赖倒置原则 1975 | 1976 | Dependency Inversion Principle (DIP) 1977 | 1978 | 这条原则说明两个基本的要点: 1979 | 1. 高阶的模块不应该依赖低阶的模块,它们都应该依赖于抽象 1980 | 2. 抽象不应该依赖于实现,实现应该依赖于抽象 1981 | 1982 | 这条起初看起来有点晦涩难懂,但是如果你使用过 PHP 框架(例如 Symfony),你应该见过 1983 | 依赖注入(DI),它是对这个概念的实现。虽然它们不是完全相等的概念,依赖倒置原则使高阶模块 1984 | 与低阶模块的实现细节和创建分离。可以使用依赖注入(DI)这种方式来实现它。最大的好处 1985 | 是它使模块之间解耦。耦合会导致你难于重构,它是一种非常糟糕的的开发模式。 1986 | 1987 | **坏:** 1988 | 1989 | ```php 1990 | class Employee 1991 | { 1992 | public function work(): void 1993 | { 1994 | // ....working 1995 | } 1996 | } 1997 | 1998 | class Robot extends Employee 1999 | { 2000 | public function work(): void 2001 | { 2002 | //.... working much more 2003 | } 2004 | } 2005 | 2006 | class Manager 2007 | { 2008 | private $employee; 2009 | 2010 | public function __construct(Employee $employee) 2011 | { 2012 | $this->employee = $employee; 2013 | } 2014 | 2015 | public function manage(): void 2016 | { 2017 | $this->employee->work(); 2018 | } 2019 | } 2020 | ``` 2021 | 2022 | **好:** 2023 | 2024 | ```php 2025 | interface Employee 2026 | { 2027 | public function work(): void; 2028 | } 2029 | 2030 | class Human implements Employee 2031 | { 2032 | public function work(): void 2033 | { 2034 | // ....working 2035 | } 2036 | } 2037 | 2038 | class Robot implements Employee 2039 | { 2040 | public function work(): void 2041 | { 2042 | //.... working much more 2043 | } 2044 | } 2045 | 2046 | class Manager 2047 | { 2048 | private $employee; 2049 | 2050 | public function __construct(Employee $employee) 2051 | { 2052 | $this->employee = $employee; 2053 | } 2054 | 2055 | public function manage(): void 2056 | { 2057 | $this->employee->work(); 2058 | } 2059 | } 2060 | ``` 2061 | 2062 | **[⬆ 返回顶部](#目录)** 2063 | 2064 | ## 别写重复代码 (DRY) 2065 | 2066 | 试着去遵循[DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) 原则. 2067 | 2068 | 尽你最大的努力去避免复制代码,它是一种非常糟糕的行为,复制代码 2069 | 通常意味着当你需要变更一些逻辑时,你需要修改不止一处。 2070 | 2071 | 试想一下,如果你在经营一家餐厅并且你在记录你仓库的进销记录:所有 2072 | 的土豆,洋葱,大蒜,辣椒等。如果你有多个列表来管理进销记录,当你 2073 | 用其中一些土豆做菜时你需要更新所有的列表。如果你只有一个列表的话 2074 | 只有一个地方需要更新。 2075 | 2076 | 通常情况下你复制代码是应该有两个或者多个略微不同的逻辑,它们大多数 2077 | 都是一样的,但是由于它们的区别致使你必须有两个或者多个隔离的但大部 2078 | 分相同的方法,移除重复的代码意味着用一个function/module/class创 2079 | 建一个能处理差异的抽象。 2080 | 2081 | 用对抽象非常关键,这正是为什么你必须学习遵守在[类](#类)章节写 2082 | 的SOLID原则,不合理的抽象比复制代码更糟糕,所以务必谨慎!说了这么多, 2083 | 如果你能设计一个合理的抽象,那就这么干!别写重复代码,否则你会发现 2084 | 任何时候当你想修改一个逻辑时你必须修改多个地方。 2085 | 2086 | **坏:** 2087 | 2088 | ```php 2089 | function showDeveloperList(array $developers): void 2090 | { 2091 | foreach ($developers as $developer) { 2092 | $expectedSalary = $developer->calculateExpectedSalary(); 2093 | $experience = $developer->getExperience(); 2094 | $githubLink = $developer->getGithubLink(); 2095 | $data = [$expectedSalary, $experience, $githubLink]; 2096 | 2097 | render($data); 2098 | } 2099 | } 2100 | 2101 | function showManagerList(array $managers): void 2102 | { 2103 | foreach ($managers as $manager) { 2104 | $expectedSalary = $manager->calculateExpectedSalary(); 2105 | $experience = $manager->getExperience(); 2106 | $githubLink = $manager->getGithubLink(); 2107 | $data = [$expectedSalary, $experience, $githubLink]; 2108 | 2109 | render($data); 2110 | } 2111 | } 2112 | ``` 2113 | 2114 | **好:** 2115 | 2116 | ```php 2117 | function showList(array $employees): void 2118 | { 2119 | foreach ($employees as $employee) { 2120 | $expectedSalary = $employee->calculateExpectedSalary(); 2121 | $experience = $employee->getExperience(); 2122 | $githubLink = $employee->getGithubLink(); 2123 | $data = [$expectedSalary, $experience, $githubLink]; 2124 | 2125 | render($data); 2126 | } 2127 | } 2128 | ``` 2129 | 2130 | **极好:** 2131 | 2132 | 最好让代码紧凑一点 2133 | 2134 | ```php 2135 | function showList(array $employees): void 2136 | { 2137 | foreach ($employees as $employee) { 2138 | render([$employee->calculateExpectedSalary(), $employee->getExperience(), $employee->getGithubLink()]); 2139 | } 2140 | } 2141 | ``` 2142 | 2143 | **[⬆ 返回顶部](#目录)** 2144 | 2145 | ## 翻译 2146 | 2147 | 其他语言的翻译: 2148 | 2149 | * :cn: **Chinese:** 2150 | * [php-cpm/clean-code-php](https://github.com/php-cpm/clean-code-php) 2151 | * :ru: **Russian:** 2152 | * [peter-gribanov/clean-code-php](https://github.com/peter-gribanov/clean-code-php) 2153 | * :es: **Spanish:** 2154 | * [fikoborquez/clean-code-php](https://github.com/fikoborquez/clean-code-php) 2155 | * :brazil: **Portuguese:** 2156 | * [fabioars/clean-code-php](https://github.com/fabioars/clean-code-php) 2157 | * [jeanjar/clean-code-php](https://github.com/jeanjar/clean-code-php/tree/pt-br) 2158 | * :thailand: **Thai:** 2159 | * [panuwizzle/clean-code-php](https://github.com/panuwizzle/clean-code-php) 2160 | * :fr: **French:** 2161 | * [errorname/clean-code-php](https://github.com/errorname/clean-code-php) 2162 | * :vietnam: **Vietnamese:** 2163 | * [viethuongdev/clean-code-php](https://github.com/viethuongdev/clean-code-php) 2164 | * :kr: **Korean:** 2165 | * [yujineeee/clean-code-php](https://github.com/yujineeee/clean-code-php) 2166 | * :tr: **Turkish:** 2167 | * [anilozmen/clean-code-php](https://github.com/anilozmen/clean-code-php) 2168 | * :iran: **Persian:** 2169 | * [amirshnll/clean-code-php](https://github.com/amirshnll/clean-code-php) 2170 | * :bangladesh: **Bangla:** 2171 | * [nayeemdev/clean-code-php](https://github.com/nayeemdev/clean-code-php) 2172 | 2173 | **[⬆ 返回顶部](#目录)** 2174 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupeter/clean-code-php", 3 | "description": "Clean Code concepts adapted for PHP", 4 | "require": { 5 | "php": ">=7.2", 6 | "symplify/easy-coding-standard": "^9.3" 7 | }, 8 | "scripts": { 9 | "check-cs": "vendor/bin/ecs check-markdown README.md", 10 | "fix-cs": "vendor/bin/ecs check-markdown README.md --fix" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | import(SetList::COMMON); 15 | $containerConfigurator->import(SetList::CLEAN_CODE); 16 | $containerConfigurator->import(SetList::PSR_12); 17 | $containerConfigurator->import(SetList::SYMPLIFY); 18 | 19 | $parameters = $containerConfigurator->parameters(); 20 | $parameters->set(Option::PATHS, [__DIR__ . '/src', __DIR__ . '/config', __DIR__ . '/ecs.php']); 21 | 22 | $parameters->set(Option::SKIP, [ 23 | BlankLineAfterOpeningTagFixer::class => null, 24 | StrictComparisonFixer::class => null, 25 | DeclareStrictTypesFixer::class => null, 26 | ]); 27 | }; 28 | --------------------------------------------------------------------------------