Привет, Хабр!

Наши коллеги из beeline cloud подкинули интересную статью для перевода про разработку на PHP, плохие практики и не только. Это история о том, как правила чистого кода могут подорвать его фактическое качество. Материал содержит много рассуждений на эту тему и будет полезен всем, кто только начинает свой путь в разработке. Приятного чтения!

Невыдуманная история одного разработчика

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

В потоке бесчисленных и одинаковых объявлений одно предложение действительно выделялось. Оно было написано как-то иначе. По-особенному, без всего этого рекламно-продающего посыла. Компания искала не просто разработчика, ей нужен был настоящий мастер своего дела. «Мы ценим чистый код», — заявляли они.

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

Фото: Ben Griffiths / Unsplash.com
Фото: Ben Griffiths / Unsplash.com

Оцениваем длину класса

Для понимания, мой основной язык — PHP, а вакансия предназначалась как раз для PHP-разработчиков.

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

Уже почти десять лет я не сталкивался с Java, но, если я правильно помню, там принято помещать открывающую скобку в конец строки. В PHP же принято выносить эту скобку в отдельную строку.

Вот простой пример:

public class Calculator {
    public double add(double number1, double number2) {
        return number1 + number2;
    }
}
class Calculator
{
    public function add(float $number1, float $number2): float
    {
        return $number1 + $number2;
    }
}

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

Вы скажете, что к длине класса такие вещи не имеют отношения. Но кому-то может всерьез показаться, что раз уж пустые строчки есть в файле, их тоже надо учитывать.

Что случится, если связать разработчику руки строгим правилом — не более ста строк кода на файл? Давайте посмотрим на возможные результаты.

Поощряем плохие практики

Определение функции в PHP, по стандарту, требует не менее четырех строк — одна для заголовка функции, две для открывающей и закрывающей скобок, и, по крайней мере, еще одна смысловая строка в теле.

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

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

Приведу здесь фрагмент из моей интеграционной библиотеки. Это часть класса, отвечающего за создание и управление доступом к удаленным ресурсам:

class Address implements AddressContract, Stringable
{
    //...

    /**
     * Checks if the collection contains parameters with
     * the given names and validates them. This method
     * accepts an arbitrary number of arguments, each of
     * which should be a string representing a parameter
     * name.
     *
     * @param string ...$names The names of the parameters
     * to check for.
     *
     * @return bool Returns true if all parameters are
     * found, false otherwise.
     */
    public function hasValid(string ...$names): bool
    {
        foreach ($names as $name) {
            if (!isset($this->parameters[$name]) || !$this->parameters[$name]->isValid()) {
                return false;
            }
        }
        return true;
    }

    //...
}

Для понимания контекста, Address в моей системе предусматривает такие параметры, как {id} в https://example.com/articles/{id}. Код проверяет, содержит ли экземпляр Address допустимые параметры.

Здесь есть весьма понятный PHPDoc (кстати, он еще в процессе разработки), а сам код прост в освоении. Он перебирает все указанные имена и проверяет, что каждое из них существует и является корректным. В противном случае возвращается false. Если оператор if не выдаст true, то на выходе тоже будет true.

А для экономии места я мог бы написать вместо этого такой код:

class Address implements AddressContract, Stringable
{
    //...

    public function hasValid(string ...$names): bool
    {
        return array_reduce($names, fn($carry, $name) => { return $carry && isset($this->parameters[$name]) && $this->parameters[$name]->isValid(); }, true);
    }

    //...
}

Ура, мы сэкономили 5 строчек!

Пусть сотрудники тратят время впустую

Зацикленность на длине класса попросту контрпродуктивна.

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

Хотя кто-то может возразить, что процесс подсчета строк можно автоматизировать, это обоюдоострый меч. Автоматизация может непроизвольно поспособствовать развитию дурной практики, поскольку разработчику наверняка захочется перехитрить систему.

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

При этом не стоит забывать, что автоматизация должна либо каким-то образом срабатывать отдельно, либо быть частью конвейера CI/CD.

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

Клубок из крошечных классов

Принудительное разбиение классов на составные части иногда может привести к непреднамеренному созданию того, что я люблю называть «крысиным королем»: клубка из крошечных классов, которые не могут жить друг без друга.

Это, пожалуй, самая распространенная проблема, и она требует к себе пристального внимания.

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

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

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

// You rarely interact with these two objects,
// except when injecting them into the next one.
FileInputStream fileStream = new FileInputStream(fileName);
BufferedInputStream bufferedStream = new BufferedInputStream(fileStream);

// You actually interact with this one.
ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);

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

Установление жесткого ограничения на длину класса может еще сильнее подтолкнуть к такой архитектуре.

Есть подходы и получше

Подсчет строк в классе сложно качественно отслеживать. Кроме того, он фактически ничего не говорит о качестве самого кода, только о его длине.

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

Возьмем в качестве примера класс Model в Laravel. Он насчитывает более 2,4 тыс. строк кода. Несмотря на то, что это может показаться огромным объемом, он намеренно спроектирован таким образом. Такой подход гарантирует, что разработчики смогут extend'нуть базовый класс Model, и вуаля, их собственная модель работает без лишних усилий.

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

