Чистый код на PHP. Функции.

Принципы разработки программного обеспечения, из книги Роберта К. Мартина «Чистый код», адаптированной для PHP. Это не руководство по стилю. Это руководство по созданию читаемого, многоразового и рефакторируемого программного обеспечения на PHP.

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

Хотя многие разработчики все еще используют PHP 5, большинство примеров в этой статье работают только с PHP 7.1+.

Аргументы функции (идеально — 2 или менее )

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

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

Плохо:

function createMenu(string $title, string $body, string $buttonText, bool $cancellable): void
{
    // ...
}

Хорошо:

class MenuConfig
{
    public $title;
    public $body;
    public $buttonText;
    public $cancellable = false;
}

$config = new MenuConfig();
$config->title = 'Foo';
$config->body = 'Bar';
$config->buttonText = 'Baz';
$config->cancellable = true;

function createMenu(MenuConfig $config): void
{
    // ...
}

Функции должны делать одну операцию

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

Плохо:

function emailClients(array $clients): void
{
    foreach ($clients as $client) {
        $clientRecord = $db->find($client);
        if ($clientRecord->isActive()) {
            email($client);
        }
    }
}

Хорошо:

function emailClients(array $clients): void
{
    $activeClients = activeClients($clients);
    array_walk($activeClients, 'email');
}

function activeClients(array $clients): array
{
    return array_filter($clients, 'isClientActive');
}

function isClientActive(int $client): bool
{
    $clientRecord = $db->find($client);

    return $clientRecord->isActive();
}

Названия функций должны сообщать, что они делают

Плохо:

class Email
{
    //...

    public function handle(): void
    {
        mail($this->to, $this->subject, $this->body);
    }
}

$message = new Email(...);
// What is this? A handle for the message? Are we writing to a file now?
$message->handle();

Хорошо:

class Email 
{
    //...

    public function send(): void
    {
        mail($this->to, $this->subject, $this->body);
    }
}

$message = new Email(...);
// Clear and obvious
$message->send();

Функции должны быть только одним уровнем абстракции

Когда у вас есть более одного уровня абстракции, ваша функция обычно делает слишком много. Разделение функций приводит к повторному использованию и более простому тестированию.

Плохо:

function parseBetterJSAlternative(string $code): void
{
    $regexes = [
        // ...
    ];

    $statements = explode(' ', $code);
    $tokens = [];
    foreach ($regexes as $regex) {
        foreach ($statements as $statement) {
            // ...
        }
    }

    $ast = [];
    foreach ($tokens as $token) {
        // lex...
    }

    foreach ($ast as $node) {
        // parse...
    }
}

Тоже плохо:

Мы выделили некоторые функции, но функция parseBetterJSAlternative () по-прежнему очень сложна и не поддается тестированию.

function tokenize(string $code): array
{
    $regexes = [
        // ...
    ];

    $statements = explode(' ', $code);
    $tokens = [];
    foreach ($regexes as $regex) {
        foreach ($statements as $statement) {
            $tokens[] = /* ... */;
        }
    }

    return $tokens;
}

function lexer(array $tokens): array
{
    $ast = [];
    foreach ($tokens as $token) {
        $ast[] = /* ... */;
    }

    return $ast;
}

function parseBetterJSAlternative(string $code): void
{
    $tokens = tokenize($code);
    $ast = lexer($tokens);
    foreach ($ast as $node) {
        // parse...
    }
}

Хорошо:

Лучшим решением является перемещение зависимостей функции parseBetterJSAlternative().

class Tokenizer
{
    public function tokenize(string $code): array
    {
        $regexes = [
            // ...
        ];

        $statements = explode(' ', $code);
        $tokens = [];
        foreach ($regexes as $regex) {
            foreach ($statements as $statement) {
                $tokens[] = /* ... */;
            }
        }

        return $tokens;
    }
}

class Lexer
{
    public function lexify(array $tokens): array
    {
        $ast = [];
        foreach ($tokens as $token) {
            $ast[] = /* ... */;
        }

        return $ast;
    }
}

class BetterJSAlternative
{
    private $tokenizer;
    private $lexer;

    public function __construct(Tokenizer $tokenizer, Lexer $lexer)
    {
        $this->tokenizer = $tokenizer;
        $this->lexer = $lexer;
    }

    public function parse(string $code): void
    {
        $tokens = $this->tokenizer->tokenize($code);
        $ast = $this->lexer->lexify($tokens);
        foreach ($ast as $node) {
            // parse...
        }
    }
}

Не используйте флаги в качестве параметров функции

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

Плохо:

function createFile(string $name, bool $temp = false): void
{
    if ($temp) {
        touch('./temp/'.$name);
    } else {
        touch($name);
    }
}

Хорошо:

function createFile(string $name): void
{
    touch($name);
}

function createTempFile(string $name): void
{
    touch('./temp/'.$name);
}

Избегайте побочных эффектов

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

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

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

Плохо:

// Глобальная переменная, на которую ссылается следующая функция.
// Если бы у нас была другая функция, которая использовала это имя, теперь это будет массив, и он может сломать его.$name = 'Ryan McDermott';

