Принципы разработки программного обеспечения, из книги Роберта К. Мартина «Чистый код», адаптированной для PHP. Это не руководство по стилю. Это руководство по созданию читаемого, многоразового и рефакторируемого программного обеспечения на PHP.
Не каждый принцип должен строго соблюдаться, и еще меньше будет универсальными. Это руководящие принципы и не более того, но они кодифицированы многолетним коллективным опытом авторов Clean Code.
Хотя многие разработчики все еще используют PHP 5, большинство примеров в этой статье работают только с PHP 7.1+.
Аргументы функции (идеально — 2 или менее )
Ограничение количества функциональных параметров невероятно важно, поскольку облегчает тестирование вашей функции. Наличие более трех приводит к комбинаторному взрыву, когда вам приходится проверять массу разных случаев с каждым отдельным аргументом.
Нулевые аргументы — идеальный случай. Один или два аргумента в порядке вещей, и три следует избегать. Все, что нужно, должно быть консолидировано. Обычно, если у вас более двух аргументов, ваша функция пытается сделать слишком много. В тех случаях, когда это не так, большую часть времени объект более высокого уровня будет достаточным в качестве аргумента
Плохо:
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 в своем приложении.
Не используйте шаблон Singleton
Синглтон — это анти-шаблон. Если перефразировать Брайана Баттона:
Как правило, они используются в качестве глобального экземпляра, почему это так плохо? Потому что вы скрываете зависимости вашего приложения в своем коде, вместо того, чтобы раскрывать их через интерфейсы. Они по своей сути приводят к тому, что код тесно связан. Это во многих случаях затрудняет тестирование.
Плохо:
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)
{
// ...
}
// ...
}
Создайте экземпляр класса DBConnection и настройте его с помощью DSN.
$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 7+, и вы не можете использовать полиморфизм, но вы все еще чувствуете необходимость проверки типа, вам следует рассмотреть объявление типа или строгий режим. Он предоставляет вам статическую типизацию поверх стандартного синтаксиса PHP. Проблема с ручной проверкой типов заключается в том, что для этого потребуется столько дополнительных слов, что искусственная «безопасность типа», которую вы получаете, не компенсирует потерянную читаемость. Держите ваш 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;
}
Удалите мертвый код
Мертвый код так же плох, как дубликат кода. Нет причин держать его в своей кодовой базе. Если это не вызывается, избавитесь от этого! В GIT все равно все останется, если вам вдруг что-то понадобится.
Плохо:
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');