Эта статья написана под влиянием моего личного опыта и книги Джона Оустерхаута «Философия проектирования программного обеспечения». Эта книга посвящена глубокому изучению вопросов создания хорошего кода и является обязательной к прочтению для всех, кто всерьез занимается разработкой программного обеспечения.

beeline cloud — secure cloud provider. Разрабатываем облачные решения, чтобы вы предоставляли клиентам лучшие сервисы.

Больше материалов о разработке читайте здесь:

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


  1. DeadMaster
    20.10.2023 14:46
    +5

    Не всегда стоит всё понимать буквально.


  1. zubrbonasus
    20.10.2023 14:46
    -1

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


  1. Senyaak
    20.10.2023 14:46

    Я думал ужас вроде оплаты по количеству символов итд страшные сказки из прошлого, а оказывается такое ещё попадается????


  1. olku
    20.10.2023 14:46

    Метрика Cognitive Complexity, пожалуй, самая человечная из всех синтетических.


  1. Politura
    20.10.2023 14:46
    +4

    С PHP не знаком, возник вопрос про этот абзац:

    Возьмем в качестве примера класс Model в Laravel. Он насчитывает более 2,4 тыс. строк кода. Несмотря на то, что это может показаться огромным объемом, он намеренно спроектирован таким образом. Такой подход гарантирует, что разработчики смогут extend'нуть базовый класс Model, и вуаля, их собственная модель работает без лишних усилий.

    Может кто-нибудь пояснить, что такое экстенднуть и почему это можно делать без проблем только с безумно огромными классами, а на классах небольшого размера нужны усилия?


    1. hello_my_name_is_dany
      20.10.2023 14:46

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


      1. PrinceKorwin
        20.10.2023 14:46

        Обычно наследуют чтобы поменять поведение метода и если в нем 2.4к строк, то ничего и не поменяешь. Я не знаю Laravel, а там зачем наследуют?


        1. FanatPHP
          20.10.2023 14:46

          Чтобы понимать, зачем наследовать класс модели, не нужно знать ларавель.


      1. Politura
        20.10.2023 14:46
        +4

        Я вам честно скажу: за четверть века практики я видел всякие классы, и на десятки тысяч строк и маленькие. Так вот, когда классы делаются под конкретную изолированную задачу, тобишь достаточно маленькие, то такая архитектура в миллион раз легче для понимания, чем ковырять архитекруру построенную на всего нескольких, но god class с тысячами строк и сотнями методов, в попытках понять какого хрена здесь все так запутанно.

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


        1. Anarchist
          20.10.2023 14:46

          "по пальцам одной головы" :)


      1. zubrbonasus
        20.10.2023 14:46
        -1

        В PHP наследование реализовано способом расширения класса потомка, классом родителя. Расширение (extends) обозначает включение всех методов и свойств в класс наследник. Методы можно переопределить.

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


      1. shasoftX
        20.10.2023 14:46
        +1

        В том же PHP есть trait-ы для эмуляции множественного наследования. Можно выносить группы функций в отдельные trait-ы которые подключать в основном классе. Для понимания общей работы никакой разницы не будет. Зато для понимания работы группы функций так, на мой взгляд, будет информативнее.


        1. MyraJKee
          20.10.2023 14:46

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


          1. Anarchist
            20.10.2023 14:46

            В джаве их не сделали по причине "ромбов смерти". Сейчас в интерфейсах допустимы тела методов.


            1. MyraJKee
              20.10.2023 14:46

              Я вроде и не говорил чего-то обратного

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


        1. eandr_67
          20.10.2023 14:46
          +1

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


  1. isNikita
    20.10.2023 14:46

    Если я правильно помню, в "чистом коде" было что-то типа правило 5-7. То-есть максимум 5-7 методов в классе, максимум 5-7 строк в методе и т. д. А это как-раз до 100 строчек кода.


    1. ptr128
      20.10.2023 14:46

      Не знаю, как в php, но в Java, C++ или C# несколько десятков методов в базовых классах - обычное явление. И при наследовании количество методов лишь нарастает.


    1. kapitalistka
      20.10.2023 14:46

      В чистом коде также регламентировпна и ширина строки: она должна влезать в экран.


  1. MyraJKee
    20.10.2023 14:46
    +1

    Кажется что не должно быть сокращения количества строк, ради сокращения количества строк. Это же бред какой-то.

    Время от времени работаю с проектом на yii, в котором десятки модулей, сотни или даже тысячи сервисов, десятки тысяч классов. Каждый класс сам по себе не большой. Но где-то 95% из них наследуются от какой-нибудь абстракции, которая тоже может наследовать абстракцию, которая реализует какой-то интерфейс. Модели, формы, контроллеры. Какая-то бизнес-логика. Чуть ли не Дтошки и вообще всё всё всё.

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


    1. FanatPHP
      20.10.2023 14:46

      Да, это проблема, и лично мне разбираться в таком тоже сложно. Но разница в том, что в таком варианте можно поменять поведение отдельных элементов системы, не задевая остальные. А в "монолитном" варианте придется дублировать код, со всеми вытекающими.


    1. arTk_ev
      20.10.2023 14:46

      В чистом коде наследование запрещено


  1. arTk_ev
    20.10.2023 14:46

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

    Нет ни одной причины делать лапшу-код, это всегда долго, дорого.


  1. KingPin_AK
    20.10.2023 14:46

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