function splitIntoFirstAndLastName(): void
{
    global $name;

    $name = explode(' ', $name);
}

splitIntoFirstAndLastName();

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

Хорошо:

function splitIntoFirstAndLastName(string $name): array
{
    return explode(' ', $name);
}

$name = 'Ryan McDermott';
$newName = splitIntoFirstAndLastName($name);

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

Не пишите глобальные функции

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

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

Плохо:

function config(): array
{
    return  [
        'foo' => 'bar',
    ]
}

Хорошо:

class Configuration
{
    private $configuration = [];

    public function __construct(array $configuration)
    {
        $this->configuration = $configuration;
    }

    public function get(string $key): ?string
    {
        return isset($this->configuration[$key]) ? $this->configuration[$key] : null;
    }
}

Загрузите конфигурацию и создайте экземпляр класса Configuration

$configuration = new Configuration([
    'foo' => 'bar',
]);

И теперь можете использовать экземпляр Configuration в своем приложении.

Не используйте шаблон Singleton

Синглтон — это анти-шаблон. Если перефразировать Брайана Баттона:

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

Плохо:

class DBConnection
{
    private static $instance;

    private function __construct(string $dsn)
    {
        // ...
    }

    public static function getInstance(): DBConnection
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    // ...
}

$singleton = DBConnection::getInstance();

Хорошо:

class DBConnection
{
    public function __construct(string $dsn)
    {
        // ...
    }

     // ...
}

Создайте экземпляр класса DBConnection и настройте его с помощью DSN.

$connection = new DBConnection($dsn);

И теперь вы должны использовать экземпляр DBConnection в своем приложении.

Инкапсулируйте условные обозначения

Плохо:

if ($article->state === 'published') {
    // ...
}

Хорошо:

if ($article->isPublished()) {
    // ...
}

Избегайте отрицаний в названиях

Плохо:

function isDOMNodeNotPresent(\DOMNode $node): bool
{
    // ...
}

if (!isDOMNodeNotPresent($node))
{
    // ...
}

Хорошо:

function isDOMNodePresent(\DOMNode $node): bool
{
    // ...
}

if (isDOMNodePresent($node)) {
    // ...
}

Избегайте условных конструкций

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

Плохо:

class Airplane
{
    // ...

    public function getCruisingAltitude(): int
    {
        switch ($this->type) {
            case '777':
                return $this->getMaxAltitude() - $this->getPassengerCount();
            case 'Air Force One':
                return $this->getMaxAltitude();
            case 'Cessna':
                return $this->getMaxAltitude() - $this->getFuelExpenditure();
        }
    }
}

Хорошо:

interface Airplane
{
    // ...

    public function getCruisingAltitude(): int;
}

class Boeing777 implements Airplane
{
    // ...

    public function getCruisingAltitude(): int
    {
        return $this->getMaxAltitude() - $this->getPassengerCount();
    }
}

class AirForceOne implements Airplane
{
    // ...

    public function getCruisingAltitude(): int
    {
        return $this->getMaxAltitude();
    }
}

class Cessna implements Airplane
{
    // ...

    public function getCruisingAltitude(): int
    {
        return $this->getMaxAltitude() - $this->getFuelExpenditure();
    }
}

Избегайте проверки типов (часть 1)

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

Плохо:

function travelToTexas($vehicle): void
{
    if ($vehicle instanceof Bicycle) {
        $vehicle->pedalTo(new Location('texas'));
    } elseif ($vehicle instanceof Car) {
        $vehicle->driveTo(new Location('texas'));
    }
}

Хорошо:

function travelToTexas(Traveler $vehicle): void
{
    $vehicle->travelTo(new Location('texas'));
}

Избегайте проверки типов (часть 2)

Если вы работаете с базовыми примитивными значениями, такими как строки, целые числа и массивы, и вы используете PHP 7+, и вы не можете использовать полиморфизм, но вы все еще чувствуете необходимость проверки типа, вам следует рассмотреть объявление типа или строгий режим. Он предоставляет вам статическую типизацию поверх стандартного синтаксиса PHP. Проблема с ручной проверкой типов заключается в том, что для этого потребуется столько дополнительных слов, что искусственная «безопасность типа», которую вы получаете, не компенсирует потерянную читаемость. Держите ваш PHP чистым, напишите хорошие тесты и получите хорошие ревью кода. В противном случае сделайте все это, но с строгим объявлением типа PHP или строгим режимом.

Плохо:

function combine($val1, $val2): int
{
    if (!is_numeric($val1) || !is_numeric($val2)) {
        throw new \Exception('Must be of type Number');
    }

    return $val1 + $val2;
}

Хорошо:

function combine(int $val1, int $val2): int
{
    return $val1 + $val2;
}

Удалите мертвый код

Мертвый код так же плох, как дубликат кода. Нет причин держать его в своей кодовой базе. Если это не вызывается, избавитесь от этого! В GIT все равно все останется, если вам вдруг что-то понадобится.

Плохо:

function oldRequestModule(string $url): void
{
    // ...
}

function newRequestModule(string $url): void
{
    // ...
}

$request = newRequestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');

Хорошо:

function requestModule(string $url): void
{
    // ...
}

$request = requestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');

 

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

два − два =