これらは、Robert MartinのClean Codeブックから引用され、PHPに適合したソフトウェア開発の原則です。 このガイドは、プログラミングスタイルについてではなく、読み取り、再利用、およびリファクタリングのPHPコードの作成について説明しています。
これらの原則のそれぞれを厳密に遵守する必要はありませんが、少数では全員が同意します。 これらは単なる推奨事項ではなく、 Clean Codeの著者の長年にわたる集団的経験の中で成文化されたものです。
この記事はclean-code-javascriptに触発されています。
内容
- 変数
- 機能
- オブジェクトとデータ構造
- クラス
- S:単一責任原則(SRP)
- O:オープン/クローズドプリンシパル(OCP)
- L:バーバラ・リスコフ代替原理(LSP)
- I:インターフェイス分離の原則(ISP)
- D:依存関係反転の原理(DIP)
変数
意味のある話し言葉の変数名を使用する
悪い:
$ymdstr = $moment->format('y-m-d');
:
$currentDate = $moment->format('y-m-d');
:
getUserInfo(); getClientData(); getCustomerRecord();
:
getUser();
,
, - . , . , , . , .
:
// What the heck is 86400 for? addExpireAt(86400);
:
// Declare them as capitalized `const` globals. interface DateGlobal { const SECONDS_IN_A_DAY = 86400; } addExpireAt(DateGlobal::SECONDS_IN_A_DAY);
:
$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); list(, $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']);
, , . , .
:
$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); });
/ - , .
:
$car = [ 'carMake' => 'Honda', 'carModel' => 'Accord', 'carColor' => 'Blue', ]; function paintCar(&$car) { $car['carColor'] = 'Red'; }
:
$car = [ 'make' => 'Honda', 'model' => 'Accord', 'color' => 'Blue', ]; function paintCar(&$car) { $car['color'] = 'Red'; }
:
function createMicrobrewery($name = null) { $breweryName = $name ?: 'Hipster Brew Co.'; // ... }
:
function createMicrobrewery($breweryName = 'Hipster Brew Co.') { // ... }
( )
, . « », .
— . - , . , , . , . , , .
:
function createMenu($title, $body, $buttonText, $cancellable) { // ... }
:
class MenuConfig { public $title; public $body; public $buttonText; public $cancelLabel = false; } $config = new MenuConfig(); $config->title = 'Foo'; $config->body = 'Bar'; $config->buttonText = 'Baz'; $config->cancelLabel = true; function createMenu(MenuConfig $config) { // ... }
-
, , . , , . - , , . , , .
:
function emailClients($clients) { foreach ($clients as $client) { $clientRecord = $db->find($client); if ($clientRecord->isActive()) { email($client); } } }
:
function emailClients($clients) { $activeClients = activeClients($clients); array_walk($activeClients, 'email'); } function activeClients($clients) { return array_filter($clients, 'isClientActive'); } function isClientActive($client) { $clientRecord = $db->find($client); return $clientRecord->isActive(); }
:
function addToDate($date, $month) { // ... } $date = new \DateTime(); // It's hard to tell from the function name what is added addToDate($date, 1);
:
function addMonthToDate($month, $date) { // ... } $date = new \DateTime(); addMonthToDate(1, $date);
, . .
:
function parseBetterJSAlternative($code) { $regexes = [ // ... ]; $statements = split(' ', $code); $tokens = []; foreach($regexes as $regex) { foreach($statements as $statement) { // ... } } $ast = []; foreach($tokens as $token) { // lex... } foreach($ast as $node) { // parse... } }
:
function tokenize($code) { $regexes = [ // ... ]; $statements = split(' ', $code); $tokens = []; foreach($regexes as $regex) { foreach($statements as $statement) { $tokens[] = /* ... */; }); }); return $tokens; } function lexer($tokens) { $ast = []; foreach($tokens as $token) { $ast[] = /* ... */; }); return $ast; } function parseBetterJSAlternative($code) { $tokens = tokenize($code); $ast = lexer($tokens); foreach($ast as $node) { // parse... }); }
. , , .
, , : , , , . . , , - . , .
, , . , . , , //.
, SOLID, «». , ! , ! , , .
:
function showDeveloperList($developers) { foreach($developers as $developer) { $expectedSalary = $developer->calculateExpectedSalary(); $experience = $developer->getExperience(); $githubLink = $developer->getGithubLink(); $data = [ $expectedSalary, $experience, $githubLink ]; render($data); } } function showManagerList($managers) { foreach($managers as $manager) { $expectedSalary = $manager->calculateExpectedSalary(); $experience = $manager->getExperience(); $githubLink = $manager->getGithubLink(); $data = [ $expectedSalary, $experience, $githubLink ]; render($data); } }
:
function showList($employees) { foreach($employees as $employe) { $expectedSalary = $employe->calculateExpectedSalary(); $experience = $employe->getExperience(); $githubLink = $employe->getGithubLink(); $data = [ $expectedSalary, $experience, $githubLink ]; render($data); } }
, . - . , .
:
function createFile($name, $temp = false) { if ($temp) { touch('./temp/'.$name); } else { touch($name); } }
:
function createFile($name) { touch($name); } function createTempFile($name) { touch('./temp/'.$name); }
, /, - . , .
. , . . , - , . .
— - ; , - ; . , .
:
// Global variable referenced by following function. // If we had another function that used this name, now it'd be an array and it could break it. $name = 'Ryan McDermott'; function splitIntoFirstAndLastName() { global $name; $name = preg_split('/ /', $name); } splitIntoFirstAndLastName(); var_dump($name); // ['Ryan', 'McDermott'];
:
$name = 'Ryan McDermott'; function splitIntoFirstAndLastName($name) { return preg_split('/ /', $name); } $newName = splitIntoFirstAndLastName($name); var_dump($name); // 'Ryan McDermott'; var_dump($newName); // ['Ryan', 'McDermott'];
— , , API , production. : . config()
, , . «» .
:
function config() { return [ 'foo' => 'bar', ] }
:
class Configuration { private static $instance; private function __construct() {/* */} public static function getInstance() { if (self::$instance === null) { self::$instance = new Configuration(); } return self::$instance; } public function get($key) {/* */} public function getAll() {/* */} } $singleton = Configuration::getInstance();
:
if ($fsm->state === 'fetching' && is_empty($listNode)) { // ... }
:
function shouldShowSpinner($fsm, $listNode) { return $fsm->state === 'fetching' && is_empty($listNode); } if (shouldShowSpinner($fsmInstance, $listNodeInstance)) { // ... }
:
function isDOMNodeNotPresent($node) { // ... } if (!isDOMNodeNotPresent($node)) { // ... }
:
function isDOMNodePresent($node) { // ... } if (isDOMNodePresent($node)) { // ... }
, . , : « - if
?» : «, , ?» : - . , if
, , . — - .
:
class Airplane { // ... public function getCruisingAltitude() { switch ($this->type) { case '777': return $this->getMaxAltitude() - $this->getPassengerCount(); case 'Air Force One': return $this->getMaxAltitude(); case 'Cessna': return $this->getMaxAltitude() - $this->getFuelExpenditure(); } } }
:
class Airplane { // ... } class Boeing777 extends Airplane { // ... public function getCruisingAltitude() { return $this->getMaxAltitude() - $this->getPassengerCount(); } } class AirForceOne extends Airplane { // ... public function getCruisingAltitude() { return $this->getMaxAltitude(); } } class Cessna extends Airplane { // ... public function getCruisingAltitude() { return $this->getMaxAltitude() - $this->getFuelExpenditure(); } }
( 1)
PHP , . . . . . , , API.
:
function travelToTexas($vehicle) { if ($vehicle instanceof Bicycle) { $vehicle->peddle($this->currentLocation, new Location('texas')); } else if ($vehicle instanceof Car) { $vehicle->drive($this->currentLocation, new Location('texas')); } }
:
function travelToTexas($vehicle) { $vehicle->move($this->currentLocation, new Location('texas')); }
( 2)
( , ) , . , , (strict mode). PHP-. , , «» . PHP, . , PHP- .
:
function combine($val1, $val2) { if (is_numeric($val1) && is_numeric($val2)) { return $val1 + $val2; } throw new \Exception('Must be of type Number'); }
:
function combine(int $val1, int $val2) { return $val1 + $val2; }
, . . - , ! , .
:
function oldRequestModule($url) { // ... } function newRequestModule($url) { // ... } $req = new newRequestModule($requestUrl); inventoryTracker('apples', $req, 'www.inventory-awesome.io');
:
function requestModule($url) { // ... } $req = new requestModule($requestUrl); inventoryTracker('apples', $req, 'www.inventory-awesome.io');
PHP public
, protected
private
. .
- , (accessor) .
-
set
. - .
- .
- .
- , .
/, .
:
class BankAccount { public $balance = 1000; } $bankAccount = new BankAccount(); // Buy shoes... $bankAccount->balance -= 100;
:
class BankAccount { private $balance; public function __construct($balance = 1000) { $this->balance = $balance; } public function withdrawBalance($amount) { if ($amount > $this->balance) { throw new \Exception('Amount greater than available balance.'); } $this->balance -= $amount; } public function depositBalance($amount) { $this->balance += $amount; } public function getBalance() { return $this->balance; } } $bankAccount = new BankAccount(); // Buy shoes... $bankAccount->withdrawBalance($shoesPrice); // Get balance $balance = $bankAccount->getBalance();
/ (members)
:
class Employee { public $name; public function __construct($name) { $this->name = $name; } } $employee = new Employee('John Doe'); echo 'Employee name: '.$employee->name; // Employee name: John Doe
:
class Employee { protected $name; public function __construct($name) { $this->name = $name; } public function getName() { return $this->name; } } $employee = new Employee('John Doe'); echo 'Employee name: '.$employee->getName(); // Employee name: John Doe
(Single Responsibility Principle, SRP)
Clean Code: « ». , , . , (conceptually cohesive), . , . , , , .
:
class UserSettings { private $user; public function __construct($user) { $this->user = user; } public function changeSettings($settings) { if ($this->verifyCredentials()) { // ... } } private function verifyCredentials() { // ... } }
:
class UserAuth { private $user; public function __construct($user) { $this->user = user; } protected function verifyCredentials() { // ... } } class UserSettings { private $user; public function __construct($user) { $this->user = $user; $this->auth = new UserAuth($user); } public function changeSettings($settings) { if ($this->auth->verifyCredentials()) { // ... } } }
/ (Open/Closed Principle, OCP)
: « (, , . .) , ». ? .
:
abstract class Adapter { protected $name; public function getName() { 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) { $this->adapter = $adapter; } public function fetch($url) { $adapterName = $this->adapter->getName(); if ($adapterName === 'ajaxAdapter') { return $this->makeAjaxCall($url); } else if ($adapterName === 'httpNodeAdapter') { return $this->makeHttpCall($url); } } protected function makeAjaxCall($url) { // request and return promise } protected function makeHttpCall($url) { // request and return promise } }
:
abstract class Adapter { abstract protected function getName(); abstract public function request($url); } class AjaxAdapter extends Adapter { protected function getName() { return 'ajaxAdapter'; } public function request($url) { // request and return promise } } class NodeAdapter extends Adapter { protected function getName() { return 'nodeAdapter'; } public function request($url) { // request and return promise } } class HttpRequester { private $adapter; public function __construct(Adapter $adapter) { $this->adapter = $adapter; } public function fetch($url) { return $this->adapter->request($url); } }
(Liskov Substitution Principle, LSP)
. : « S — , S (, S) - (, . .)». .
: , . . — , is-a , .
:
class Rectangle { private $width, $height; public function __construct() { $this->width = 0; $this->height = 0; } public function setColor($color) { // ... } public function render($area) { // ... } public function setWidth($width) { $this->width = $width; } public function setHeight($height) { $this->height = $height; } public function getArea() { return $this->width * $this->height; } } class Square extends Rectangle { public function setWidth($width) { $this->width = $this->height = $width; } public function setHeight(height) { $this->width = $this->height = $height; } } function renderLargeRectangles($rectangles) { foreach($rectangle in $rectangles) { $rectangle->setWidth(4); $rectangle->setHeight(5); $area = $rectangle->getArea(); // : Will return 25 for Square. Should be 20. $rectangle->render($area); }); } $rectangles = [new Rectangle(), new Rectangle(), new Square()]; renderLargeRectangles($rectangles);
:
abstract class Shape { private $width, $height; abstract public function getArea(); public function setColor($color) { // ... } public function render($area) { // ... } } class Rectangle extends Shape { public function __construct { parent::__construct(); $this->width = 0; $this->height = 0; } public function setWidth($width) { $this->width = $width; } public function setHeight($height) { $this->height = $height; } public function getArea() { return $this->width * $this->height; } } class Square extends Shape { public function __construct { parent::__construct(); $this->length = 0; } public function setLength($length) { $this->length = $length; } public function getArea() { return $this->length * $this->length; } } function renderLargeRectangles($rectangles) { foreach($rectangle in $rectangles) { if ($rectangle instanceof Square) { $rectangle->setLength(5); } else if ($rectangle instanceof Rectangle) { $rectangle->setWidth(4); $rectangle->setHeight(5); } $area = $rectangle->getArea(); $rectangle->render($area); }); } $shapes = [new Rectangle(), new Rectangle(), new Square()]; renderLargeRectangles($shapes);
(Interface Segregation Principle, ISP)
ISP, « , ».
: , (settings objects). , . , .
:
interface WorkerInterface { public function work(); public function eat(); } class Worker implements WorkerInterface { public function work() { // ....working } public function eat() { // ...... eating in launch break } } class SuperWorker implements WorkerInterface { public function work() { //.... working much more } public function eat() { //.... eating in launch break } } class Manager { /** @var WorkerInterface $worker **/ private $worker; public function setWorker(WorkerInterface $worker) { $this->worker = $worker; } public function manage() { $this->worker->work(); } }
:
interface WorkerInterface extends FeedableInterface, WorkableInterface { } interface WorkableInterface { public function work(); } interface FeedableInterface { public function eat(); } class Worker implements WorkableInterface, FeedableInterface { public function work() { // ....working } public function eat() { //.... eating in launch break } } class Robot implements WorkableInterface { public function work() { // ....working } } class SuperWorker implements WorkerInterface { public function work() { //.... working much more } public function eat() { //.... eating in launch break } } class Manager { /** @var $worker WorkableInterface **/ private $worker; public function setWorker(WorkableInterface $w) { $this->worker = $w; } public function manage() { $this->worker->work(); } }
(Dependency Inversion Principle, DIP)
:
- . .
- . .
, PHP- ( Symfony), (Dependency Injection, DI). , DI . DI. , (coupling) . — , .
:
class Worker { public function work() { // ....working } } class Manager { /** @var Worker $worker **/ private $worker; public function __construct(Worker $worker) { $this->worker = $worker; } public function manage() { $this->worker->work(); } } class SuperWorker extends Worker { public function work() { //.... working much more } }
:
interface WorkerInterface { public function work(); } class Worker implements WorkerInterface { public function work() { // ....working } } class SuperWorker implements WorkerInterface { public function work() { //.... working much more } } class Manager { /** @var Worker $worker **/ private $worker; public function __construct(WorkerInterface $worker) { $this->worker = $worker; } public function manage() { $this->worker->work(); } }
, , PHPUnit Doctrine. . (chaining), , . this
— .
:
class Car { private $make, $model, $color; public function __construct() { $this->make = 'Honda'; $this->model = 'Accord'; $this->color = 'white'; } public function setMake($make) { $this->make = $make; } public function setModel($model) { $this->model = $model; } public function setColor($color) { $this->color = $color; } public function dump() { var_dump($this->make, $this->model, $this->color); } } $car = new Car(); $car->setColor('pink'); $car->setMake('Ford'); $car->setModel('F-150'); $car->dump();
:
class Car { private $make, $model, $color; public function __construct() { $this->make = 'Honda'; $this->model = 'Accord'; $this->color = 'white'; } public function setMake($make) { $this->make = $make; // NOTE: Returning this for chaining return $this; } public function setModel($model) { $this->model = $model; // NOTE: Returning this for chaining return $this; } public function setColor($color) { $this->color = $color; // NOTE: Returning this for chaining return $this; } public function dump() { var_dump($this->make, $this->model, $this->color); } } $car = (new Car()) ->setColor('pink') ->setMake('Ford') ->setModel('F-150') ->dump();
: « ?» , , :
- — is-a, has-a. : → vs. → (UserDetails).
- . ( , .)
- , . ( .)
:
class Employee { private $name, $email; public function __construct($name, $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, $salary; public function __construct($ssn, $salary) { parent::__construct(); $this->ssn = $ssn; $this->salary = $salary; } // ... }
:
class EmployeeTaxData { private $ssn, $salary; public function __construct($ssn, $salary) { $this->ssn = $ssn; $this->salary = $salary; } // ... } class Employee { private $name, $email, $taxData; public function __construct($name, $email) { $this->name = $name; $this->email = $email; } public function setTaxData($ssn, $salary) { $this->taxData = new EmployeeTaxData($ssn, $salary); } // ... }