Для кого статья

Вы уже написали свои первые 1000 строк кода и сейчас хотите сделать их понятнее, потому что внесение изменений занимает столько-же времени, сколько написать заново, но советы из ООП, SOLID, clean architecture и т.д. непонятны вам.

О чем статья

Эта статья - не объяснение принципов ООП, SOLID своими словами, а попытка создать промежуточный уровень между никакой и чистой архитектурами. 100% советы будут накладываться друг на друга и перефразировать SOLID, но так даже лучше.

От кого статья

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

Отказ от ответственности

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

Формат статьи - наводящие советы / вопросы.

Содержание:

  1. К чему относится функция.

  2. Как вы будете модернизировать одну функцию, не затрагивая другую.

  3. На сколько логических частей я могу раздробить мою функцию?

  4. Повторяющиеся слова в названиях функций / переменных.

  5. Что является центральными объектами вашего кода.

  6. На какие аналогичные функции может быть заменена ваша функция?

  7. Как выглядит идеальный псевдокод вашей функции?

  8. Обращайте внимание на формат данных.

  9. Отдавайте предпочтение пространству имен, а не ветвлениям.

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

Совет номер 1

Когда пишете код и не знаете как его организовать - задайте себе вопрос следующего типа:
“К чему относится моя функция?” / “К чему относится этот функционал?” / “За что отвечает этот функционал?”
Попробуйте мысленно проставить хэштеги вашей функции:
#обработка, #валидация, #проверка, #БД, #отображение.
Безусловно, запрос к БД может являться частью обработки, но он же в будущем может использоваться и для другой функции,
даже если пока написан только для этой.
Ремарка: Вообще в разработке уже есть устоявщийся набор таких тегов, некоторые из них: validate, check, get, set, show, load, send. Сюда же входит CRUD и HTTP заголовки.

Совет номер 2

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

Небольшие изменения не должны существенно затрагивать другие функции.

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

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

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

Совет номер 3

“На какие части я бы разделил этот функционал?”, “На какие еще подфункции можно разделить код этой функции?”.

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

def get_product_price():
… # Здесь код

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

  1. Применить общую формулу процентов. - Та самая атомарная операция.
    Раздробить это действие уже не получится.

  2. Применить ограничения к цене.
    Товар не может стоить меньше, чем похожий товар из прошлогодней коллекции и т.п.

  3. Применить скидку. Скидка не может быть отрицательной, больше 100%, и т.п.

Две функции ниже могут быть общими для всего проекта и находиться в модуле "util.py".
Классы могут использовать эти функции под разными и аргументами, делая обертку вокруг них.

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

# (Не очень удачное название)
def calculate_percentage_number(number: int, percentage: int) -> int:
    return number * (percentage / 100)


def limit_number(number: int, min_: int, max_: int, ) -> int:
    """Вернет число или ограничение числа."""
    return min(max(min_, number), max_)


def get_product_price(price: int, discount: int, ) -> int:
    min_discount = 10  # Лучше поместить внутрь класса    
    max_discount = 20  # Лучше поместить внутрь класса
    discount = calculate_percentage_number(number=price, percentage=discount, )
    discount = limit_number(
        number=discount,
        min_=min_discount,
        max_=max_discount,
    )
    discounted_price = price - discount
    if 0 < discounted_price < price:
        return discounted_price
    # Игнорируем скидку в случае ошибки. 
    logger.log(DiscountError)
    return price  # Более разумным будет применить базовую скидку.

Обратите внимание как меняются имена переменных в зависимости от контекста,
price -> number, discount -> percentage.

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

Разумеется, не нужно сразу дробить ваш функционал на 1000 частей, далеко не все вам понадобится (принцип YAGNI), но вы должно быть к этому готовым.
Подсказка: Для процентов можно создать отдельный тип, что бы не путать с обычными числами.

Совет номер 4

Обратите внимание на повторяющиеся "user" в названии функций.

