Как стать автором
Обновить

Комментарии 59

п.2 нарушает буковку D в аббревиатуре SOLID. Что побудило вас настолько явно нарушать наши религиозные догмы?

Еретик-с.

Не нарушает, в пункте речь про конфигурацию di, а не код, видимо не очень чётко сформулировал мысль

читал бегло, но
4. Каждый класс закрывается интерфейсом - сильно перегрузит код. Интерфейсы лучше использовать, когда это действительно нужно.
5. Чем меньше методов в интерфейсе, тем лучше.- плохой пример, т.е. в данном случае - то что плохо, как раз нормально, а что хорошо - плохо.
7. Максимально строгая типизация. - у php нет перегрузки функций, поэтому mixed вполне можно использовать где нужно.
8. Флаги в аргументах функций - они обычно появляются в процессе доработки, чтобы избежать дублирования кода. Если там не куча флагов, то ничего страшного не вижу.
17. Не работаем с ассоциативными массивами, только с объектами. - а почему, чем плохо? Мне кажется массивы более "легкие", чем объекты..
15. Композиция, а не наследование - тоже можно поспорить - смотря как и где использовать.

т.е. вы написали что хорошо делать так, а так делать плохо - а почему?

т.е. вы написали что хорошо делать так, а так делать плохо - а почему?

Думаю "так делать плохо" потому, что иначе у автора ломается внутренний комфорт и чувство прекрасного.

9. Количество аргументов в функции максимум 3

А, что если у меня в функции описана сложная формула для расчета результата по огромной массе входящих значений?
Все? Закрывать лавочку и уходить из программирования?

А что если у меня в функции описана сложная формула для расчета результата по огромной массе входящих значений?

Разбить эти аргументы на группы, каждая в своем DTO/VO.

Зачем это делать? Параметры функций вполне типизируемы и так.

При чем здесь типизация?

При том что типизация и именование аргументов решают ту же задачу что и ДТО которое вы предлагаете. Так зачем городить лишнюю абстракцию в виде ДТО если можно обойтись стандартными средствами?

а почему, чем плохо? Мне кажется массивы более "легкие", чем объекты..

если массив $data получен из клиента $_POST, $_GET итд то лучше всё таки объект, т.к в большинстве фреймворков он хотя бы проверяется на инъекции.

в большинстве фреймворков он хотя бы проверяется на инъекции

Эээ... А можно чуть подробнее? Как именно проверяется и на какие конкретно "инъекции"?

В laravel например вадидатор запроса возвращает массив. Предлагаете его в обьект пихать? Зачем?

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

сильно перегрузит код

Не очень понятный аргумент. интерфейс сидит в своем файле и никому не мешает. В каком смысле "перегрузит"?

Максимально строгая типизация. - у php нет перегрузки функций, поэтому mixed вполне можно использовать где нужно.

Эти два утверждения вообще никак не противоречат друг другу. Да, должна быть максимально строгая типизация. Да, где нужно, mixed вполне можно использовать.

 Если там не куча флагов, то ничего страшного не вижу.

Это понимание приходит со временем. Которое потратил разбор кода, который сам же писал полгода назад.

Мне кажется массивы более "легкие", чем объекты..

Да при чем здесь "легкость". Уже одного автодополнения достаточно. Не говоря уже про автоматические проверки целостности. Если вбил в массив неправильный ключ, то никто не заметит. А объект уже (теперь) такого не даст. Плюс типизация элементов. Тут целый мир разницы, между старым шаляй-валяй и прекрасным новым пхп Никиты Попова.

"Не очень понятный аргумент. интерфейс сидит в своем файле и никому не мешает. В каком смысле "перегрузит"? "

а зачем он тогда нужен?

"Эти два утверждения вообще никак не противоречат друг другу. Да, должна быть максимально строгая типизация. Да, где нужно, mixed вполне можно использовать."

кому должна, вам? вообще-то я тоже самое написал, кроме "должен"

"Это понимание приходит со временем. Которое потратил разбор кода, который сам же писал полгода назад."

приходит с опытом, а не временем.

"Да при чем здесь "легкость". Уже одного автодополнения достаточно... "

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

вы чего своим комментарием сказать-то хотели?

Интерфейс нужен что бы определить контракт. Не понимаю как фраза "сидит в своём файле" могла вызвать вопрос "зачем он нужен?"

