Чистый код на PHP

в 12:10, , рубрики: clean code, php, solid, Блог компании Mail.Ru Group, никто не читает теги, Программирование, Проектирование и рефакторинг, Совершенный код

Чистый код на PHP - 1

Это принципы разработки ПО, взятые из книги Clean Code Роберта Мартина и адаптированные для PHP. Это руководство не по стилям программирования, а по созданию читабельного, многократно используемого и пригодного для рефакторинга кода на PHP.

Не каждый из этих принципов должен строго соблюдаться, и ещё с меньшим количеством все будут согласны. Это лишь рекомендации, не более, но все они кодифицированы в многолетнем коллективном опыте автора Clean Code.

Статья вдохновлена clean-code-javascript.

Содержание

  1. Переменные
  2. Функции
  3. Объекты и структуры данных
  4. Классы
    • S: Принцип единственной ответственности (Single Responsibility Principle, SRP)
    • O: Принцип открытости/закрытости (Open/Closed Principle, OCP)
    • L: Принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP)
    • I: Принцип разделения интерфейса (Interface Segregation Principle, ISP)
    • D: Принцип инверсии зависимостей (Dependency Inversion Principle, 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() {
    $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($configuration) {/* */}
    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)

Этот принцип гласит:

  1. Высокоуровневые модули не должны зависеть от низкоуровневых. Оба вида должны зависеть от абстракций.
  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Сначала это может быть трудным для понимания, но если вы работали с 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();

Композиция лучше наследования

Как говорится в известной книге «Шаблоны проектирования» Банды четырёх, по мере возможности нужно выбирать композицию, а не наследование. Есть много хороших причин использовать как наследование, так и композицию. Главная цель этой максимы заключается в том, если вы инстинктивно склоняетесь к наследованию, то постарайтесь представить, может ли композиция лучше решить вашу задачу. В каких-то случаях это действительно более подходящий вариант.

Вы спросите: «А когда лучше выбирать наследование?» Всё зависит от конкретной задачи, но можно ориентироваться на этот список ситуаций, когда наследование предпочтительнее композиции:

  1. Ваше наследование — это взаимосвязь is-a, а не has-a. Пример: Человек → Животное vs. Пользователь → Детали пользователя (UserDetails).
  2. Вы можете повторно использовать код из базовых классов. (Люди могут двигаться, как животные.)
  3. Вы хотите внести глобальные изменения в производные классы, изменив базовый класс. (Изменение расхода калорий у животных во время движения.)

Плохо:

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);
    }
    // ...
}

Автор: AloneCoder

Источник

Поделиться

* - обязательные к заполнению поля