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

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

Хорошая статья

Дополненю, что в python 3.12 появился декоратор @override, позволяющий указать, какие методы мы наследуем

deprecated в данном контексте - "устаревший", а не "осуждаемый".

Разные источники предлагают разные переводы. В таком варианте будет даже лучше.

Спасибо.

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

Я б тоже лучше слился, чем сидеть примеры рожать по 200 строчек, как в статье.

Когда меня спрашивали про SOLID обычно обходилось без кода.

абстрактный код (код который нельзя скомпилировать и/или выполнить) делает любые принципы еще более абстрактными, а значит сложными для понимания, ИМХО.

Ты поднял очень важную для меня тему. Когда я отдавал статью на рецензию то рецензенты сеньоры писали разные замечание, а рецензенты уровня джун+ все написали, что примеры лучше сделать более конкретные (чтоб их можно было закинуть в IDE и выполнить). Я думал над тем, как это сделать. Отказался от примеров на кошечках и рыбках. Классы аутентификации из примеров — это реальные классы, которые крутятся на проде. В методах этих классов обычно простой реквест в сервис аутентификации, обработка статус кода и сохранение их в экземпляре класса. С кодом запроса в api все хорошо справляются. Код с запросом скорее всего будет не интересен. Но при этом увеличит и так большие примеры.

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

Вот например, вы пишите:

Давайте разбираться по порядку. ABC — это класс-помощник, который указывает метакласс metaclass=ABCMeta в качестве параметров класса. 

Зачем тут нужен помошник? есть 150 способов указать метакласс вкачестве параметров класса, зачем нужен именно помошник?

Я начинаю это читать и сразу чуствую что я не в теме, и дальше больше но уже даже не возьмусь формулировать, возможно это мне чего то не хватает, я не претендую на авторитетность мнения!

Тут дальше в комментариях написано:

Куча слов - и ни одного упоминания назначения принципов.

я думаю это замечание более важное, чем мои изначальные ощущения.

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

Привет. Давай по порядку.

Зачем тут нужен помощник? есть 150 способов указать метакласс в качестве параметров класса, зачем нужен именно помощник?

Я описал абстрактный класс так как рекомендует документация. В исходный код питона его добавил Андрей Светлов в коммите от 14.12.2012. По моему класс помощник доступен с версии 3.5. До этого абстрактный класс строился при помощи конструкции metaclass=ABCMeta.

Мы в команде используем именно абстрактные классы и именно способ рекомендуемый в документации.

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