такое ощущение, что общаюсь с детьми, которые нахватались умных слов, "модных" методик и теперь пытаются их везде впихнуть, просто потому, что это "модно и правильно" не разбираясь в сути. Интерфейсы, если простыми словами, нужны для передачи/получения определенных данных от разных источников (н-р: подключение базы, где метод connect может быть описан в интерфейсе, а разные классы от него уже делают подключение для mysql, mssql, postgesql и т.д.), также для передачи классов по интерфейсу, где также определяется поведение, в зависимости что передается.

зачем интерфейс этому классу:


class A(){
public Print(string $msg){
echo $msg;
}

чтобы что? и таких классов большинство. интерфейсы нужны там, где нужны и не нужны там где не нужны.

Что за скрытая агрессия не понимаю.

При чём тут модно/не модно? Есть терминология и люди ей пользуются. Вы же не говорите "эта штука при помощи которой ты дышишь". Вы говорите нос, лёгкие и т.д. Так и тут.

Никто вас переубедить не пытается потому что это на**р никому не надо. Уж я тем более.

Касательно класса. Там дальше давали ссылку на комментарий где в ветке говорится про стабильные и волатильные зависимости. Советую почитать. Лично я противник interface obsession, но использовать их безусловно необходимо.

И помните, только ситхи всё возводят в абсолют.

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

фанатPHP - ребенок, который минусит тех, кто с ним не согласен и наоборот. думал вы лучше объясните.

Если каждый класс реализует интерфейс, а использующие его классы зависят от него, а не от реализации, то:

1) Легче писать тесты замокав эти интерфейсы.
2) По мере развития кода очень просто на основе контракта, описанного в интерфейсе, подменить реализацию на новую или использовать несколько разных реализаций.

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

а чем это расходится с моимии рссужудениями.

Легче писать тесты замокав эти интерфейсы.

Класс мокается точно так же, как и интерфейс.

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

Ну а почему нельзя это сделать когда у вас действительно появится новая реализация? Зачем надо делать это заранее и постоянно поддерживать?

Интерфейсы, если простыми словами, нужны для передачи/получения определенных данных от разных источников

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

Да, плюсую. А теперь попробуйте это объяснить молодым программистам, которые всё знают.. потому что.. бам-бам - они всё знают..) так дядя Федор сказал..

Всем читать A Philosophie of Software Design by Ousterhout.

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

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

Когда вы один интерфейс растащили по 5 вы убили высокоуровневую абстракцию.

Не совсем понял что здесь подразумевается, но если имеется ввиду, что «если сделать из 1 интерфейса на 5 методов, 5 интерфейсов по 1 методу, то это убьет высокоуровневую абстракцию», то в корне с этим не согласен

  1. Массивы передаются по значению, а объекты по ссылкеДумаю причина в этом.

Это как раз не имеет значения.

Я заметил одну вещь - здесь на хабре чаще можно встретить упоминание PHP, Go, Java, JavaScript Python, Kotlin, С/C++ и даже прости господи 1С, но вот С# остается как бы в тени, с чем это связано? Его не часто используют?

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

А если хотите читать про шарпы, то подпишитесь на соответствующий хаб, трафик там сравним с пхпешным.

Сначала придумываем себе проблемы, потом героически их решаем. Всё по маркетологовски, как учил святой скилбокс.

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

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

В DI подставляем реализации, а не интерфейсы

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

Не надо пробрасывать в классы весь контейнер целиком

Почему? Тут есть утверждение без доказательства.

5. Чем меньше методов в интерфейсе, тем лучше.

Большие интерфейсы сложнее поддерживать и реализовать. Нарушается принцип единственной ответственности.

Тут нарушается не SRP, а ISP. SOLID явно определяет принцип разделения интерфейсов.

В интерфейсах указываем какие exception могут быть выброшены

Весьма общее требование. Например, в слое адаптеров реализуется обращение к мессенджерам. По-хорошему, мессенджеров может быть много, они будут добавляться. Допустим, у нас есть задача рассылать в Slack, Telegram, WhatsApp и еще куда-то. У каждого мессенджера может быть, скажем, внешняя библиотека со своими кастомными исключениями. Перечислять их все в интерфейсе клиента и пополнять каждый раз?

Используем strict_types=1. Типы указываем максимально жестко.

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

