From 0369a79444c67e2c7bfad8d860f0dc441c4c2d97 Mon Sep 17 00:00:00 2001 From: Lev Vereshchagin Date: Mon, 8 Sep 2025 13:13:35 +0300 Subject: [PATCH 1/2] Add comments for SOLID guide --- solid.md | 221 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 121 insertions(+), 100 deletions(-) diff --git a/solid.md b/solid.md index 5aa8538..031ebf9 100644 --- a/solid.md +++ b/solid.md @@ -28,8 +28,8 @@ SOLID — это набор принципов от Роберта Мартин ### Почему SOLID? Принципы SOLID на нашей практике показывают себя хорошо (т.е. мы для себя это обосновали эмпирически — мы их применяли и, на наш взгляд, они показали пользу): -* код проще изучать -* код проще тестировать +* код проще изучать +* код проще тестировать * легче онбордиться в команде Из некоторых минусов можно отметить то, что код становится чуть более многословным и его становится чуть больше. @@ -38,32 +38,34 @@ SOLID — это набор принципов от Роберта Мартин Большинство гайдов в интернете страдает от нескольких вещей: -* сухое изложение принципов без объяснения сути -* копирование одной и той же информации из раза в раз без добавления новой информации -* игнорирование оригинальных текстов, неточное их цитирование +* сухое изложение принципов без объяснения сути +* копирование одной и той же информации из раза в раз без добавления новой информации +* игнорирование оригинальных текстов, неточное их цитирование * при прочтении гайда нет понимания как практически это применять -Поэтому, принципы SOLID так сложно применять в жизни и почти все на вопрос о принципах SOLID отвечают что-то размытое. +Поэтому, принципы SOLID так сложно применять в жизни и почти все на вопрос о принципах SOLID отвечают что-то размытое. Мы пишем свой гайд для того, чтобы у читающих была возможность его действительно практически применять. ### Классические проблемы SOLID или ситуация «да, но» -Почти все разговоры на собеседованиях или в целом о SOLID часто сводятся к следующему утверждению: «SOLID — это хорошо/отлично/здорово/надо применять/нужно всем/база». +Почти все разговоры на собеседованиях или в целом о SOLID часто сводятся к следующему утверждению: «SOLID — это хорошо/отлично/здорово/надо применять/нужно всем/база». Следующий вопрос, который мы обычно задаем: «а как вы внедряли SOLID». И вот тут рождаются фразы, которые каждый наш интервьюер слышал много раз: -* «я SOLID чувствую интуитивно» -* «SOLID сам собой получается от здравого смысла» +* «я SOLID чувствую интуитивно» +* «SOLID сам собой получается от здравого смысла» * «SOLID это очевидно» -По факту, сами принципы SOLID изложены в книге Agile software patterns, principles and development 2003 года и занимают около 30 страниц текста и сложны в такой степени, что без осмысления и рефлексии их применять невозможно, тем более «интуитивно» (вы не родились с ощущением SOLID). Откуда появляется эта «интуитивность» мы попробуем рассказать далее. -Кстати говоря, оригинальная публикация почти всеми гайдами игнорируется и зачастую даже ссылок на неё получить в этих гайдах невозможно, поэтому почти всегда вопрос «а откуда вы знаете о SOLID» к ней не приводит. +По факту, сами принципы SOLID изложены в книге Agile software patterns, principles and development 2003 года и занимают около 30 страниц текста и сложны в такой степени, что без осмысления и рефлексии их применять невозможно, тем более «интуитивно» (вы не родились с ощущением SOLID). Откуда появляется эта «интуитивность» мы попробуем рассказать далее. +Кстати говоря, оригинальная публикация почти всеми гайдами игнорируется и зачастую даже ссылок на неё получить в этих гайдах невозможно, поэтому почти всегда вопрос «а откуда вы знаете о SOLID» к ней не приводит. В нашем же гайде мы полагаемся на первоисточник и стараемся это дополнять нашим же опытом. ### Важные примечания -* применять SOLID стоит осознанно, никакого «самостоятельного», «интуитивного» пути нет -* необходимо использовать mypy (или red_knot, когда его сделают) и покрывать весь код 100% аннотациями типов (кроме тех случаев, когда mypy сам выводит типы) +* применять SOLID стоит осознанно, никакого «самостоятельного», «интуитивного» пути нет + +* необходимо использовать mypy (или red_knot, когда его сделают) и покрывать весь код 100% аннотациями типов (кроме тех случаев, когда mypy сам выводит типы) * интерфейс — это либо набор публичных методов (без подчеркиваний), либо abc, либо typing.Protocol + * мы трактуем, что SOLID — это про ООП, и применяем его для ООП (но не отрицаем, что какие-то из принципов применимы и за рамками ООП) # Принципы @@ -71,43 +73,47 @@ SOLID — это набор принципов от Роберта Мартин ## Принцип единой ответственности (Single Responsibility Principle, SRP) + + + > A class should have only one reason to change Если переводить с английского, то приблизительный смысл может звучать так: «класс имеет одну причину для изменений». Что такое причина? Слово «причина» (reason) вызвало непонимание длинной в множество лет, поэтому в 2014 году Мартин [написал статью, которая разъясняет это слово](https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html). Проблема в том, что даже эта статья даёт нам лишь приблизительные ориентиры. А мы с вами пишем конкретный код. Собирая всё вместе, сформулируем идеи, к которым мы пришли в процессе применения принципа: -* в корне SRP принципа лежит идея разделения ответственности. Стоит помнить о ней при написании кода. Т.е. код лучше разделять на куски, каждый из которых реализует очень конкретный и маленький кусок логики +* в корне SRP принципа лежит идея разделения ответственности. Стоит помнить о ней при написании кода. Т.е. код лучше разделять на куски, каждый из которых реализует очень конкретный и маленький кусок логики + * слово «причина» которая лежит в основе SRP — это ваша командная договоренность. Вы внутри команды должны договориться о том, что является причиной для изменений. Аргументом почему это нужно делать является код, иллюстрирующий работу с модемом \[вставить сюда код с модемом\]. У нас есть 4 метода и программисты бьются на несколько групп. Кто-то считает, что ответственность едина — мы, ведь, работаем с модемом. Работа с модемом — это ответственность, и причина изменений. Кто-то считает, что работа с каналом связи — это одна причина для изменений, а работа с передачей данных — другая. Если мы с вами будем пытаться решить кто из них прав, то сойдем с ума. А нам платят не за философию, поэтому мы предлагаем вам выбрать (и выбирать в будущем) ваши причины и ответственности внутри команды самостоятельно (кстати, на мой вкус, один класс модема — это вполне SRP). -Возникает вопрос — «а как нам договориться внутри команды о трактовке слова причина?». Подход довольно прост — мы можем это уточнять в процессе ревью. А изначально можно выбирать исходя из вашего чувства прекрасного или обратившись в сообщество за примерами. +Возникает вопрос — «а как нам договориться внутри команды о трактовке слова причина?». Подход довольно прост — мы можем это уточнять в процессе ревью. А изначально можно выбирать исходя из вашего чувства прекрасного или обратившись в сообщество за примерами. Кстати, этот принцип претендует на первое место среди двух принципов, благодаря которым программисты «интуитивно чувствуют SOLID». Из-за его обманчивой простоты появляется это ложное чувство. Как мы проиллюстрировали, это очень непростой принцип с неоднозначной трактовкой. И всего лишь из-за слова «причина». Пример кода, который нарушает SRP принцип: ```python -@dataclasses.dataclass -class UserService: - database_host: str - database_port: str - kafka_topic: str - kafka_host: str - - async def create_user(self, user_data: UserData) -> None: - database_connection = asyncpg.connect(self.database_host, self.database_port) - await database_connection.execute("INSERT INTO users ...") - - kafka_connection = aiokafka.connect(self.kafka_host) - await kafka_connection.send(user_data, self.kafka_topic) - - async def delete_user(self, user_id: str) -> None: - database_connection = asyncpg.connect(self.database_host, self.database_port) - await database_connection.execute("DELETE FROM users WHERE ...") +@dataclasses.dataclass +class UserService: + database_host: str + database_port: str + kafka_topic: str + kafka_host: str + + async def create_user(self, user_data: UserData) -> None: + database_connection = asyncpg.connect(self.database_host, self.database_port) + await database_connection.execute("INSERT INTO users ...") + + kafka_connection = aiokafka.connect(self.kafka_host) + await kafka_connection.send(user_data, self.kafka_topic) + + async def delete_user(self, user_id: str) -> None: + database_connection = asyncpg.connect(self.database_host, self.database_port) + await database_connection.execute("DELETE FROM users WHERE ...") ... - kafka_connection = aiokafka.connect(self.kafka_host) + kafka_connection = aiokafka.connect(self.kafka_host) await kafka_connection.send(user_data, self.kafka_topic) ``` -Данный класс отвечает не только за работу с пользователями, но ещё в себе содержит подключение к kafka, postgres. Получается, что он несет ответственность за подключения к этим компонентам инфраструктуры, хотя его основная задача работать с пользователями (мы делаем этот вывод по семантике кода). Минусы заключаются в том, что такой код хрупкий и его сложно тестировать, потому что придётся заводить множество моков, а в частности патчить. Это плохо, потому что вместо проверки нашего кода на нужное нам поведение (создание и удаление пользователей), мы будем вынуждены в тестах как-то патчить соединения с кафкой и постгресом, а это магические действия с неизвестными последствиями. Мы потеряли чистоту кодовой базы сразу в двух местах, получили «магические» действия и повысили хрупкость кода (любые изменения в библиотеках, механизмах патчинга — и вы получите ошибки, которые никак не связаны с тем, что делает UserService). -В данном случае может показаться, что «а чего такого? ну запатчу», и это резонный комментарий, в данном маленьком примере, конечно, ничего страшного не произойдет. Код будет похуже, более хрупкий, но все это не кажется критическим. Дело в том, что этот пример выглядит плохо на масштабе. Когда в вашей кодовой базе это становится основным подходом, то все начинает быть хрупким и неустойчивым. +Данный класс отвечает не только за работу с пользователями, но ещё в себе содержит подключение к kafka, postgres. Получается, что он несет ответственность за подключения к этим компонентам инфраструктуры, хотя его основная задача работать с пользователями (мы делаем этот вывод по семантике кода). Минусы заключаются в том, что такой код хрупкий и его сложно тестировать, потому что придётся заводить множество моков, а в частности патчить. Это плохо, потому что вместо проверки нашего кода на нужное нам поведение (создание и удаление пользователей), мы будем вынуждены в тестах как-то патчить соединения с кафкой и постгресом, а это магические действия с неизвестными последствиями. Мы потеряли чистоту кодовой базы сразу в двух местах, получили «магические» действия и повысили хрупкость кода (любые изменения в библиотеках, механизмах патчинга — и вы получите ошибки, которые никак не связаны с тем, что делает UserService). +В данном случае может показаться, что «а чего такого? ну запатчу», и это резонный комментарий, в данном маленьком примере, конечно, ничего страшного не произойдет. Код будет похуже, более хрупкий, но все это не кажется критическим. Дело в том, что этот пример выглядит плохо на масштабе. Когда в вашей кодовой базе это становится основным подходом, то все начинает быть хрупким и неустойчивым. Что можно здесь улучшить? Отказаться от инстанцирования подключений внутри и передавать их в качестве аргументов классу. Итого, мы бы написали что-то такое, чтобы соблюсти SRP: @@ -136,18 +142,22 @@ class UserService: > Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification -Второй «тот самый принцип», из-за которого формируется ложное ощущение «интуитивности» SOLID (помните, мы говорили об этом в начале статьи). Этот принцип сформулирован сложнее SRP, но его суть гораздо ближе к базовым идеям, которым учат каждого программиста. Поэтому, он тоже может «обмануть» человека, который впервые ознакамливается с SOLID. +Второй «тот самый принцип», из-за которого формируется ложное ощущение «интуитивности» SOLID (помните, мы говорили об этом в начале статьи). Этот принцип сформулирован сложнее SRP, но его суть гораздо ближе к базовым идеям, которым учат каждого программиста. Поэтому, он тоже может «обмануть» человека, который впервые ознакамливается с SOLID. Переводя принцип на русский получим что-то вроде «программные сущности (классы, модули, функции и так далее) должны быть открыты для расширения, но закрыты для модификации». Проблема формулировки здесь в том, что непонятно что такое «расширение» и что такое «модификация». И даже после чтения книги намного понятнее не становится. -Поэтому вот наша трактовка. -Принцип, по сути, описывает следующую идею: пишите код так, чтобы вы могли вносить в него новую функциональность без переписывания старого, а путем написания нового. Т.е. если вы пишете код, который вам понадобится уже в следующем пуллреквесте «рефакторить» (читай удалять), то принцип OCP вы скорее всего нарушаете -Сама идея практически понятна для всех программистов. По сути, нас с самого начала учат, что когда нам надо написать функцию, которая складывает 2 \+ 3, мы пишем sum_two_numbers = lambda a,b: a \+ b, sum_two_numbers(2, 3), а не просто «2 \+ 3» в коде. Это базовое умение программиста — обобщать задачи. По сути, OCP очень близко к этой идее, мы здесь достаточным образом обобщаем функциональность, думаем о будущем и позволяем будущим нам или нашим коллегам не переписывая нашего кода добавлять новые функции в наш продукт +Поэтому вот наша трактовка. +Принцип, по сути, описывает следующую идею: пишите код так, чтобы вы могли вносить в него новую функциональность без переписывания старого, а путем написания нового. Т.е. если вы пишете код, который вам понадобится уже в следующем пуллреквесте «рефакторить» (читай: удалять), то принцип OCP вы скорее всего нарушаете +Сама идея практически понятна для всех программистов. По сути, нас с самого начала учат, что когда нам надо написать функцию, которая складывает 2 \+ 3, мы пишем sum_two_numbers = lambda a,b: a \+ b, sum_two_numbers(2, 3), а не просто «2 \+ 3» в коде. Это базовое умение программиста — обобщать задачи. По сути, OCP очень близко к этой идее, мы здесь достаточным образом обобщаем функциональность, думаем о будущем и позволяем будущим нам или нашим коллегам не переписывая нашего кода добавлять новые функции в наш продукт Однако, все это становится куда менее понятным, когда мы практикуем этот принцип в «развесистом» ооп коде, который нам приходится писать. Понятно ли как написать класс, который будет расширяться, но не переписываться? Какие функции бизнес захочет завтра? Как сделать так, чтобы модуль, который мы пишем для сегодняшней задачи, нам не понадобилось уже завтра переименовывать и переписывать? На эти вопросы, на самом деле, нет прямых ответов. И, возвращаясь к предыдущему, нам платят не за философию, поэтому превратим это в практическое руководство к действию: -* наша задача продумать возможность расширения ооп кода таким образом, чтобы не понадобилось этот код переписывать завтра -* т.к. это сложно, вы можете отслеживать нарушение принципа — если вы видите, что вы или ваш коллега часто переписывает один и тот же код, считайте что у вас проблема с OCP -* если у вас есть проблема с OCP — ваша задача переписать этот код так, чтобы в следующий раз вам не понадобилось его переписывать вновь -* нет нужды следовать принципу фанатично. Т.е. если вы столкнулись переписыванием одного и того же кода, устранили проблему, но иногда небольшими (определите внутри что такое «небольшими», на мой взгляд 1-5 строк, может 10\) кусками продолжаете его изменять, то в этом, вероятно, нет большой проблемы + + +* наша задача продумать возможность расширения ооп кода таким образом, чтобы не понадобилось этот код переписывать завтра +* т.к. это сложно, вы можете отслеживать нарушение принципа — если вы видите, что вы или ваш коллега часто переписывает один и тот же код, считайте что у вас проблема с OCP +* если у вас есть проблема с OCP — ваша задача переписать этот код так, чтобы в следующий раз вам не понадобилось его переписывать вновь + +* нет нужды следовать принципу фанатично. Т.е. если вы столкнулись переписыванием одного и того же кода, устранили проблему, но иногда небольшими (определите внутри что такое «небольшими», на мой взгляд 1-5 строк, может 10\) кусками продолжаете его +изменять, то в этом, вероятно, нет большой проблемы * лучше не изнурять себя неукоснительным соблюдением принципа потому, что вы можете развить в себе невроз, так и не достигнув сто процентного соблюдения OCP #### Пример @@ -240,81 +250,87 @@ class CommandRunner: > Subtypes must be substitutable for their base types -Как иногда говорят на собеседованиях «в питоне нет типов», что обычно воспринимается как некорректный ответ. Однако, всегда понятно откуда в данном случае «растут корни» — типизация в питоне работает в рантайме и разработчику не очень-то заметна. Разработчику на питоне нет нужды думать о типах большую часть времени, поэтому для некоторых разработчиков этих самых типов как будто «нет». Всё ещё запутывают аннотации типов, которые часто называют типизацией. И добивает это всё наличие статически типизированных, компилируемых языков. -Поэтому, когда мы читаем оригинальную трактовку LSP принципа, то возникает вопрос — «а применимо ли это к питону?». Здесь я бы вспомнил, что SOLID — в первую очередь про ООП (в нашей трактовке). Поэтому, мы у себя слово «типы» заменяем на «классы». И получается, что принцип (не очень точный перевод на русский) «типы должны заменяться на подтипы без нарушения работы программы» мы можем перефразировать как «классы должны заменяться на подклассы без нарушения работы программы» для Python. +Как иногда говорят на собеседованиях «в питоне нет типов», что обычно воспринимается как некорректный ответ. Однако, всегда понятно откуда в данном случае «растут корни» — типизация в питоне работает в рантайме и разработчику не очень-то заметна. Разработчику на питоне нет нужды думать о типах большую часть времени, поэтому для некоторых разработчиков этих самых типов как будто «нет». Всё ещё запутывают аннотации типов, которые часто называют типизацией. И добивает это всё наличие статически типизированных, компилируемых языков. +Поэтому, когда мы читаем оригинальную трактовку LSP принципа, то возникает вопрос — «а применимо ли это к питону?». Здесь я бы вспомнил, что SOLID — в первую очередь про ООП (в нашей трактовке). Поэтому, мы у себя слово «типы» заменяем на «классы». И получается, что принцип (не очень точный перевод на русский) «типы должны заменяться на подтипы без нарушения работы программы» мы можем перефразировать как «классы должны заменяться на подклассы без нарушения работы программы» для Python. Наш подход к этому принципу такой: чтобы достичь LSP, в первую очередь нам необходимо покрывать код аннотациями типов полностью. К сожалению даже так, **не все** кейсы закрываются аннотациями, но, тем не менее, большая часть всё таки закрывается. Чтобы вас не мучать сразу приведём пример случая, который не покрывается mypy: -```python -class ParentShape: +```python +class ParentShape: ... -class Circle(ParentShape): - def draw_shape(self) -> None: +class Circle(ParentShape): + def draw_shape(self) -> None: ... -class Rectangle(ParentShape): - def draw_shape(self) -> None: +class Rectangle(ParentShape): + def draw_shape(self) -> None: ... -# аннотация ParentShape — источник нарушения LSP -def draw_some_shape(one_shape: ParentShape) -> None: - # нарушение принципа OCP - if isinstance(one_shape, Circle) or isinstance(one_shape, Rectangle): - one_shape.draw_shape() +# аннотация ParentShape — источник нарушения LSP +def draw_some_shape(one_shape: ParentShape) -> None: + # нарушение принципа OCP + if isinstance(one_shape, Circle) or isinstance(one_shape, Rectangle): + one_shape.draw_shape() ``` -Это нарушение и LSP и OCP принципов сразу. OCP нарушается потому, что каждый раз, когда мы захотим добавить новую фигуру, нам надо будет менять код \`draw_some_shape\`. LSP нарушается потому, что в предке ParentShape нет метода draw_shape. А это означает, что если мы хотим в программе ParentShape заменить на Circle, например, то LSP принцип будет нарушен, т.к. невозможно это сделать без нарушения функционирования программы. При этом, мы понимаем, что код с isinstance плохой, но это не каждому разработчику очевидно, этот код здесь довольно «натянутый». Есть сложность и с ParentShape. Мы с вами понимаем, что это некий «базовый» класс, чей прямой инстанс по коду мы, скорее всего, не встретим. Однако, знание, что это нарушает LSP, нас здесь толкает либо к тому, чтобы отказаться от ParentShape в пользу Protocol, добавить к нему метод draw_shape, указывать union типов Circle | Rectangle или воспользоваться (не стоит) Any. Я бы выбрал Protocol, тогда из проблем в коде останется только нарушение OCP принципа. Конечно, реальные программисты так редко пишут, но это всё таки случается. + + +Это нарушение и LSP и OCP принципов сразу. OCP нарушается потому, что каждый раз, когда мы захотим добавить новую фигуру, нам надо будет менять код \`draw_some_shape\`. LSP нарушается потому, что в предке ParentShape нет метода draw_shape. А это означает, что если мы хотим в программе ParentShape заменить на Circle, например, то LSP принцип будет нарушен, т.к. невозможно это сделать без нарушения функционирования программы. При этом, мы понимаем, что код с isinstance плохой, но это не каждому разработчику очевидно, этот код здесь довольно «натянутый». Есть сложность и с ParentShape. Мы с вами понимаем, что это некий «базовый» класс, чей прямой инстанс по коду мы, скорее всего, не встретим. Однако, знание, что это нарушает LSP, нас здесь толкает либо к тому, чтобы отказаться от ParentShape в пользу Protocol, добавить к нему метод draw_shape, указывать union типов Circle | Rectangle или воспользоваться (не стоит) Any. Я бы выбрал Protocol, тогда из проблем в коде останется только нарушение OCP принципа. Конечно, реальные программисты так редко пишут, но это всё таки случается. Важный дополнительный вывод: нарушение LSP появляется не когда мы пишем классы, а когда их применяем. А теперь рассмотрим более очевидный пример. Он показывает как сломается программа, если мы заменим класс Animal на класс Dog по всему нашему коду: -```python -class Animal: - def make_sound(self): +```python +class Animal: + def make_sound(self): return "generic sound" -class Dog(Animal): - def make_sound(self): +class Dog(Animal): + def make_sound(self): return ["гав", "гав"] -def listen(animal: Animal): +def listen(animal: Animal): print(animal.make_sound().upper()) -listen(Animal()) -listen(Dog()) # Ошибка: 'list' object has no attribute 'upper' +listen(Animal()) +listen(Dog()) # Ошибка: 'list' object has no attribute 'upper' ``` Нарушение LSP принципа произошло в том, что мы написали класс Dog и метод make_sound в нём таким образом, что возвращаемый тип полностью отличен от предка. Если мы добавим сюда аннотации типов, то mypy не даст нам совершить такую ошибку. + + Вот и ещё пример нарушения LSP: -```python -class Bird: - def fly(self): +```python +class Bird: + def fly(self): ... -class Penguin(Bird): - def fly(self): - raise NotImplementedError("...") -``` +class Penguin(Bird): + def fly(self): + raise NotImplementedError("...") +``` Такое нарушение mypy так же не сможет «поймать», хотя здесь мы нарушаем сразу и LSP и ISP принципы. Нарушение LSP происходит потому, что если мы заменим в программе Bird на Penguin, то вызовы метода fly совсем не будут ожидать ошибки. ISP же мы нарушаем, т.к. «потребителю» Penguin не нужен метод fly, но он его получает. Совершенно очевидно, что в таком случае Penguin не должен быть «потомком» Bird, тогда и наши проблемы исчезнут. Данный пример очень «картонный», а в реальной жизни обычно такие случаи устроены сложнее, однако на этом примере легко иллюстрировать проблемы. Как мы видим, LSP принцип легко соблюдать, но при этом легко нарушать, указав неправильные типы и даже неправильно организовав наследование. Во многом вам здесь помогает mypy, но иногда даже он не может понять есть ли нарушение и здесь вам стоит полагаться на свою внимательность и процесс код ревью. -Аргументы в пользу соблюдения LSP: -— помогает устойчиво работать с полиморфизмом -— помогает достигать более чистого ООП дизайна, т.е. код становится более читаемым -— позволяет достигать большей безопасности вашего кода. Т.к. при расширении кодовой базы мы с меньшей вероятностью сломаем код +Аргументы в пользу соблюдения LSP: +— помогает устойчиво работать с полиморфизмом +— помогает достигать более чистого ООП дизайна, т.е. код становится более читаемым +— позволяет достигать большей безопасности вашего кода. Т.к. при расширении кодовой базы мы с меньшей вероятностью сломаем код — позволяет достигать расширяемости кода, потому, что LSP — это enabler для OCP ## Принцип разделения интерфейсов (Interface Segregation Principle, ISP) + + > Clients should not be forced to depend on methods that they do not use. -Наша трактовка этого принципа адаптирована для python: интерфейсы (здесь референс к началу ^) не должны содержать методов, которыми не будут пользоваться клиенты; стоит делать раздельные маленькие интерфейсы для разных клиентов, исходя из их потребностей. +Наша трактовка этого принципа адаптирована для python: интерфейсы (здесь референс к началу ^) не должны содержать методов, которыми не будут пользоваться клиенты; стоит делать раздельные маленькие интерфейсы для разных клиентов, исходя из их потребностей. Иными словами, если вы пишите интерфейс для того, чтобы его использовали в какой-то части кода и там нужно три метода, то пишите три метода, не стоит писать 10 или 15 методов «про запас». В оригинальной трактовке сформулирована идея, что клиент не должен зависеть от методов, которыми не пользуется. Но в питоне, благодаря duck typing, не похоже чтобы мы «зависели» напрямую от методов, которыми не пользуемся. Рассмотрим такой кейс: ```python @@ -413,50 +429,52 @@ make_a_copy(scanner=Scanner(), printer=Printer()) Почему так лучше: -- Клиентская функция `make_a_copy` зависит только от необходимых интерфейсов — **снижение [связности](https://ru.wikipedia.org/wiki/Зацепление_\(программирование\))**. +- Клиентская функция `make_a_copy` зависит только от необходимых интерфейсов — **снижение [связности](https://ru.wikipedia.org/wiki/Зацепление_\(программирование\))**. - Сканирование и печать могут быть реализованы разными девайсами — **гибкость и расширяемость**. ## Принцип инверсии зависимостей (Dependency Inversion Principle, DIP) -> a. High-level modules should not depend on low-level modules. Both should depend on abstractions. + + +> a. High-level modules should not depend on low-level modules. Both should depend on abstractions. > b. Abstractions should not depend on details. Details should depend on abstractions. Высокоуровневые модули должны зависеть от абстракций, а не завязываться на конкретных реализациях — так звучит принцип в вольном переводе на русский язык. Можно перефразировать: «в качестве зависимостей полагайтесь на абстракции, а не конкретную реализацию». Здесь возникают вопросы. Для начала, что такое абстракция (термин из ооп, абстрактный класс, протокол, просто какой-то класс)? Под абстракцией конкретно мы (впрочем, Мартин идёт туда же) понимаем «интерфейс» (определение в начале статьи). А что такое зависимость? С этим чуть сложнее, но мы предполагаем, что если какой-то части кода нужна другая часть кода для выполнения своих задач, то мы считаем, что последняя часть является зависимостью для первой. Например: если есть класс A и метод get_some, а в нём для работы нам понадобится класс B и его метод get_another, то получается, что класс A зависит от класса B. И вот мы подходим к DI паттерну. Это тема, которая отличается от DIP принципа. Давайте для начала разберемся, что такое DI паттерн. Вот наглядный пример DI паттерна: -```python -# Плохо: -class CrmClient: - def fetch_user_balance_from_crm(self, user_uuid: str) -> decimal.Decimal: - httpx_connection: httpx.Client = httpx.Client(...) +```python +# Плохо: +class CrmClient: + def fetch_user_balance_from_crm(self, user_uuid: str) -> decimal.Decimal: + httpx_connection: httpx.Client = httpx.Client(...) ... -# «Ручной» DI, мы ожидаем зависимость снаружи: -class CrmClient: - def fetch_user_balance_from_crm(self, httpx_connection: httpx.Client, user_uuid: str) -> decimal.Decimal: +# «Ручной» DI, мы ожидаем зависимость снаружи: +class CrmClient: + def fetch_user_balance_from_crm(self, httpx_connection: httpx.Client, user_uuid: str) -> decimal.Decimal: ... -# «Ручной» DI можно сделать лучше: -@dataclasses.dataclass(kw_only=True, slots=True, frozen=True) # избавляет от бойлерплейта, уменьшает количество ошибок \+ память -class CrmClient: - # теперь можно CrmClient разместить в DI фреймворке и получать httpx.Client снаружи - # а можно просто создавать его руками, если вы не хотите брать DI фреймворк +# «Ручной» DI можно сделать лучше: +@dataclasses.dataclass(kw_only=True, slots=True, frozen=True) # избавляет от бойлерплейта, уменьшает количество ошибок \+ память +class CrmClient: + # теперь можно CrmClient разместить в DI фреймворке и получать httpx.Client снаружи + # а можно просто создавать его руками, если вы не хотите брать DI фреймворк httpx_connection: httpx.Client - def fetch_user_balance_from_crm(self, user_uuid: str) -> decimal.Decimal: - … + def fetch_user_balance_from_crm(self, user_uuid: str) -> decimal.Decimal: + … ``` DI паттерн — это наш повседневный инструмент и мы верим в то, что он позволяет делать код проще и чище, удобнее для тестирования. Однако, как он связан с принципом DIP (возвращаясь к SOLID)? Прямой зависимости между ними нет, однако мы верим, что DI паттерн — это подход, помогающий воплощать DIP в жизнь. Однако, полностью его внедрив, DIP мы не достигнем. Потому что в определении сказано, что мы должны полагаться на абстракции, а не конкретную реализацию. Тогда как в DI мы часто используем конкретные реализации. Но, вспоминая утиную типизацию в python, мы можем быть уверены, что в тестах эти конкретные реализации мы можем подменять на моки, которые реализуют только нужные нам методы. Почему, вдруг, мы пишем про это? На наш взгляд, Мартин как раз исходя из потребностей тестируемости рекомендует завязываться на абстракции. Питон «исправляет» это своей гибкой природой. Поэтому, выходит, что мы вроде бы, внедряя DI, DIP не достигаем, а вроде бы, учитывая утиную типизацию, достигаем. Можно ли сказать, что мы однозначно правы? Наверное, нет, но мы предпочитаем считать, что формула «DI \+ утиная типизация = DIP» верна. -Следующий этап — это когда DI паттерн объединяем с DI фреймворком и где-то на горизонте начинает маячить термин IoC — inversion of control. Что ужасного в этом термине? Во-первых, он спутывается с термином DIP. Во-вторых, по нему нет консенсуса — что это такое, а в ряде русскоязычных телеграм чатов вас вообще перетрут в пыль, если вы поднимите вопрос IoC. Мы понимаем под IoC использование DI-«контейнеров» (мы их ещё называем IoC контейнерами), это когда нужные зависимости собираются в объект или объекты, где они определены в виде декларативных атрибутов, которые ссылаются друг на друга, этакий декларативный граф зависимостей вашего приложения. Вот пример: +Следующий этап — это когда DI паттерн объединяем с DI фреймворком и где-то на горизонте начинает маячить термин IoC — inversion of control. Что ужасного в этом термине? Во-первых, он спутывается с термином DIP. Во-вторых, по нему нет консенсуса — что это такое, а в ряде русскоязычных телеграм чатов вас вообще перетрут в пыль, если вы поднимите вопрос IoC. Мы понимаем под IoC использование DI-«контейнеров» (мы их ещё называем IoC контейнерами), это когда нужные зависимости собираются в объект или объекты, где они определены в виде декларативных атрибутов, которые ссылаются друг на друга, этакий декларативный граф зависимостей вашего приложения. Вот пример: ```python from that_depends import providers class Container(BaseContainer): my_local_config = providers.Singleton(Config) - db_session = providers.Factory(create_db_session, config=my_local_config.db) + db_session = providers.Factory(create_db_session, config=my_local_config.db) user_repository = providers.Factory( UserRepository, my_local_config.users, @@ -464,10 +482,10 @@ class Container(BaseContainer): ) ``` -Тут, наверное, возникает философский вопрос — как нам различать что должно попадать в DI-контейнер, а что должно оставаться простым импортом. Здесь можно исходить из правила, что если это (не все признаки, но большая часть): -— объект (класс) -— он зависит от других объектов или является их зависимостью -— используется в нескольких местах +Тут, наверное, возникает философский вопрос — как нам различать что должно попадать в DI-контейнер, а что должно оставаться простым импортом. Здесь можно исходить из правила, что если это (не все признаки, но большая часть): +— объект (класс) +— он зависит от других объектов или является их зависимостью +— используется в нескольких местах — его нужно регулярно инстанцировать То тогда такую «штуку» имеет смысл размещать в DI-контейнере. Классический пример — соединение с базой данных. @@ -636,3 +654,6 @@ class InMemoryTodoService: ``` Однако если мы не планируем поддерживать несколько имплементаций `TodoService` (например, in-memory и Postgres) и будем тестировать FastAPI-приложение интеграционно (не мокая `TodoService`), то можно обойтись без интерфейса. + + + From 4826cc6cb71e7b16d52e4922dba5eadfc320d634 Mon Sep 17 00:00:00 2001 From: Lev Vereshchagin Date: Mon, 8 Sep 2025 13:16:12 +0300 Subject: [PATCH 2/2] Fixed typo in DIP principle description comment --- solid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solid.md b/solid.md index 031ebf9..92cd08b 100644 --- a/solid.md +++ b/solid.md @@ -434,7 +434,7 @@ make_a_copy(scanner=Scanner(), printer=Printer()) ## Принцип инверсии зависимостей (Dependency Inversion Principle, DIP) - + > a. High-level modules should not depend on low-level modules. Both should depend on abstractions. > b. Abstractions should not depend on details. Details should depend on abstractions.