Исходный код python, абстрактные классы. Класс помошник. (https://github.com/python/cpython/blob/9cdf05bc28c5cd8b000b9541a907028819b3d63e/Lib/abc.py#L184)

Куча слов - и ни одного упоминания назначения принципов.

Я видел этот комментарий я не знаю что на него ответить. Надо задать вопрос более конкретно.

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

Ты прав, без SOLID у меня было все хорошо. Многие проекты на первых этапах своего развития отлично обходятся без него.

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

Я построил объяснение принципа отталкиваясь именно от такого примера.

Понимание SOLID пришло ко мне через боль когда нужно было вносить изменения в код

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

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

Вот у меня есть конкретный вопрос: как быть со вспомогательными (дата)классами-зависимостями?

Скажем, класс BaseRunner хранит настройки в BaseConfig:

@dataclass
class BaseConfig:
  name: str

class BaseRunner(ABC):
  def __init__(self, conf: BaseConfig):
    self.conf = conf
    
  @abstractmethod
  def run(self):
    """do work here"""

Добавляем более конкретный интерфейс PeriodicRunner. У него помимо общих с предком есть свои настройки, соответственно, у него свой конфиг:

class PeriodicConfig(BaseConfig):
    name: str
    period: int

class PeriodicRunner(BaseRunner, ABC):
    def __init__(self, conf: PeriodicConfig):
        super().__init__(conf)

    def run(self):
        while True:
            self.run_once()
            sleep(self.conf.period)

    @abstractmethod
    def run_once(self):
        """do work once here"""

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

Возможно ли вынести состояние/конфигурацию в отдельный датакласс не нарушая принципа подстановки Лисков?

Не нарушая LSP - запросто. Собственно, ваш код и не нарушает.

Вот с тайп-чекером тут сложнее...

Если взять код, работающий с классом, реализующим интерфейс BaseRunner, и подсунуть ему реализацию PeriodicRunner:

config = BaseConfig(name='test runner')
runner = PeriodicRunner(config)
runner.run()

то все сломается, потому что у них разная сигнатура:

    sleep(self.conf.period)
AttributeError: 'BaseConfig' object has no attribute 'period'

Это определенно нарушает LSP.

"Подсунуть реализацию" означает "подсунуть готовый PeriodicRunner". Создание PeriodicRunner внутри функции не входит в понятие "подсовывания".

Иными словами, LSP требует чтобы вот такой код работал корректно:

def run_me(runner: BaseRunner):
  runner.run() # или использование метода run любым другом образом

run_me(PeriodicRuner(...))

Но этот код работает корректно, следовательно LSP не нарушен

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

Считать ли это зависимостью на абстракциях?

def print_items(sequence):
    for i in sequence:
        print(i)


Утиная типизация подразумевает неявные интерфейсы. В данном случае sequence это iterable https://docs.python.org/3/glossary.html#term-iterable.

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

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

Нет, если наследник тоже абстрактный класс.

Если вы в своем коде уже используете абстрактные классы — скорее всего, в коде с SOLID все хорошо

Я разный код видел.

Спасибо за коментарий.

Считать ли это зависимостью на абстракциях?

Я думаю что нет. Здесь, как в ссылке написано, объект sequence должен реализовывать методы iter и next. Тогда он будет соответствовать интерфейсу и его можно здесь применить.

Нет, если наследник тоже абстрактный класс.

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

Я разный код видел.

Очень смешно. Особенно когда свой смотришь годовалый. :)

осуждаемым


Никогда не слышал такого перевода deprecated.

Это директива ООН

Специально полазил по словарям. Гугл предлагает устарел, яндекс переводит как осуждаемый. Родной с детства Англо-русский словарь Мюллера переводит как 1) обесценивать(ся), падать в цене 2) унижать, умалять, недооценивать. А лингво в вариантах хужожественного перевода еще больше вариантов предлагает. Сам в шоке.

не тем словарём пользовались, погуглите историю про гуртовщика мыши
нужно было переводить не непосредственно, а через один шаг с толковым словарём "Deprecated refers to a software or programming language feature that is tolerated or supported but not recommended"
есть два термина obsoleted и deprecated, они оба значат одно и тоже "устаревший", но несколько разной окраски
deprecated это указание авторов библиотеки не использовать больше конкретный API, а то хуже будет, но это просто они снимают с себя ответственность, хуже может доолго не быть
obsoleted - нейтральный, встречался раньше, теперь норма использовать deprecated но перевод на русский один и тот же "устаревший"

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

Ведь из этого очевидно следует все остальное. Ну или почти все.

Вот и тут. Куча слов - и ни одного упоминания назначения принципов.

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

Сказать про модули верхнего и нижнего уровня и не придумать примера с абстракциями разного уровня (и их зависимостями)

Какая зависимость инвертируется в итоге? Между чем и чем? Кто верхнего уровня, кто нижнего, какую проблему решили? Да бог его знает

Аналогично, создание замены deprecated классу не относится к принципу open/closed. Это просто новый класс. Принцип open/closed, действительно, связан с принципом подстановки Барбары Лисков, но ещë и с принципом единой ответственности. В разрезе этой троицы мы можем сформулировать принцип открытости/закрытости интерфейсов так: "ваши родительские или абстрактные классы должны решать одну задачу. Эта задача определяется private и protected методами и свойствами класса. Если вы можете выделить в вашем абстрактном базовом классе функционал, который вы можете защитить от изменений в потомках, то он, скорее-всего, отвечает принципам SOL".

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