Если аргументов много - значит метод выполняет слишком много действий

Эта штука идет из "Чистого кода" Роберта Мартина. Принятие более 3 аргументов еще не означает, что метод обязательно нарушает SRP. Тут утверждение очень общее. Присмотреться к такому методу при рефакторинге, разумеется, стоит. Тут уже речь о code smell. Вероятно, стоит заменить параметры вызовами внутри или передачей объекта.

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

Не мутировать объекты переданные в метод/функцию

Так тут проблема скорее в том, что мутировать объекты в принципе можно. Свойства и методы в публичном доступе - это доступный контракт. Значит, либо его забыли инкапсулировать, либо к нему таки можно обращаться.

Заполнение DTO через конструктор с именованными аргументами

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

Класс меньше ~150 строк, метод меньше ~30 строк. Это значительно упрощает чтение кода.

Классы и методы не должны определять свое качество количеством строк. Они должны руководствоваться принципами, такими, как, например, SOLID, GRASP. Да, как я писал выше, размер класса - это повод к нему присмотреться. Но не повод сразу идти его рефакторить.

На любой код обязательно пишем unit тест

Тут рекомендую почитать вот эту статью. "Обязательно" - термин огульный. 100% покрытия в хотя бы среднем проекте не будет. Да оно и не требуется. Особенно, если помимо unit-тестов есть другие составляющие пирамиды тестирования.

15. Композиция, а не наследование

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

По умолчанию указываем final для классов

И еще пара полезных статей: раз, два. И рекомендую обратить внимание вот на этот комментарий

Про пункт DI. Мне кажется автор некорректно описал то, что имел ввиду. Я вот вижу, что его конструкторы принимают интерфейсы (что и требуется). А вот, в конфигурации контейнера он предпочитает ставить реализации (это конфигурация кода, а не сам код). И его можно понять - если везде прокидывать интерфейсы, то не получится в разные классы прокинуть разные реализации.

Да, я потому и пишу, что тут решение из разряда it depends. Не хватает более широкого обоснования, почему принимается именно такой выбор.

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

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

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

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

Ну вот у вас есть драйвер локальной файловой системы и драйвер FTP. Второй может выбрасывать NetworkException, а первый нет. Что во что надо кастить? При этом вызывающий код по NetworkException может попробовать повторить операцию, а по AccessDeniedException в этом нет смысла.

namespace App\Infra\Storage

interface Storage { 
    /**
     * @throws StoreException
     */
    public function store(): void
}

abstract class AbstractStorage implements Storage {
    /**
     * @throws StoreException
     */
    abstract public function store(): void;
}

class StoreException extends RuntimeException { ... }

class LocalStorage extends AbstractStorage {
    public function __construct(\ExternalImplLocalStorage $externalClass) {
        $this->smth = $externalClass;
    }
    public function store(): void {
        try {
            ...
        } catch (\Throwable $t) {
            ...
            throw new StoreException(1, ..., $t);
        }
    }
}
class FtpStorage extends AbstractStorage {
    public function __construct(\ExternalImplFtpStorage $externalClass) {
        $this->smth = $externalClass;
    }
    public function store(): void {
        try {
            ...
        } catch (NetworkException $t) {
            ...
        } catch (\Throwable $t) {
            ...
            throw new StoreException(1, ..., $t);
        }
    }
}

либо

namespace App\Infra\Storage

interface Storage { 
    /**
     * @throws StoreException
     */
    public function store(): void
}

class SomeService {
    public function __construct(Storage $engine) {
        $this->engine = $engine;
    }
    /**
     * @throws StoreException
     */
    public function store(): void {
        ... 
        $this->engine->store();
        ...
    }
}

class StoreException extends RuntimeException { ... }

class LocalStorage extends \ExternalImplLocalStorage implements Storage {
    public function store(): void {
        try {
            ...
        } catch (\Throwable $t) {
            ...
            throw new StoreException("...", 1, $t);
        }
    }
}
class FtpStorage extends \ExternalImplFtpStorage implements Storage {
    public function store(): void {
        try {
            ...
        } catch (NetworkException $t) {
            ...
        } catch (\Throwable $t) {
            ...
            throw new StoreException("...", 1, $t);
        }
    }
}

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

