Это принципы разработки ПО, взятые из книги 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() {
    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)


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


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

Комментарии (113)


  1. max_m
    01.09.2017 15:24
    +1

    решение для «Избегайте проверки типов (часть 1)» будет ещё лучше если интерфейсы использовать:

    interface Movable { public function move(...); }
    function travelToTexas(Movable $vehicle) { ... }
    


    1. ghost404
      05.09.2017 17:07

      уже используется интерфейс Traveler


  1. shornikov
    01.09.2017 16:16
    +1

    foreach ($locations as $location)

    Сам так делаю, но начинаю сомневаться в правильности такого подхода. Переменные получаются излишне схожими (хоть за пределами цикла вторая обычно больше не используется), да и в скорости набора такой вариант проигрывает (я про то, что приличные ide подставляют по первым набранным буквам: тут приходится выбирать из двух вариантов).


    1. Eldhenn
      01.09.2017 16:37
      -3

      Я обычно пишу

      foreach ($locations as $loc)

      Внутри блока обычно понятно, что такое $loc (или $p[erson], или $s[tudent]).


    1. zenn
      01.09.2017 16:55

      Аналогично, очень часто, делая нечто подобное:

      $records = User::all();
      foreach ($records as $record) {
          // some actions with $record->objects
      }
      

      автокомплит, к примеру у phpstorm внутри перебора foreach(){} сует именно records вместо ожидаемой record. Отсюда у меня так же имеется ряд сомнений относительно данной рекомендации (или адекватности автокомплита phpstorm).


      1. mdErrDX5341
        01.09.2017 17:18

        я обычно пишу так


        $userList = User::all();
        foreach ($userList as $user){
        //code
        }

        Но я бы не советовал так делать… так как вроде не стандарт, но визуально так проще и пропадает проблема с news...


      1. dmz9
        02.09.2017 12:55
        +1

        на такой автокомплит грех жаловаться:

        $mice = $children = $matches = $photos = $wives = $loaves = $women = $geese = $data = [];
        foreach ( $mice as $mouse ) {}
        foreach ( $children as $child ) {}
        foreach ( $matches as $match ) {}
        foreach ( $photos as $photo ) {}
        foreach ( $wives as $wife ) {}
        foreach ( $loaves as $loaf ) {}
        foreach ( $women as $woman ) {}
        foreach ( $geese as $goose ) {}
        foreach ( $data as $datum ) {}

        про data=>datum вовсе не знал пока впервые в самом же шторме не увидел, оказалось это совершенно корректно


      1. technokid
        02.09.2017 12:56

        У вас User::all() может вернуть пустой массив или обьект. И у вас нету проверки, а сразу идет цикл


        1. zenn
          02.09.2017 13:56

          Вообще, код — чисто символичный и ничего он там не может вернуть другого, кроме коллекции объектов (см. laravel doc). И да, давно уже никто не проверяет результаты выборок (по крайней мере в разумных orm) на false, null или прочую дрянь, т.к. методы возвращают ожидаемые результаты одного типа (кроме ситуативных, типа ->first() или ->firstOrNull()).


        1. mdErrDX5341
          02.09.2017 15:31

          интерпретатор не будет жаловаться… но как и написали этот код чисто символичный


          $newsList = $newsRepository->all()
          foreach($newsList as $news) {
          //code
          }
          //$news = $newsList->first();
          //$news = $newsList->last();

          меня только одно интересует, многих раздражает слово list в конце, просто я постоянно использую такую практику а feedback пока не почувствовал, но опять повторюсь, не используйте...


    1. barantaran
      01.09.2017 17:18
      -5

      foreach ($locations as $oneLocation)


    1. n0dwis
      02.09.2017 20:15

      Я использую приставку cur, и, возможно, сокращаю имя переменной. Т.е.

      foreach ($locations as $curLocation)
      

      или
      foreach ($locations as $curLoc)
      

      Второй вариант выглядит не очень, но отлично срабатывает автокомплит, да и проблем с чтением кода не возникает.


  1. win0err
    01.09.2017 16:22

    Тут чушь написана. Такое просто не захватит переменную $name.
    `
    $name = 'Ryan McDermott';


    function splitIntoFirstAndLastName() {
    $name = preg_split('/ /', $name);
    }


    splitIntoFirstAndLastName();


    var_dump($name); // ['Ryan', 'McDermott'];
    // Не ['Ryan', 'McDermott'], а string(14) 'Ryan McDermott'
    `


    Хотя вот так прокатит:


    1. win0err
      01.09.2017 16:33

      Прошу прощения, нечаянно отправил комментарий, не могу изменить его уже.

      Хотя вот так прокатит:
      $name = 'Ryan McDermott';
      $splitIntoFirstAndLastName = function () use (&$name) {
      $name = preg_split('/ /', $name);
      };
      $splitIntoFirstAndLastName();

      var_dump($name); // ['Ryan', 'McDermott'];

      Но такой код ещё нужно додуматься написать


      1. nokimaro
        01.09.2017 17:32
        -3

        или внутри функции надо было объявить global $name;


        function splitIntoFirstAndLastName() {
        global $name;
        $name = preg_split('/ /', $name);
        }


        1. SerafimArts
          01.09.2017 19:24
          +3

          За глобалсы надо отрубать всё, что выпирает. Подумайте над этим на досуге =))))


          1. ghost404
            05.09.2017 16:50

            В этом и суть. По моему где-то даже высказывались о бессмысленности этого правила


            1. SerafimArts
              05.09.2017 22:55

              Ой, значит я не правильно понял комментарий автора выше, прошу прощения.



  1. Dragomeat
    01.09.2017 17:51
    -4

    Только что проверил код с глобалом, всё работает как и у автора статьи. Вывод: вы немного поспешили со своим комментарием)


    1. win0err
      01.09.2017 18:39
      +1

      Внутри функции другая область видимости.

      Можно захватить переменную извне тремя способами, в статье не используется ни один из них:
      1. Лямбда с use, как описал выше;
      2. Вариант nokimaro с global внутри функции;
      3. Использовать $GLOBALS, но это дичь какая-то:

      <?php
      $name = 'Ryan McDermott';
      function splitIntoFirstAndLastName() {
      $GLOBALS['name'] = preg_split('/ /', $GLOBALS['name']);
      }
      splitIntoFirstAndLastName();
      var_dump($name); // ['Ryan', 'McDermott'];
      


      Чтобы не соврать, проверил все варианты (и вариант из статьи) в PHP 5.6 и 7.0, а также прочёл документацию, вдруг забыл что.
      php.net/manual/en/language.variables.scope.php


  1. porutchik
    01.09.2017 21:39
    +3

    Так и не понял, что за 86400. Можно же проще написать 24*60*60 и сразу всё понятно.


    1. zoonman
      02.09.2017 05:52
      -11

      Внезапно ваш код оказывается на Марсе и надо использовать местные сутки.
      24*60*60 уже не прокатит.


      1. daggert
        02.09.2017 08:37
        +8

        Вы наверное не понимаете сути такой записи: 86400 — сложно прочесть и на вскидку сказать сутки это или нет, тогда как записав форматом часы*минуты*секунды — получается более гибко для изменений и более читабельно.


        1. isden
          02.09.2017 17:40

          А еще можно делать вот так:

          strtotime('1 day', 0)


          1. daggert
            02.09.2017 20:42

            ИМХО конечно, но зачем городить лишние функции? Планета вроде не собирается менять орбиту, скорость вращения и наклон, то есть мы всегда знаем что в сутках 24 часа, в часе 60 минут, в минуте 60 секунд, а это означает, как минимум, что созданная давныыым давно запись часы*минуты*секунды и понятна и легко записывается одним простым int


            1. isden
              02.09.2017 20:52
              +2

              Зачем городить? Она в PHP уже есть, просто заменить магическое число (умножение магических чисел) на вызов этой функции. Как мне кажется, это намного читабельнее.


              1. daggert
                02.09.2017 21:55

                Вызов доп функций не люблю, когда можно обойтись строкой на 10 символов (:


                1. isden
                  02.09.2017 22:21

                  Она вызывается 1 раз в секции определения констант. Более того, с подобным подходом удобно определять необходимые константы промежутков вида «1 week» или «1 day +1 hour» / «1 day +1 minute».


                  1. daggert
                    02.09.2017 22:49

                    Про константы не подумал, ваша правда.


            1. VolCh
              02.09.2017 20:56

              В некоторых минутах 61 секунда :)


              1. sumanai
                02.09.2017 23:24
                +1

                А PHP знает, когда будут коррекционные секунды?


                1. Casus
                  04.09.2017 13:36
                  +1

                  PHP зависит от системных вызовов, если система на NTP — да.


        1. goral
          03.09.2017 12:01

          Я думаю zoonman имел виду, что костанта одна, а в вашем случае записей вида 24*60*60 может быть много и все их найти проблематично, особенно, если придется портировать код под Марсианские реалии.


          1. daggert
            03.09.2017 13:19

            А почему мы в константу не загоним 24*60*60?


        1. zoonman
          04.09.2017 01:01

          Лично мне легко это прочесть и сказать, сутки это или нет. Увы, такие базовые вещи, как количество секунд сутках можно и знать.

          Как было сказано в статье, вынесение этих чисел в константы с корректным именем решает проблему восприятия.
          Не нужно потом запоминать, что там.

          Могу привести немного высосанный из пальца пример.
          В PHP есть встроенная константа ?, но для наглядности допустим, что ее нет.

          Если вы в коде будете повсюду писать что-то вроде 3.14, то вначале все будет хорошо.
          Но если внезапно потребуется увеличить точность вычислений, то прийдется прошерстить весь код заменив 3.14 на 3.14159.
          Если еще раз понадобится это сделать, то еще прийдется менять весь код.
          Если вы где-то в одном месте недопишете одну цифру, например 3.1415, то автозамена уже не сработает. Это место будет приводить к неправильным результатам.
          Если бы всюду использовали нечто Math::PI, то проблема была бы решена раз и навсегда.


          1. daggert
            04.09.2017 09:34

            > Как было сказано в статье, вынесение этих чисел в константы с корректным именем решает проблему восприятия.

            Я думаю что с этим утверждением спорить глупо (:


      1. Szer
        02.09.2017 10:44
        +5

        Если Вы реально на такие случаи закладываетесь в своём коде, мне жалко Вашего работодателя/клиентов, потому что Вы делаете какую-то херню.


      1. michael_vostrikov
        02.09.2017 15:41
        +1

        Внезапно, 86400 секунд на Марсе это все равно 24*60*60 секунд.


        1. zoonman
          04.09.2017 01:12
          -1

          Внезапно в марсианских сутках 88775 земных секунд.


          1. michael_vostrikov
            04.09.2017 05:17
            +2

            Ну и что? Замена 86400 на 24*60*60 от этого не становится неправильной.


      1. porutchik
        02.09.2017 16:13

        На Марсе в сутках 86400 марсианских секунд.


        1. mdErrDX5341
          03.09.2017 12:30
          -1

          Смысл в том, что магические символы это всегда дурной тон в программировании...


        1. zoonman
          04.09.2017 01:14
          -1

          Написанный код не станет марсианским, также как и системные часы не станут марсианскими. Они продолжат отсчитывать время в земных секундах.
          Поэтому проще обновить прошивку ПО, понимающую, что в марсианских сутках 88775 земных секунд.


          1. ghost404
            05.09.2017 16:59
            +1

            можно вопрос, а часто вы сайтики на марсе запускаете?


    1. alex6636
      02.09.2017 12:56

      Лол, ну попробуйте такое записать в свойство класса по умолчанию и посмотрите что скажет интерпретатор


      1. sumanai
        02.09.2017 14:23
        +2

        Ваши знания сильно устарели. С 5.6 в константы можно пихать вычисляемые выражения.


        1. alex6636
          02.09.2017 15:03
          -2

          оно то хорошо, хотя мы пока пишем 5.4 совместимый код. кстати, причем здесь константы?


          1. sumanai
            02.09.2017 15:08
            +2

            хотя мы пока пишем 5.4 совместимый код

            Подкиньте начальству идею, что PHP 7 работает в 2 раза быстрее.
            кстати, причем здесь константы?

            В примере ХОРОШО используются константы класса, так как число секунд в сутках вещь константная, в большинстве случаев.


            1. alex6636
              02.09.2017 15:11
              -3

              это пока не так актуально, во-первый нет пока адекватных сборок для линукса с php 7, во-вторых, заметного прироста производительности не будет, т.к основные тормоза связаны с базой данных. поэтому уж точно нет смысла переписывать старый код, другое дело, что может есть смысл писать новый 100% совместимым под 7.0.
              я просто комментарий писал к свойству класса, а не константе, но да, в данном случае правильнее использовать константу конечно


              1. sumanai
                02.09.2017 15:20
                +3

                во-первый нет пока адекватных сборок для линукса с php 7,

                Как там, в 2015?
                во-вторых, заметного прироста производительности не будет, т.к основные тормоза связаны с базой данных

                Мне кажется, из-за того, что все пишут про тормоза БД, все забили на код, и как раз он сейчас является причиной тормозов. Например на типичном форуме на phpBB версий 3.1 и новее обработка кода занимает в 20 раз больше времени, чем работа с БД.
                поэтому уж точно нет смысла переписывать старый код

                А там и не нужно переписывать, если кон конечно не писался во времена PHP 3. Поправить нужно пару мест типа preg_replace с модификатором e, но этот код уже давно попахивал и сыпал варнингами.


                1. alex6636
                  02.09.2017 18:53

                  хорошо еще, что производительность битрикса не привели в качестве примера)


        1. Sannis
          02.09.2017 15:33

          Когда я увидел в коде коллеги const list = array(1, 2, 3); мне поплохело. Если так можно написать не значит что нужно.


    1. ghost404
      05.09.2017 16:56

      Мне и моим голлегам проще читать цифры вида 3600 и 86400, хотя авторы со мной не согласны


  1. EverOne
    01.09.2017 21:54

    Заголовок спойлера
    class BankAccount {
    ...
        public function withdrawBalance($amount) {
            if ($amount > $this->balance) {
                throw new \Exception('Amount greater than available balance.');
            }
            $this->balance -= $amount;
        }
    ...
    }


    1. win0err
      01.09.2017 22:19
      +1

      Лучше делать так, чтобы потом было легко отловить исключение:

      <?php
      class BalanceException extends \Exception { } // Ну или BankAccountException, зависит уже от конкретной ситуации
      
      class BankAccount {
      ...
          public function withdrawBalance($amount) {
              if ($amount > $this->balance) {
                  throw new BalanceException('Amount greater than available balance.');
              }
              $this->balance -= $amount;
          }
      ...
      }
      


      1. EverOne
        02.09.2017 00:08
        +1

        Я не об отлове (хотя и о нем тоже), а вообще об исключениях в бизнес-логике. Исключения должны обрабатывать ошибки в работе системы (такие как невозможность чтения из БД или неправильный тип аргумента функции), а не ошибки в действиях пользователя (такие как неверный пароль или недостаточная сумма на балансе, например). Я не ошибаюсь?


        1. SerafimArts
          02.09.2017 01:08

          В целом, нет, не ошибаешься, но исключения, вида NotFoundHttpException, UnprocessableEntityHttpException и прочие — говорят об обратном.


        1. dmz9
          02.09.2017 13:39
          +3

          Исключения должны обрабатывать ошибки в работе системы

          не так.

          Исключения нужны для передачи ошибок вверх по стеку вызовов
          + в них можно указать экстра-данные (если как «белый человек» расширяешь стандартный класс, добавляешь методы для get/set этих данных)
          + они избавляют все промежуточные классы/методы между throw и catch от необходимости
          а. знать о кодах и типах ошибок «нижестоящих» классов
          б. знать о том как их обработать
          в. иметь лишнюю ответственность в виде «я могу/должен это обработать»
          г. (как следствие) иметь лишний защитный код
          д. иметь много типов возврата из метода (return false для ошибки, string для текста ошибки, string[] для набора ошибок)

          Исключение прерывает поток исполнения, а обычная ошибка, которая отдается в return — это продолжение потока исполнения, как будто бы так и надо. Поэтому исключения нельзя использовать для нормального ветвления логики и следует использовать если из за конкретной ошибки нет возможности работать дальше (в текущем контексте, а не во всей программе). Типичный пример — «первая линия обороны» — проверка аргументов в методах где нет доверия входным данным.

          Ошибки надо возвращать когда их ожидают.
          Ошибки валидатора — ожидаемые. Собери их в массив для начала, засунь в исключение и брось.
          Сверху, тот кто вызвал валидатор, поймает исключение и получит нужные данные для формирования юзер-френдли ответа (и логирования и еще чего нибудь).

          Если брать конкретно этот простой пример — выброс исключения оправдан.
          Можно ли продолжить операцию при отрицательном балансе? — нет, нельзя (хотя это зависит от бизнеса), поэтому тут именно исключение.
          Намного проще поставить один try/catch где то наверху для критического кода и ожидать в нем исключения, нежели распихивать обработку в каждый класс, да еще и передачу по стеку вызов данных определенного типа и рассказывать каждому классу как работать с ними.


        1. VolCh
          02.09.2017 21:11
          +2

          Недостаточная сумма на балансе — не ошибка пользователя в методе, изменяющем баланс. Ошибки пользовательского ввода должны проверяться раньше, чем попадут в бизнес-логику, например на фронте или в контроллере, а уж если попали в модель, то попытка овердрафта — это исключение, возможно требующее вмешательства СБ. Надо понимать разницу между правилом валидации «сумма операции не должна быть меньше баланса» и бизнес-правило «ни при каких операциях итоговый баланс не должен быть меньше нуля», даже если проверка осуществляется идентичным или даже одним и тем же кодом.


  1. kloppspb
    02.09.2017 01:21

    Не добавляйте ненужный контекст

    О, да! Это (как и многое другое) не только к PHP относится, конечно же.

    Сейчас работаю с базой, проектировщик которой за каким-то лешим решил все айдишники во всех таблицах обозвать «имя_таблицы_id». Учитывая, что таблиц в схеме около сотни, и встречаются среди них с именами вроде «some_cool_stuff_report_queue», хочется убивать…


    1. VolCh
      02.09.2017 21:13

      В данном случае это может быть оправдано.


      1. kloppspb
        03.09.2017 13:15

        Не представляю ни одного такого случая. Тем более что — см. ниже.


    1. JSmitty
      02.09.2017 22:23

      селекты упрощаются немного, т.к. если есть табличка rooms и в ней PK room_id, то все ссылки на нее из других таблиц называются room_id, и тогда например можно писать такое:

      SELECT * FROM persons LEFT JOIN rooms USING (room_id)

      Если колонка PK называется всегда id, то потребуется полностью писать — ON (rooms.id = persons.room_id)


      1. kloppspb
        03.09.2017 06:11

        В 50k строк исходников, работающих с этой базой, я не обнаружил ни одного подобного джойна.


      1. michael_vostrikov
        03.09.2017 10:53
        +1

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


    1. gmini
      06.09.2017 09:57

      Это вопрос религиозный — есть оракловский подход, называть свой ключ ID, а внешние ключи another_table_id.
      и есть MS SQL подход — во всех таблицах называть ключи <table_name>_id.
      Как указали ниже, это связано с синтаксисом джойнов в разных диалектах SQL


  1. vtvz_ru
    02.09.2017 01:49

    Аргументы по умолчанию не работают, если по умолчанию переменная ($this->default или self::$default), хотя это бывает необходимо


  1. zoonman
    02.09.2017 06:37
    +2

    Принцип подстановки Барбары Лисков продемонстрирован достаточно сложно для понимания.
    Дело в том, что существуют высота и ширина в квадрате равные между собой. Поэтому установка любого из этих свойств является корректной операцией.

    $area = $rectangle->getArea(); // Плохо: Will return 25 for Square. Should be 20.
    

    Самый безграмотный комментарий, который я встречал. It must be 25 for Square.

    Дело в том, что квадрат это частный случай прямоугольника, у которого стороны равны. Спешу заметить, что квадрат это еще и ромб, и параллелограмм.

    Осознание что прямоугольник может быть квадратом позволяет предположить, что последовательность установки разных высот и ширин будет влиять на значение площади. Поэтому следует ожидать специфического поведения для частных случаев.

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

    Лично мне больше понравилось описание на Stackoverflow.

    DuckShaped

    В двух словах я бы описал его так: когда у вас есть два похожих, но совершенно разных объекта, не наследуйте один от другого.


    1. michael_vostrikov
      02.09.2017 16:21
      -1

      Поэтому установка любого из этих свойств является корректной операцией.

      Изменение ширины у математического квадрата или прямоугольника это некорректная операция. У квадрата нет изменяющихся сторон. Если сторона изменилась, значит мы задали другой квадрат. А раз другой, то он вполне может быть прямоугольником независимо от исходного квадрата.


      Если квадрат меняет стороны, значит у нас какой-то свой объект, который мы почему-то называем квадратом.


      Проблема в том, что в современном ООП нет возможности описать связь, что квадрат это прямоугольник. Вот если бы тип менялся при изменении стороны, проблемы бы не было.


      Может я не прав, но в примере из статьи я не вижу большой проблемы. Площадь квадрата будет нормально считаться после изменения сторон. Это не будет работать только в каких-нибудь тестах, где проверяется конкретный результат. Но в тестах лучше проверять потомков, а не родителя. Проблема будет, если будет меняться поведение или добавляться ограничения, например, setWidth() будет все под-объекты уничтожать, к которым есть доступ снаружи.


      1. daggert
        02.09.2017 22:02

        Проблема в том, что в современном ООП нет возможности описать связь, что квадрат это прямоугольник.

        class Квадрат extends Прямоугольник?


        1. sumanai
          02.09.2017 23:27

          Там в следующем предложении описание ошибочности этого подхода в данном случае.


          1. daggert
            03.09.2017 13:33

            Честно говоря так и не понимаю ошибочности данного подхода. Наверное я слишком узко смотрю?


            1. michael_vostrikov
              03.09.2017 17:09
              +1

              Если коротко, квадрат добавляет ограничения в реализацию предка-прямоугольника, что запрещено LSP.


        1. michael_vostrikov
          03.09.2017 11:02

          Считается, что это нарушает SOLID. Тут есть обсуждение по этой теме.


      1. zoonman
        04.09.2017 01:47

        Проблема в том, что в современном ООП нет возможности описать связь, что квадрат это прямоугольник. Вот если бы тип менялся при изменении стороны, проблемы бы не было.

        Именно поэтому и производится разделение на разные классы.
        Чисто из геометрического смысла, попытка задать высоту или ширину у квадрата приводит к автоматическому изменению ширины или высоты соответственно.
        Просто как ни крути, но прямоугольник и квадрат хоть и одного поля, но разные ягоды.


        1. michael_vostrikov
          04.09.2017 05:20

          Разные-то разные, но связь между ними есть. А ООП не позволяет ее выразить. Про квадрат и прямоугольник знают все, а вот с какими-нибудь бизнес-сущностями это уже не так просто.


      1. ghost404
        05.09.2017 16:02

        может дополните мою правку?


  1. jtiq
    02.09.2017 12:56
    +1

    Я тоже придерживаюсь правил, но когда устаю, то уже у меня получается как попало :( почти как быдло-кодинг
    Как с этим бороться?


    1. F0iL
      02.09.2017 14:04

      Обязательным прохождением code-review от коллег, которые будут не давать мержиться и больно бить по рукам за любой дурнопахнущий код?)


      1. zoonman
        04.09.2017 01:52

        При этом нужно ментально перестроиться к тому, что к тебе не придираются, а наоборот помогают.
        Многие новички воспринимают Code Review очень болезненно, как страшный суд. Поэтому задача старших коллег быть как можно осторожнее в выражениях и стараться объяснять, почему именно код плох и как его можно улучшить.


    1. den_rad
      03.09.2017 01:19

      Я не пушу вечером. Лучше утром свежим взглядом просмотреть код и сделать push.
      И code review рулит, конечно


  1. Travelcamnet
    02.09.2017 12:56
    -4

    $locations = ['Austin', 'New York', 'San Francisco'];


    ПЛОХО
    
    foreach ($locations as $location) {
        doStuff();
        doSomeOtherStuff();
        dispatch($location);
    });

    В каждом цикле PHP будет пересоздавать переменную. Так же данный код работает заметно медленней, чем следующие два.

    ПЛОХО
    
    for ($i = 0; $i < count($locations); $i++) {
        $li = $locations[$i];
        doStuff();
        doSomeOtherStuff();
        dispatch($li);
    }

    На каждом шаге постоянно будет получатся размер массива, а так же опять же создаваться дополнительная переменная.

    ХОРОШО
    
    for ($i = 0, $k=count($locations); $i < $k; $i++) {
        doStuff();
        doSomeOtherStuff();
        dispatch($locations[$i]);
    }
    

    Никаких лишних переменных и вычислений в циклах. Как по мне разница между $locations[$i] и $location не играет роли, а вот потери в скорости во втором случае при больших массивах будет существенно.


    1. antonwork
      02.09.2017 13:35
      +3

      > $locations[$i] и $location не играет роли, а вот потери в скорости во втором случае при больших массивах будет существенно.

      ох уж эти микрооптимизации… в плане производительности разницы не будет никакой


    1. win0err
      02.09.2017 16:48
      +4

      Не стоит забывать, что бы уже не в 2013-м году. Интерпретатор очень сильно поумнел в версии 7.0. И такие микрооптимизации бессмысленны.

      Докажу тестом:

      Исходный код теста
      Доступен на GitHub Gist, вносите правки, если я где-то ошибся.

      В тесте измеряем изменение в потребляемой памяти в байтах и время выполнения в микросекундах
      <?php 
      echo 'PHP ' . phpversion() . PHP_EOL . PHP_EOL;
      $test = [];
      for($i = 0; $i < 100000; $i++)
      	$test[$i] = $i;
      process_test(function() use ($test) {
      	foreach($test as $t)
      	    continue; 
      });
      process_test(function() use ($test) {
      	for($a = 0; $a < sizeof($test); $a++)
      		continue; 
      });
      process_test(function() use ($test) {
      	for($a = 0, $b = sizeof($test); $a < $b; $a++)
      		continue;  
      });
      function process_test($callback) {
      	static $runNumber = 0;
      	$runNumber++;
      
      	$startedAt = microtime(true);
      	$callback();
      	echo 'Test #' . $runNumber . ': ' . ( microtime(true) - $startedAt ) . ' seconds' . PHP_EOL;
      }
      


      1. Travelcamnet
        04.09.2017 07:37
        -3

        Согласен, что для 7 актуальности не несет. Но, если я не ошибаюсь, доля 5.6 еще достаточно велика, чтобы не забывать о таких вещах)


        1. SerafimArts
          06.09.2017 04:15

          Хочу на всякий случай заметить, что Laravel 5.5 LTS требует 7.0 в минималках, Symfony 4.0 LTS уже 7.1, Doctrine — тоже 7.1, Propel 3 — тоже 7.1, а версия 7.2 на данный момент находится на стадии релиз кандидата. Исходя из этих критериев можно заявлять, что у большинства современных или актуальных проектов на данный момент стоит 7.0 или 7.1.

          Доля php 5.6, подозреваю, такая же, как и 5.5, как и 5.4 и прочих, может чуть более. Каждый год образуется некий процент людей, которые живут (написали или поддерживают) в легаси коде и не могут переехать на актуальные версии по этим или иным причинам (ну например у них говнобитрикс или просто лень).


    1. VolCh
      02.09.2017 21:19
      +2

      Он не будет пересоздавать переменную, он будет перезаписывать её значение, стоимость чего значительно ниже чем стоимость создания, это без учёта возможных оптимизаций в трансляторе


  1. yazyk_na_nojkah
    02.09.2017 19:01
    +1

    Покажите эту статью битриксоидам.


  1. Novikofff
    02.09.2017 23:26
    +2

    Хорошо:

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

    PSR — Что такое?
    DI — И так сойдет)))

    Вцелом статья хорошая, но нужно было конечно кому то показать)


    1. ghost404
      05.09.2017 16:11

      Автор оригинала содрал код отсюда и даже не особо заморачивался с переводом в PHP код (1, 2, 3, 4).


  1. VolCh
    03.09.2017 10:27

    Формальное определение: «Если S — это подтип Т, то объекты типа Т могут быть заменены объектами типа S (например, вместо объектов типа Т можно подставить объекты типа S) без изменения каких-либо свойств программы (корректность, задачи и т. д.)». Ещё более пугающее определение.

    Можно объяснить проще: если у вас есть родительский и дочерний классы, тогда они могут быть взаимозаменяемы без получения некорректных результатов.

    Более простое объяснение не соотвествует формальному. Не взаимозаменяемы, а родительский заменяем на дочерний.


    Сцепление — очень плохой шаблон разработки, затрудняющий рефакторинг кода.

    Сцепление не шаблон, а характеристика, метрика кода.


  1. Kwisatz
    03.09.2017 10:53

    Господа, прошу, не нужно учить весь мир создавать пустые конструкторы.
    Это все приводит к тому, что народ делает вещи наподобие:

    $lionOptions=['size' => 'big', 
                         'color' => 'black']
    $lion=new Lion($lionOptions);
    


    И если в php7 при указании типов это хоть как то переживаемо, то в php5 код превращается в неюзабельное нечто.

    И даже если сеттеров не очень много, не очень правильно создавать неполноценных львов, вы не находите?


  1. vlakarados
    04.09.2017 10:04
    +2

    «Не пишите в глобальные функции» — и сделали синглтон. Сделали ЕЩЕ хуже. Синглтон так же глобален, только без слова глобал.


    1. SerafimArts
      04.09.2017 15:52
      +1

      Очень часто можно услышать "не используйте ххх" без аргументации "почему не использовать" (да ещё и капслоком накричали). У синглтона две проблемы — это глобальный стейт и скрытые зависимости в местах использования.


      Проблема глобалсов же — в неконтролируемости data-flow (плюс всё, перечисленное выше для синглтона). При этом, в случае синглтона мы получем контроль за состоянием, а значит и допустимость stateless, например в тестировании, через какой-нибудь вспомогательный метод setInstance(?Singleton $instance): void


      Остаётся лишь одна проблема, от которой не избавиться — это скрытые зависимости. Но если не переусердствовать с этим, то это не так уж и страшно. В частности — использовать какие-нибудь контроллеры\презентеры в местах применения оного, делегируя его состояние сторонним сервисам. Такие вещи почти всегда тестируются функциональными тестами, а не юнитами, так что фатальных проблем я не наблюдаю.


      В любом случае синглтон, максимум для чего может понадобиться — это для DI контейнера. Больше я хз где его можно применять.


      Хочется услышать ваше мнение где я ошибаюсь. Почему синглтон хуже глобалсов? Лично я категорически не согласен, это глобалсы на несколько порядков хуже синглтонов.


      1. VolCh
        05.09.2017 04:05

        При этом, в случае синглтона мы получем контроль за состоянием

        Можем получить, если постараемся. Но очень часто синглтоны открыты на изменение всем.


        1. SerafimArts
          05.09.2017 04:48

          Можем получить, если постараемся. Но очень часто синглтоны открыты на изменение всем.

          Кажется, что для глобального доступа они и предназначены. В любом случае, под "контролем состояния" я подразумевал хотя бы минимальную верификацию данных, а не знание о том что в данный момент содержит синглтон (хотя и это не исключено при должной реализации) и кто его использует (а вот это уже невозможно без магии обратной трассировки, что есть "небольшая дичь").


          Но всё равно мой тезис о том, что "глобалсы — это треш и если уж кто-то упоролся до глобального стейта, то лучше уж синглтоны" остаётся в силе =)



  1. ghost404
    05.09.2017 15:59

    Вообще-то вы очень сильно поспешили с переводом.
    Там куча косяков. Большую часть я поправил, но например реализация того же LSP не правильная.
    А про использование SECONDS_IN_A_DAY я вообще молчу.


    Если уж сделали перевод, поделитесь с сообществом.


    1. ghost404
      05.09.2017 17:18

      И актуализируйте пожалуйста перевод до текущей версии. Перевод сильно отстал.


  1. Mausglov
    05.09.2017 16:23

    В разъяснении принципа Лисков серьёзная ошибка:

    Можно объяснить проще: если у вас есть родительский и дочерний классы, тогда они могут быть взаимозаменяемы без получения некорректных результатов

    Они не взаимозаменяемы. Принцип говорит о том, что экземпляр родительского класса можно заменить экземпляром дочернего класса, и всё будет хорошо. В обратную сторону заменять, разумеется, нельзя: смысл дочернего класса в расширении функционала родителя, и если вы возьмёте экземпляр родительского класса и попробуете обратиться к этому расширенному функционалу — вас ждут неприятности.


  1. oxidmod
    05.09.2017 16:26

    Скажите, а почему фигуры не инициализируются через конструктор?
    Ведь так намного лучше, не?

    abstract class Shape {
        abstract public function getArea();
    }
    
    class Rectangle extends Shape {
        private $width;
        private $height;
        public function __construct(float $width, float $height) {
            $this->width = $width;
            $this->height = $$height;
        }
    
        public function setWidth(float  $width): self {
            return new self($width, $this->height);
        }
    
        public function setHeight(float $height): self {
            return new self($this->width, $height);
        }
    
        public function getArea(): float {
            return $this->width * $this->height;
        }
    }
    
    class Square extends Shape {
        private $length;
        public function __construct(float $length) {
            $this->length = $length;
        }
    
        public function setLength(float $length): float {
            return new self($length);
        }
    
        public function getArea() {
            return $this->length * $this->length;
        }
    }
    
    function renderShapes(Shape $shapes) {
        foreach($shapes in $shape) {
            $area = $shape->getArea(); 
            // render $area
        });
    }
    
    $shapes = [new Rectangle(5, 6), new Rectangle(2.5, 10), new Square(3.14)];
    renderShapes($shapes);
    


    1. ghost404
      05.09.2017 17:10

      Это сделано для демонстрации проблемы LSP
      Есть пример с immutable классами.


      1. oxidmod
        05.09.2017 17:27

        Да я как-бы не про immutable говорю.
        Я о том, что пример ниочемный вцелом.
        Что говорит принцип подстановки? что вместо родительского класса может использоваться дочерний и клиентский код не должен от этого зависеть.
        В текущей реализации ваш клиентский код зависит от типа. В нем есть проверка на тип, которую чуть выше советовали избегать.
        В пулл-реквесте где вместо базового класса используется интерфейс вообще не понятно что. Где использование наследников Shape вперемешку? Там код завязан на конкретные типы.


        1. ghost404
          05.09.2017 17:41

          Пример с прямоугольником и квадратом самый популярны при описании принципа LSP.


          В PR код завязан на конкретные типы потому, что квадрат и прямоугольник это разные типы. В этом и суть LSP


          1. oxidmod
            05.09.2017 17:50

            Это подтипы Shape, значит код завязанный на Shape должен одинаково работать с Rectangle и Square.

            Но у вас нету кода завязанного на Shape. Там не демонстрируется LSP

            function areaVerifier(Shape $shape, $expectedArea): bool {
                return $shape->area() === $expectedArea;
            }
            


            Вот так будет пример. Клиентский код завязан на абстракцию, и может работать с любым подтипом.


            1. ghost404
              05.09.2017 18:08

              Спасибо за замечание. Уже убрал интерфейс Shape.


              1. oxidmod
                05.09.2017 18:23

                рука_лицо.джпег! Если это разные совсем сущности и не являются подтипами одной абстракции то при чем тут LSP совсем???


                1. ghost404
                  05.09.2017 18:45

                  рука_лицо.джпег! при том что вынесение одной в подтип другой является нарушением LSP, о чем и говорит весь раздел


                  Если вам не нравится пример, предложите лучше, а не критикуйте.
                  Это открытый проект.


                  1. oxidmod
                    05.09.2017 22:51

                    Внимательно читаем первые 3 абзаца

                    Перечитываем определением Мартина еще разок или два.
                    Это тоже самое что написала сама Лисков, но человеческим языком.

                    Принцип Лисков не о том, что квадрат не нужно наследовать от прямоугольника. Он о программировании на контрактах и о правильном наследовании.
                    В вашем изначальном примере квадрат унаследованный от прямоугольника замещал логику сеттеров. Это неправильный подход к наследованию. При наследовании реализация должна расширяться, но не замещаться. Если у вас возникла необходимость полностью заменить логику в наследнике, значит этот наслендик никакой на самом деле не наследник.

                    А теперь сводим все к общему знаменателю. У нас, к примеру, есть задача вывести название фигуры и её площадь. Мы еще даже не знаем какие фигуры мы будем выводить, но мы знаем что нам нужно её название и её площадь. Определим контракт (он же и будет базовым типом по сути):

                    Например так
                    interface Shape {
                        public function name(): string;
                        
                        public function area(): float;
                    }
                    


                    1. ghost404
                      06.09.2017 00:15

                      Вы не поверите. Я не только Википедию читал, но и ещё с десяток статей по теме.
                      Но вы похоже не поняли о чем статья и о чем проект.


                      Вы мне раз за разом пытаетесь объяснить как правильно реализовать LSP.
                      Задача же проекта показать проблему, то есть пример нарушения LSP, и привести пример решения конкретной проблемы, то есть решение которое не будет нарушать LSP. Как ваше решение, так и моё не нарушает LSP, что и требуется.


                      Код решат разные задачи потому и правильное решение будет разным.


                      Ну и авторам больше по нраву другой пример.


                      1. oxidmod
                        06.09.2017 07:24

                        Но должно же быть два примера. Как плохо и как хорошо. К примеру как плохо у меня вопросов нет. Вот унаследовали квадрат от прямоугольника, но клиентский код не может работать с квадратом как с прямоугольником.
                        Но теперь мы тупо сделали 2 класса независимых и 2 функи для каждого типа и все хорошо? Нет, не хорошо. Фигур много, вам придется под каждую писать свою версию клиентского кода! Вы завязаны на реализацию, а не на абстракцию.
                        Нарушение LSP не в наследовании, а в том что неправильно выбрана абстракция. Не квадрат подтип прямоугольника. Обе фигуры подтипы абстракции Shape. Тогда можно написать клиентский код завязанный на Shape так, что ему будет не важно какой подтип прилетел.
                        Как же вы читали то Википедию? Там везде о подтипах и о том, что базовый тип может быть заменен подтипом или подтипы друг другом.
                        Убрав подтипы вы не сделали хорошо. Вы просто задублировали количество клиентского кода.


                      1. oxidmod
                        06.09.2017 10:14

                        Ну и авторам больше по нраву другой пример.

                        Даже в этом примере авторы не отказались от наследования. Вы же видите? Они исправили абстракцию, но пингвин по прежнему подтип птицы. Код завязанный на птиц в целом может нарисовать и установить расположение птице и пингвину без любых модификаций.