def get_user_data():    
    ...


def notify_user_friends():    
    ...


def create_user_post():    
    ...

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

Ремарка: Лично я считаю, что инструкция "class" в пайтоне перегружена,
это и пространство имен, и структура данных, и сами классы собственно.

Лучше будет:

class User():
    def get_data():
        ...

    def notify_friends():
        ...

Совет номер 5

ООП вращается вокруг объектов / сущностей / моделей, которые определяет бизнес / работодатель.

В проекте условного мессенджера класс "сообщение" будет большим,
а в проекте про такси класс "сообщение" будет куда меньше, зато будет большой класс "автомобиль".

Определите для себя, какие классы в вашем проекте центральные и наполняйте их методами.

Ремарка: возможно в ближайшем будущем какой-нибудь ИИ создаст универсальную структуру для каждого объекта на земле и в каждом проекте будут одинаковые объекты, но скорее всего ИИ просто научится программировать лучше нас, без всякой организации кода :)

На моей практике начало любого проекта это небольшой набор стандартных функций и классов, например:
View, DB, User, Mail. Они используются для общих целей.
Очень быстро в сервисе такси класс Taxi перерастет остальные классы и будет иметь собственный метод приветствия.

def some_func(user: User):
    ...
    View.say_hello(name=user.name, )  # Общее приветствие.
    taxi.say_hello(name=user.name, )  # Приветствие от конкретного такси.
    ...

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

Общий метод say_hello помещается в общий класс View,
а вот taxi_say_hello в класс Taxi.

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

Ремарка: насколько я знаю, подход MVC (Model-View-Controller) имеет как сторонников, так и противников.

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

Совет номер 6

На что я МОГУ заменить свою функцию / класс?

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

В какой-то момент вы решили сменить этот фреймворк.

Старый фреймворк:

recipient = BarMailAgent.serialize_recipient(recipient=...) 
FooMailAgent.send(text=self.get_txt_data(), recipient=..., retry=3, delay=10)

Новый фреймворк:

# recipient serialization already inside the method
BarMailAgent.send(message=self.get_txt_data(), email=..., attempts=3, interval=10)

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

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

class User:
    def send_email(self, version: int = 1, arguments=...):
        if version == 1:
            recipient = BarMailAgent.serialize_recipient(recipient=...)
            FooMailAgent.send(text=self.get_txt_data(), recipient=..., retry=3, delay=10)
        else:
            # recipient serialization already inside the method
            BarMailAgent.send(message=self.get_txt_data(), email=..., attempts=3, interval=10)

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

Совет номер 7

Напишите сперва идеальный псевдокод, как в идеальном случае должен выглядеть ваш код, пример:

def register(user: User):
    user.validate()
    user.save()
    logger.log(event=events.registration, entity=user, )
    mail.send(event=events.registration, recipient=user.email, )
    notifier.notify(event=events.registration, recipients=user.possible_friends, )
    statistics.add_record(event=events.registration, recipient=user.email,)

Ремарка: Я пользуюсь правилом: 1 строчка - 1 действие.

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

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

Где-то снаружи код может выглядеть так:

def register_handler(update, context):
    try:
        events.register(user=context.user)
    except Exceptions.Registration.ValidationError:
        # Где-то внутри будет: "400. Увы, вы ввели некорректные данные, мы не можем сохранить такого пользователя."
        events.fails.registration(user=user)
    except Exceptions.Registration.DbError:
        # Где-то внутри будет: "503. Внутренняя ошибка, приносим свои извинения."
        events.fails.registration(user=user)

Должен отметить, что этот код вызывает у меня самого несколько некритичных сомнений:

  1. Должен ли блок try/except быть снаружи метода "register"?

  2. Можно ли упаковать "user" в "events.registration"?

  3. Нужно ли передавать целиком пользователя или только необходимые атрибуты?
    С 1-ой стороны это делает код очевиднее, с другой - при изменении необходимого набора - придется больше писать.
    Я для себя пришел к такому компромиссу:
    Если атрибут неотъемлемая часть объекта (почта, телефон, айди) - передаем объект целиком, иначе - только атрибут.

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

Совет номер 8

Обращайте внимание на формат данных.

Какой-нибудь фреймворк может передавать на вход вашим обработчикам объект под названием event / update.

Функции проверки из этого объекта нужен только атрибут "user",
а базе данных из этого объекта нужен только атрибут "ID" или "role".

Т.е. условная проверка прав доступа может выглядеть так:
update / event - передано в обработчик.
update.user - передано в функцию проверки.
user.id - передано в запрос к базе данных.

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

Мои функции валидации / проверки не зависят от формата данных предоставленных фреймворком.

Совет номер 9

Отдавайте предпочтение пространству имен, а не ветвлениям.
Каждая ветка if/else усложняет код, создает потенциальную возможность ошибки и усложняет тестирование.

Ремарка: в архитектуре существуют метрики сложности кода, чрезмерное ветвление ухудшает показатели.

Теоретически, все API можно написать на ветвлениях, но не нужно:

def gloabal_handler(request):
    if request.url == 'settings':
        ...
    elif request.url == 'photos':
        ...

Отдавайте ветвления на откуп ЯП, ведь в конечном счете пространство имен можно представить как:

for key in namespace:
    if key == dot_value:
        return key

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

Совет номер 10

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

Здесь аргумент 'hello' всегда одинаковый, он не несет никакой полезной нагрузки при анализе кода, в 9 / 10 случаях при чтении кода мы НЕ хотим концентрировать свое внимание на том, какой текст отправляется, но код ниже заставляет нас это делать.

Легко читаемая функция, но может быть еще проще.

# Используйте переменную-константу вместо 'hello'
bot.send_message(text='hello', recipient=user.id, )

Краткость - сестра таланта.

View.say_hello(recipient=recipient, ) # bot.send_message внутри

Благодарю за внимание.

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


  1. DavidGolifox
    09.01.2023 16:23

    предпочтение простарнству имен

    устаявщийся набор таких тегов

    не путать с обчыными числами.

    "503. Внутрення ошибка,

    Исправьте пожалуйста.


    1. david-shiko Автор
      09.01.2023 17:15
      +1

      Благодарю, исправил.


  1. amazed
    09.01.2023 16:25
    +24

    Вы уже написали свои первые 1000 строк кода и сейчас хотите сделать их понятнее, потому что внесение изменений занимает столько-же времени, сколько написать заново, но советы из ООП, SOLID, clean architecture и т.д. непонятны вам.

    Если вы написали 1000 строк, думать о том как сделать код легко сопровождаемым рано. Я вот написал наверно не менее пол миллиона и до сих пор не могу понять как ООП и SOLID может мне помочь справиться со сложностью по сравнению с процедурным слилем и god-объектами :). Или могу. Пока не уверен.

    По мне, здесь нужно разделять цели.

    Первая цель - научиться писать работающий код больше чем с двумя условиями и не тонуть в нем.

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

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

    Самая сложная цель - третья.

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

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

    При этом есть два направления - от структурного к чисто функциональному стилю и к ООП стилю.

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

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

    Здесь можно создать объект. При этом по мне, важно помнить, что мы создаем объект как "элемент поведения", а не как данные о неком доменном объекте оснащенные поведением. Второй путь хорош для UI фреймворков (кнопка-объект), может быть для игр, но создает очень много сложностей в других случаях (ИМХО).

    Такой объект может создаваться операцией new, отрабатывать и тут же удаляться. Это нормально. В этом случае он мало отличается от функции.

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

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

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

    Но что если мы пишем код все вместе большими командами?

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

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

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

    Дальше можно уже подумать про SOLID.

    Допустим, мы запихнули в один объект и некую абстрактную логику и обращение к конкретному девайсу. Т.е. нарушили "S". Как теперь ее тестировать? Стенд для испытания этого объекта станет очень сложным и специфичным. А еще непонятным. Делим это на две части и все становится проще. Здесь необходимость тестировать помогает нам осознать важность S.

    Допустим, при малейшем изменении задачи нам придется лезть в кишки объекта и все там менять, вместо того чтобы добавить новое поведение не меняя объекта (буква "O")? Это значит в нашем решении было недостаточно "фреймворковости", оно было слишком частным, или это может означать что мы распилили на объекты искусственно, не в том месте как надо (что по сути близко к недостатку "фреймворковости" по сути).

    Мы ставим объект на стенд для тестирования и прямо не знаем с чего начать. У него столько методов! Здесь мы нарушили букву "I". На стенде тестируемый интерфейс должен выглядеть предельно просто и понятно - зачем он и как проверить что он это делает. Клиенты у интерфейса могут быть сегодня одни а завтра другие, но если он понятно тестируется, он уже соблюдает I в достаточной мере.

    Есть замечательный принцип, использованный природой - каждый ген (а ген это элемент поведения, т.е. объект) вездесущ и активируется по месту в той части программы где он нужен. Значит для изменения программы достаточно добавить до кучи новый ген или подменить существующий. Этот замечательный принцип нужно не упустить и в разработке где возможно. Это "D".

    Это был мой путь. Кто прошел иной путь, делитесь.


    1. dabrahabra
      09.01.2023 19:02

      Может у меня такое впечатление сложилось, но с N-tier 80% кода это перекладываение данных и там вообще особо не надо думать. Зато остальные 20% это именно то, где нужны правильные подходы. Когда вы готовы браться за эти 20% и после этого команда не хочет утопить вас - вы двигаетесь в правильном направлении.


    1. DollyPapper
      09.01.2023 20:17

      Это пожалуй лучшее объяснение SOLID, что я когда либо слышал, не приведя даже не строчки кода. У вас прям талант объяснять.


      1. david-shiko Автор
        09.01.2023 20:36

        Мерси, очень блгодаен. Первая статья, я в шоке.


        1. amazed
          09.01.2023 20:39
          +4

          не не, у вас есть код, это у меня нет кода. давайте ка разберемся ка про кого он...


          1. david-shiko Автор
            09.01.2023 21:25
            +1

            Конфуз. А я готов и удалить свой код, раз уж такие ставки ! :)
            Мне ваше объяснение нравится, но оно как бы "сверху-вниз". Понятно тем, кто через это прошел, и непонятно тем, кому предстоит. Я пытался ответить на вопрос "как сделать здесь и сейчас".


            1. amazed
              09.01.2023 21:44

              Ага. Любое объяснение работает для того, кто сам уже почти прошел, но еще нуждается в подтверждении своих догадок.


    1. speshuric
      10.01.2023 04:25
      +1

      +1
      Я написал не 1000 строк кода, а, наверное, уже 1000 раз по 1000 и мне сложно представить, как человек, написавший 1000 строк кода сможет хотя бы осознать большую часть рекомендаций. А когда сможет осознать, то большая часть приведённых советов начинает выглядеть спорными или узкоприменимыми.


    1. Refridgerator
      10.01.2023 08:57

      Ваш комментарий достоин отдельной статьи.

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

      1) SOLID, ООП и прочие аббревиатуры — это безусловно важно, но: есть риск натягивания совы на глобус ради красивого слова, потому всегда к ним отношусь с предельной осторожностью, а чаще просто прохожу мимо. Чем хуже например Finite Universally Code Kraft? Миллион такого можно напридумать если задаться целью.

      2) Чтобы миллион раз переписывать один и тот же код, он должен быть — кратким и понятным спустя много лет. Не должно возникать вопросов, зачем тут этот класс или метод и почему без него тут нельзя было бы обойтись. Так же в коде полезна классовая несправедливость — когда сразу понятно, кто тут высокуровневый аристократ, пишущий var t = database.Execute("sp_get_report_data") и не желающий даже знать тип возвращаемого значения, а кто простой работяга, открывающий и закрывающий двери коннекты, о которых хозяин уже давно забыл. Поэтому, кстати, мне и не нравится подход C# с async/await.

      3) Goto зло, недетерминированные циклы зло, проверки на корректность введёных данных — зло в злотой степени. Если в коде написано func(double a) {if(a<0) throw... это плохой код. Хороший код это func(Distance a)... где объект с отрицательным значением просто не может существовать.

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

      4) new очевидно тоже зло, Stack Overflow очевидно тоже. Если кто угодно может забирать сколько угодно памяти — это даже не коммунизм, это анархия в чистом виде, а с анархией порядка не получится. Если холопу хочется денег (памяти), чтобы покушать — холоп получает не деньги, а покушать. Заодно и проблема с коррупцией и нецелевым расходом ценных ресурсов решается. Ну а за аристократами полицейские наблюдают, кто что делает тщательно записывают, а иногда даже и убивают без суда и следствия.


  1. Persik1
    09.01.2023 21:18

    Статья достаточно высокого уровня


  1. Andrey_Solomatin
    09.01.2023 23:43

    Статья неплоха, но стоит немного пополировать.

    Совет 1 не очень закончен. Задать вопрос хорошо, но что с этим делать дальше.

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

    C def send_email условием внутри категорически не соглачен. Стоит сделать интерфейс(абстрактный базовый класс) EmailSender и к нему две имплементации. Что-то типа условия в коде должно быть один раз, там где мы создаём инстанс одного или другого класса. Для остальной программы это работа с интерфейсом.

    Совет 9 я не понял.

    Ремарка: в архитектуре существуют метрики сложности кода, чрезмерное ветвление ухудшает показатели.

    Сложность она ортоганальна архитектуре. Советы 3 и 7 как раз описывают способы борьбы с ней.


    1. david-shiko Автор
      10.01.2023 13:58
      +1

      Про `def send_email` согласен, спорное решение, но я считаю это допустимым компромиссом между чистотой и простотой.
      Признаться, я просто примера лучше не придумал :)

      9 - можно долго раскывать, если перефразировать:
      1. if вредны, но без них никак.
      2. if должно быть поменьше в основных функциях / обработчиках.
      3. Вложенные if еще больше ухудшают чтение.
      4. Много if в одном месте - плохой признак.


      1. Andrey_Solomatin
        10.01.2023 21:25

        if  и их разновидности (switch, for, while) не вредны, вредна сложность.

        Очень рекомендую именно со стороны цикломатической сложности заходить к этому вопросу. Что такое много? У каждного это своё. А вот сложность эта цифра и для всех она одинакова.

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

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

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

        def __init__(self, email_sender):
          self._email_sender = email_sender
        
        def send_email(self, *args):
           self._email_sender.send(*args)

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

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

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


  1. astypalaia
    11.01.2023 14:23

    К сожалению главная проблема алкоголика не в том, что он не может бросить пить, а в том, что он не хочет бросать и вообще не видит в этом никакого смысла. Главная проблема г-кодера не в том, что он не знает как улучшить свой код, а в том, что он считает свой код совершенным. Если код вдруг где-то перестает работать - это проблема криворуких юзеров с их кривым железом. Или проблема тестировщиков - опять же с их кривыми руками и стендами. Если кто-то не может ничего понять в их "совершенном" коде - значит у кого-то просто недостаточно мозгов. В тот момент, когда разработчик задумывается над вопросом как бы улучшить код - он уже наполовину исцелен. Благо на тему совершенного кода написанны тонны литературы - и, кстати, очень неплохой литературы. Вот даже здесь на Хабре была не так давно статейка на тему. Суть ее можно выразить перефразируя Роберта Мартина: Код должен быть простым. Вы уже сделали свой код простым? Отлично! - теперь сделайте его еще проще!