Под такого рода зависимостями понимаются любые зависимости, реализующие "один из" вариантов - как в примере реализация сохранения чего либо (local/ftp/webdav/s3/etc.), реализация брокера сообщений (rabbit/kafka/etc.), реализации отправки сообщений (slack/telegram/sms/email/etc.), реализации интеграций с платежными системами и так далее.

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

Там где catch (NetworkException $t) можно ретраить, уведомлять, что угодно делать. Можно прокидывать его 3м аргументом во внутренее исключение и уровнем выше делать getPrevious() и так далее.

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

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

Там где catch (NetworkException $t) можно ретраить, уведомлять

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

и уровнем выше делать getPrevious()

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

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

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

Так это находится в FtpStorage, который часть библиотеки

В написанных примерах, это не часть библиотеки

Ну а тогда зачем нам StoreException, какая от него польза?

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

Также в этом случае в интерфейсе не задокументировано, что может быть в getPrevious()

И не должно быть более того, что предоставляет интерфейс \Throwable - задача разбираться с внутренними исключениями библиотек лежит на обертках этих библиотек - в примерах это AbstractStorage/LocalStorage/FtpStorage

В написанных примерах, это не часть библиотеки

Так разговор то был про внешние библиотеки. Вы же сами процитировали это предложение. Именно в этом и проблема с описанием исключений в интерфейсах.

код точно знает какое исключение будет при работе с хранилищем

Так нам это знание бесполезно. Я и так знаю, что любое исключение при работе с интерфейсом хранилища можно считать StoreException. Я могу ловить просто Throwable, результат будет тот же самый. Точно знать надо конкретные исключения, например когда мы хотим задать конкретную обработку именно для NetworkException. А если любое исключение это StoreException, то в какой ситуации это полезнее, чем просто Throwable?

задача разбираться с внутренними исключениями библиотек лежит на обертках этих библиотек - в примерах это AbstractStorage/LocalStorage/FtpStorage

Нет, LocalStorage и FtpStorage это функциональность сторонней библиотеки. Именно за этим мы ее и подключаем. Они реализуют один интерфейс. Вы говорите, что авторы этой библиотеки должны в этом интерфейсе указывать только StoreException. А это может быть не всегда быть удобно, часто нам нужно обработать конкретные ошибки, которые может выдавать конкретная реализация.

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

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

def calc_polynom(a, b, c, d):
  return a * x**3 + b * x**2 + c * x + d

Аж четыре аргумента! А максимум допустимо три. Что предлагается сделать? Несколько дополнительных абстракций ввести? Интересно глянуть на рефакторинг.

Можно написать функцию, которая считает значение многочлена степени n и передавать один массив коэфов:)

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

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

Просто многие с фанатизмом относятся к подобным рекомендациям из книжек, тем более они сформулированы так, как будто это жесткие постулаты (5 строк кода в функции и точка!).

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

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

Точно, предлагаю пойти дальше и во все функции передавать один аргумент foo(…) - который произвольное количество аргументов. :)

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

Давно отменили. Есть же оператор распаковки аргументов.

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

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

Позволяет разрабатывать отталкиваясь от интерфейса.

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

С интерфейсом не работает функция IDE "Go to definition", приходится постоянно еще раз нажимать сочетание для "Go to implementation", чтобы перейти к реализации. Еще при изменениях сигнатуры методов надо в интерфейсе тоже обновлять. Это усложнение поддержки, а не упрощение.

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

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

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

Чем меньше методов в интерфейсе, тем лучше.

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

Вот есть у меня какая-то библиотека для HTTP запросов, они сделали себе MyHttpClient\NameAwareInterface, и что, я должен в бизнес-логике использовать зависимость от MyHttpClient? А если нет, у меня будет 2 интерфейса NameAwareInterface, а может и 10, фиг поймешь где какой использовать.

Интерфейс должен описывать методы управления и взаимодействия. Принципы группировки методов в интерфейс такие же, как и группировки методов в класс.

В интерфейсах указываем какие exception могут быть выброшены

Говорят, это пробовали в Java, и решили, что это была ошибка.
Драйвер локальной файловой системы не может выбрасывать NetworkException, а драйвер Amazon S3 может. Что в интерфейсе писать?

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

Если аргументов много - значит метод выполняет слишком много действий

Нет, не значит.

Класс меньше ~150 строк, метод меньше ~30 строк. Это значительно упрощает чтение кода.

