
これらは、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);
}
// ...
}