Причëм, этот принцип действует в обе стороны. Например, у вас есть функция react_to_noise, которая принимает на вход наследников класса Animal: Cat, Dog, Parrot. Допустим, что функция использует метод fly() только если мы имеем дело с Parrot. Это нарушает принципы LID. Здесь мы должны сделать так, чтобы всегда выполнялось react_to_noise(animal: Animal), и внутри не было вызовов методов, специфичных для наследников. Поэтому, мы можем применить принципы ID и создать интерфейс Bird(Animal) в котором будет реализован или объявлен метод fly(). Тогда, для функций react_to_noise_animal(animal: Animal) и react_to_noise_bird(bird: Bird) будет выполняться принцип L. В python для понимания такой подстановки прекрасно подходит механизм типизации Generic.

Привет. Спасибо за комментарий. Ну смотри, по тексту. Порядок подачи информации у меня не такой как обычно пишут про SOLID. Написано так, потому что я отталкиваюсь не от принципов, а от кода. В идеальном мире я начинаю писать код с абстрактных классов. Значит и повествование я начинаю с одного абстрактного класса и одного зависимого продового. В этот момент кода недостаточно для всех примеров про инверсию. Поэтому в том месте, где ты это ожидаешь все про инверсию этого нет.
Чуть ниже, когда принципе открытости и закрытости добавили дополнительный класс. А в принципе подстановки Лисков я разбирал разные виды зависимостей и некоторые особенности реализации в Python.
В принципе единой ответственности есть примеры с множественными зависимостями абстрактных классов для тонких и толстых интерфейсов.

Перечитал статью и не нашёл там объяснения про инверсию зависимостей (хотя есть его вполне валидная формулировка). Само по себе выделение интерфейса (например, абстрактного класса) это не инверсия. Инверсия возникает в тот момент, когда вы определяете, ГДЕ у вас определён интерфейс.

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

Однако если вы определите AbstractAuthUser В МЕСТЕ ЕГО ИСПОЛЬЗОВАНИЯ, в модуле высокого уровня, то зависимости инвертируются: модули более низкого уровня будут вынуждены узнать о модуле высокого уровня (чтобы узнать про интерфейс, который они реализуют), но модуль высокого уровня будет независимым и самодостаточным.

Ура, ваши зависимости инвертированы! Вы можете разрабатывать модуль высокого уровня, вообще не зная, какие бывают конкретные реализации (и даже бывают ли они вообще). Зависимость была туда - стала сюда. Она инвертирована! И тут уже становятся важны опен-клоз, лисков и всё такое.

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

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

Здесь все более прозрачно. У класса AuthUserKeycloak может быть только одна причина для внесения изменений — изменения в аутентификацию или в работу с Keycloak. Если вы вносите изменения в класс для добавления новых пермишенов или для изменения способа сохранения, это уже дополнительные причины

А как определили, что в данном случае это причины разные, а не одна? Что если требования к пермишенам, способам хранения и алгоритму аутенитификации приходят от одной роли?

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

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

Надеюсь я ответил на твой вопрос.

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

Декоратор abstractmethod гарантирует, что у дочернего класса будут все методы, которые декорированы этим декоратором. Им нужно оборачивать все методы, которые будет использовать бизнес-логика. Разработчики, которые будут обращаться к классу аутентификации, могут быть уверены, что у него всегда есть методы is_authenticated, get_email, get_department, потому что они декорированы abstractmethod, а значит, обязательны для реализации в дочерних классах, унаследованных от абстрактного.

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

Вносить изменения в класс AuthUserAD — плохая идея ... Если мы не исправим все места, где он вызывается по старому имени, то гарантированно уроним код.

решается статической типизацией, где опечатки не приводят к рантайм ошибке

  • Принцип открытости/закрытости Мейера

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

Определение Мейера поддерживает идею наследования реализации. Реализация может быть переопределена через наследование,

и

Это определение поддерживает идею наследования от абстрактных базовых классов

Такое ощущение что попал на физику в 5 классе, когда говорят упрощённо и не всю правду о реальности. Ничего о наследовании и абстрактных или базовых или классах при описании паттернов просто неизвестно. Это деталь реализации, никак не относящаяся в общему принципу

Если мы где-то не заменили класс AuthUserAD на AuthUserKeycloak, у нас все продолжит работать, поскольку мы не удалили класс AuthUserAD. Он все еще присутствует в коде и работает лучше, чем прежде, поскольку нагрузка на AD уменьшится. При каждом создании инстанса AuthUserAD мы будем видеть ошибку в логах и постепенно безболезненно выпилим его из возможных мест.

опять проблемы питона, которые не являются реально проблемами программирования в целом

Принцип подстановки Лисков

в этом пункте в статье я вообще не увидел самого принципа подстановки Лисков, что это такое и почему. "Вот есть интерфейсы, они могут быть разные, всё такое", казалось бы, причём здесь это

Привет. Спасибо за коментарий.

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

Если бы ты написал это лет 10 назад я бы, скорее всего, с тобой согласился. Но сейчас Python-у ничего не нужно доказывать. Как будет через 10 лет я не знаю.

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

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

опять проблемы питона, которые не являются реально проблемами программирования в целом

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

Здесь скорее задеты проблемы микросервисоной архитектуры. Мы не можем закрыть доступ из сервисов в AD на уровне инфраструктуры пока у меня живут сервисы которые туда ходят. Мы деплоим их постепенно. Такие проблемы не завязаны на языке.

в этом пункте в статье я вообще не увидел самого принципа подстановки Лисков, что это такое и почему. "Вот есть интерфейсы, они могут быть разные, всё такое", казалось бы, причём здесь это

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

Принцип подстановки Лисков — это принцип организации подтипов в объектно-ориентированном программировании, предложенный Барбарой Лисков в 1987 году: если q(x) является свойством, верным относительно объектов x некоторого типа T, тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.

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

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

Из этого самым важным я считаю «объекты типа T в программе могут быть замещены объектами типа S без каких-либо изменений». Это главное. Это возможно при соблюдении интерфейсов и сохранении сигнатур методов. А что от чего наследуется и каким образом мы проверяем что интерфейсы совпали это вторично. Для меня проблема, когда разработчики наследуются и в новом классе незначительно меняют интерфейс или сигнатуры. И даже если сейчас это не аффектит но через год другой может выстрелить в ногу. В таком случае проверка типов ничем не поможет. Более того в Python в типизации можно указывать не родительский тип, а список из дочерних. Более того, для соблюдения этого условия, мне даже не нужно наследование. Если класс T и класс S реализуют один интерфейс с моей точки зрения они уже соответствуют принципу подстановки Лисков потому что мы можем объект T заменить на объект S и наоборот. А в коде для проверки типов линтером я укажу список типов либо родительский тип если он есть. В Python какой способ проверки типов из них выбрать это преднамеренный выбор программиста.

Надеюсь, что после прочтения статьи принципы SOLID стали для вас такими же очевидными, как для Роберта С. Мартина.

Нет. Вообще нет. Дядюшка Боб молодец: напихал в Джаву абстракций в 2000-х годах и свалил писать на Clojure. А люди всё продолжают слепо перетаскивать из Джавы на все остальные языки.

cd venv/lib/python3.9/site-packages/django/
grep -lir "from abs import"  

И ничего не найдено, хотя там есть Абстрактные классы. Вот этот from abs - он не выглядит python way. Слишком много Абстракций начинают усложнять понимание, что в свою очередь начинает противоречить KISS. Плюс в питоне можно вот так сделать class MyModelAdmin(AdminImageMixin, admin.ModelAdmin), а в java множественное наследование запрещено. Вот тут даже интерфейса не надо

""" Демонстрируем Инъекцию Зависимостей """

class EmailNotifier:
    """ Отправим через E-mail """
    def send(self, to, message):
        print(f"Notify {to} by mail. {message}")

class SmsNotifier:
    """ Отправим в Telegramm """
    def send(self, to, message):
        print(f"Notify {to} by sms. {message}")


def notify_user_by_email(to, message):
    """ Тут функция зависит от настроек почтового сервера """
    by_email = EmailNotifier()
    by_email.send(to, message)

def notify_user_di(to, message, notifier):
    """ Инъекция notifier. Главное, чтобы у notifier был метод send() А там хоть email, хоть MailChimp """
    notifier.send(to, message)


if __name__ == "__main__":

    # Вот такие красавцы
    user_list = [
        {"name": "Bob", "phone": "+7-903", "email": "uncle.bob@gmail.com"},
        {"name": "Mike", "phone": "", "email": "mechanic@yahoo.com"},
        {"name": "Jeff", "phone": "+7-926", "email":""},
    ]

    for user in user_list:
        if user["phone"]:
            """ Отправляем в телефон """
            notify_user_di(user["phone"], "Welcome home!", SmsNotifier())

        if user["email"]:
            """ На email """
            notify_user_di(user["email"], "Welcome home!", EmailNotifier())

А обязательно вот так сложно говорить на собеседовании, например, "инверсия зависимостей"?

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

То есть рекомендаций/лайфхаков, как делать код гибким/понятным такое огромное множество, что любое перечисление принципов SOLID, KISS и прочих - это маленькая капля в море личного опыта.

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

Было бы неплохо, если бы все программисты на личном опыте понимали, зачем придуман SOLID. Но есть ли способ при приеме на работу не парить мозг зубрежкой терминов из конкретных книг/статей?

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

Было бы неплохо, если бы все программисты на личном опыте понимали, зачем придуман SOLID.

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

рекомендаций/лайфхаков, как делать код гибким/понятным

Уже становится ясно, что эти рекомендации а) субъективны, потому что что одному понятно, то другому мрак и ужас б) не обязательны, потому что текущей команде и так может быть хорошо, и проблем с сопровождением и развитием у них не возникает. Не говоря уже о том, что это может быть MVP/POC, и развивать его никто не планирует вообще.

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

Если один кусок кода лезет в другой, это просто невозможно тестировать

Не, ну тут обычно речь о другом. Я вот видел код, где некий коннект к базе создавался внутри определенного класса. Это зависимость. И в таком виде ее правда невозможно тестировать, потому что нельзя подсунуть классу другой коннект, он работает только с пром базой. А вот если создание зависимости вынести наружу, и сделать inject этого коннекта к базе в класс - тестирование становится возможным и достаточно удобным. Это правда про dependency inject, но суть надеюсь ясна.

Шел 2023 год, а в статьях до сих пор используют неверное определение и объяснение принципа Single Responsibility, которое к тому же висит в русской википедии (в англоязычной пофиксили). И это при том, что Мартин еще в 2014 выпустил статью, в которой попытался разъяснить принцип. А затем сделал это еще раз, в книге Clean Architecture 2017-го:

Of all the SOLID principles, the Single Responsibility Principle (SRP) might be the
least well understood. That’s likely because it has a particularly inappropriate name.
It is too easy for programmers to hear the name and then assume that it means that
every module should do just one thing.


Make no mistake, there is a principle like that. A function should do one, and only
one, thing. We use that principle when we are refactoring large functions into
smaller functions; we use it at the lowest levels. But it is not one of the SOLID
principles — it is not the SRP.


Historically, the SRP has been described this way:
A module should have one, and only one, reason to change.


Software systems are changed to satisfy users and stakeholders; those users and
stakeholders are the “reason to change” that the principle is talking about. Indeed,
we can rephrase the principle to say this:
A module should be responsible to one, and only one, user or stakeholder.


Unfortunately, the words “user” and “stakeholder” aren’t really the right words to
use here. There will likely be more than one user or stakeholder who wants the
system changed in the same way. Instead, we’re really referring to a group—one or
more people who require that change. We’ll refer to that group as an actor.
Thus the final version of the SRP is:
A module should be responsible to one, and only one, actor.

Now, what do we mean by the word “module”? The simplest definition is just a
source file. Most of the time that definition works fine. Some languages and
development environments, though, don’t use source files to contain their code. In
those cases a module is just a cohesive set of functions and data structures.

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

Шел 2023 год, а в статьях до сих пор используют неверное определение и объяснение принципа Single Responsibility

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

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

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

Второе определение, которое дано Робертом Мартином в 2014 тоже очень хорошее. Но оно не столько про код сколько про архитектуру.

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

Это определение скорее для сеньоров и архитекторов. Термин actor это тоже скорее про Event Storming. На этапе Event Storming участвуют разработчики, которые очень хорошо знают SOLID и умеют его применять. Вряд ли они зайдут почитать эту статью.

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

Первая версия появилась когда:

  1. Не так давно был совершен переход от двухуровневой архитектуры к трехуровневой (многоуровневой).

  2. Не большие монолиты способны решать любые задачи.

  3. В по-настоящему больших проектах внедряют распределенную архитектуру и сервис-ориентированную архитектуру (SOA) в частности.

А когда появилась вторая версия:

  1. Абсолютно все проекты значительно выросли.

  2. Распределенная архитектура стала микросервисной и асинхронной.

  3. На многих проектах микросервисы вытеснили монолиты.

  4. Во многих проектах проектирование от DDD. В по-настоящему больших проектах проектирование от DDD стандарт.

Как следствие сильно поменялся подход к проектированию ПО в целом. И Роберт Мартин отразил это в новой трактовке старого принципа. Но это не значит, что старое определение не верное. Они скорее дополняют друг друга.

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

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

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

>Если ответ содержит в себе несколько пунктов из списка функционала, который может изменится то класс не соответствует принципу

А как узнать содержит или нет?

>Как следствие сильно поменялся подход к проектированию ПО в целом. И Роберт Мартин отразил это в новой трактовке старого принципа

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

>Оно не подходит под те примеры, которые я привожу

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

Привет. Спасибо за комментарий, глубоко копнул.

Смысл этого принципа же не в том чтобы просто сказать "код надо декомпозировать". Это и так все понимают, что надо. Вопрос в том - когда?

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

А как узнать содержит или нет?

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

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

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

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

Я уверен что примеры отражают идею, которая стоит за изначально сформулированнвм принципом. А для переформулированого определения нужны модули которые будут работать с инстансом AuthUserKeycloak где определен actor и его пермишены. В моем случае модуль это инстанс миктосервиса который реализует доменную логику. Получается цепочка actor инициирует команду которая запускает доменную логику в микросервисе. Это сильно круто для данной статьи и для целевой аудитории на которую данная статья расчитана.

И Kergan88, надеюсь что наше обсуждение поможет какому нибудь начинаюшиму програмистаму получить оффер. :)

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

Привет.

Принцип не изменился со временем. Мартин просто переформулировал определение.

Формулирование нового определения не делает старый неверным.

У меня есть классы к которым я не могу применить обновленный принцип. Например классы нотификации через email и через например sms. В них нет actor. Обе нотификации может инициироватьодин actor но я же не буду из-за этого валить все в один класс.

Или классы аутентификации и авторизации когда у нас уже как бы есть actor но мы еще не знаем это Guest, Registered user, Administrator или кто то еще. А пока мы этого не знаем то мы не можем его отнести к какому-то модулю.

Как применить A module should be responsible to one, and only one, actor. если actor нет или он не определен.

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

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

  • Что старое определение неверно?

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

  • Что надо использовать только переформулированное определение?

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

  • Что нужно поменять формулировку определения принципа SRP в статье?

Мой опыт показывает что начинающеммому разработчику лучше ставить задачи по добавлению нового функционала внутри модуля. А для таких задачах переформулированное определение не подходит. В правильной архитектуре в модуль уже приходит конкретный actor. Но внутри модуля старое определение отлично подходит. И код получается солидный. Когда разработчик дорастет до того уровня что будет сам проектировать модуль, нарезать таски и раздавать их мидлам и джунам тогда он воспользуется определением A module should be responsible to one, and only one, actor. Разработчику такого уровня уже будет не нужна эта статья. :)

Формулирование нового определения не делает старый неверным.

Дело не в формулировке, а в сути конкретного принципа, которую она пытается донести.

Но не совсем понимаю какую мысль ты хочешь до меня донести.

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

Вообще, когда разбирался с SOLID на примере Python понял, что сам модуль typing из стандартной библиотеки прекрасно демонстрирует как минимум первые 4 принципа (хотя и пятый можно натянуть)

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