Нет, не упрощает. Я читаю не просто так, а чтобы понять логику. Если она разбросана по разным местам, то понимать ее сложнее. Разбивать надо по логическим границам, а не по количеству.

По умолчанию указываем final для классов. Такое правило позволяет избежать проблем с наследованием и поощряет использование композиции.

Зато делает невозможным моки в тестах, из-за чего надо использовать интерфейсы где они не нужны.
Лучше сделать правило для code sniffer, что наследование идет только от абстрактного класса.

Соблюдаем закон Деметры
$this->clientDecorator->send();

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

Любой дополнительный класс надо поддерживать, любая абстракция усложняет понимание кода, поэтому такой подход усложняет поддержку. Абстракция упрощает поддержку когда она содержит логику и используется в нескольких местах (или будет использоваться), тогда сложность из этих мест переносится в абстракцию.

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

Вот вроде все хорошо, но от первого пункта хочется пулю пустить в лоб

Зачем тогда вообще использовать контейнер, если не использовать автовайринг? Создавай все сам тогда при инициализации приложения

Если мне надо сделать сервис, в который я проталкиваю логгер, то на кой черт я должен лезть в конфиг и явно указывать какой логгер прокидывать? Мне любой сойдет, дефолтного будет достаточно в 99.9% случаев. Та же история с прокидыванием репозиториев.

Хотя, если вам платят за кол-во строк, то пожалуйста

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

Чем это читается хуже?

class MyService {
  __constructor(
    private EntityManager $manager,
    private Dependency $dependency,
  ) {}
}

зачем это дважды читать? в коде и еще раз в yaml настройках?

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

Cоветы в целом полезные, я бы примеры поправил на более показательные.

Сгруппировал по степени принятия

Категория "мегаимбовая гиперзачетная убербаза"

  • Не надо пробрасывать в классы весь контейнер целиком

  • Не мутировать объекты переданные в метод/функцию

  • Тестируем поведение, а не детали реализации

  • Композиция, а не наследование

  • Соблюдаем закон Деметры

  • Чем меньше методов в интерфейсе, тем лучше (Только это не про SRP, а про Interface Segregation)

  • Unit тесты всегда

Категория "Чую, куда ветер дует"

  • Максимально строгая типизация

  • Заполнение DTO через конструктор с именованными аргументами, readonly свойствами, без get/set методов

  • По умолчанию указываем final для классов

  • Не работаем с ассоциативными массивами, только с объектами

Категория "Благие намерения"

  • В DI подставляем реализации, а не интерфейсы

  • В интерфейсах указываем какие exception могут быть выброшены

  • Минимальная цикломатическая сложность

  • Количество аргументов в функции максимум 3

Категория "Автор что-то имел сказать"

  • Зависимости в di указываем явно.
    Явные зависимости в конструкторе класса, autowiring не разу не создавал проблем.
    Хочу конкретный пример, который убедит меня использовать искусственный идентификатор app.services.my_service в замен натуральному

  • Каждый класс закрывается интерфейсом
    Смущает слово "каждый". Это как делать настоящий самолет из лего -
    мы выкидываем жёсткую связь даже там, где она, прикиньте, бывает полезна.

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

  • Флаги в аргументах функций
    Смысл есть, но пример с всего лишь 1 флагом не вдохновляет на подвиги. Также как пример про большие классы - негуманно делать из этого правила (то что соблюдают?), это скорее небольшой notice на будущее.

Статья в целом понравилась: материал изложен доступно. Однако, стоит отметить, что в тексте чувствуется влияние взглядов Ричарда "Религиозного фаната" Мартина, что придает некоторую радикальность.

Это как минимум видно из :

  • "Флаги в аргументах функций" - тут я на 100% согласен, Мартин кстати очень хорошо это аргументировал, тут бы примеров по больше.

  • "Количество аргументов в функции максимум 3" - тоже самое, Мартин кстати отдельно упоминает что есть исключения из этого "правила"

  • "Не мутировать объекты переданные в метод/функцию" - тут я тоже полностью согласен. Видимо у автора тоже был негативный опыт

  • "Тестируем поведение, а не детали реализации" - очень очень давно помоему описали еще в TDD помоему Кент Бек (но могу ошибаться).

Описывать интер

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

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

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

которые никто не заставляет применять

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории