Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions solid.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,26 @@ SOLID — это набор принципов от Роберта Мартин
### Важные примечания

* применять SOLID стоит осознанно, никакого «самостоятельного», «интуитивного» пути нет
<!-- 1. Не хватает пояснения, а зачем вообще покрывать аннотацией код. Исхожу из понимания, что "примечания" – поясняют текст, а здесь постулат. Добавил бы комментарий в стиле: так вы сможете уменьшить количество ошибок ... . Также по интерфейсам, я бы чуть раскрыл, почему не подойдут закрытые методы, ведь интерфейсом он не перестаёт быть. -->
* необходимо использовать mypy (или red_knot, когда его сделают) и покрывать весь код 100% аннотациями типов (кроме тех случаев, когда mypy сам выводит типы)
* интерфейс — это либо набор публичных методов (без подчеркиваний), либо abc, либо typing.Protocol
<!-- -->
* мы трактуем, что SOLID — это про ООП, и применяем его для ООП (но не отрицаем, что какие-то из принципов применимы и за рамками ООП)

# Принципы
Здесь и далее изложена наша трактовка SOLID принципов. В качестве первоисточника мы используем [Agile software patterns, principles and development](https://dl.ebooksworld.ir/motoman/Pearson.Agile.Software.Development.Principles.Patterns.and.Practices.www.EBooksWorld.ir.pdf) от [Роберта Мартина](https://www.google.com/search?q=robert+martin+in+the+bathrobe&sca_esv=e98085266670db2f&rlz=1C5GCEM_enRU1156RU1162&udm=2&biw=1728&bih=958&sxsrf=AE3TifPGpbG0VjeuheuWgJOeImA0eGKkmw%3A1751981495217&ei=tx1taM-EDZK4wPAP6eyc2Q0&ved=0ahUKEwiPvLPVr62OAxUSHBAIHWk2J9sQ4dUDCBA&uact=5&oq=robert+martin+in+the+bathrobe&gs_lp=EgNpbWciHXJvYmVydCBtYXJ0aW4gaW4gdGhlIGJhdGhyb2JlSKVmUI0OWLVlcAp4AJABAZgBlwOgAfEbqgEIMzcuMS40LTG4AQPIAQD4AQGYAgygAuIGwgIGEAAYBxgewgIIEAAYBxgKGB7CAggQABgHGAgYHsICBxAjGCcYyQLCAgUQABiABMICBxAAGIAEGBPCAgoQABiABBgTGMcDwgIGEAAYExgewgIIEAAYExgIGB6YAwCIBgGSBwIxMqAHlDyyBwIxMLgH4AbCBwUyLjcuM8gHGw&sclient=img#vhid=ZrTVGSeCQAXVKM&vssid=mosaic).

## Принцип единой ответственности (Single Responsibility Principle, SRP)

<!-- 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 — это ваша командная договоренность. Вы внутри команды должны договориться о том, что является причиной для изменений. Аргументом почему это нужно делать является код, иллюстрирующий работу с модемом \[вставить сюда код с модемом\]. У нас есть 4 метода и программисты бьются на несколько групп. Кто-то считает, что ответственность едина — мы, ведь, работаем с модемом. Работа с модемом — это ответственность, и причина изменений. Кто-то считает, что работа с каналом связи — это одна причина для изменений, а работа с передачей данных — другая. Если мы с вами будем пытаться решить кто из них прав, то сойдем с ума. А нам платят не за философию, поэтому мы предлагаем вам выбрать (и выбирать в будущем) ваши причины и ответственности внутри команды самостоятельно (кстати, на мой вкус, один класс модема — это вполне SRP).

Возникает вопрос — «а как нам договориться внутри команды о трактовке слова причина?». Подход довольно прост — мы можем это уточнять в процессе ревью. А изначально можно выбирать исходя из вашего чувства прекрасного или обратившись в сообщество за примерами.
Expand Down Expand Up @@ -140,14 +146,18 @@ class UserService:
Переводя принцип на русский получим что-то вроде «программные сущности (классы, модули, функции и так далее) должны быть открыты для расширения, но закрыты для модификации». Проблема формулировки здесь в том, что непонятно что такое «расширение» и что такое «модификация». И даже после чтения книги намного понятнее не становится.

Поэтому вот наша трактовка.
Принцип, по сути, описывает следующую идею: пишите код так, чтобы вы могли вносить в него новую функциональность без переписывания старого, а путем написания нового. Т.е. если вы пишете код, который вам понадобится уже в следующем пуллреквесте «рефакторить» (читай удалять), то принцип OCP вы скорее всего нарушаете
Принцип, по сути, описывает следующую идею: пишите код так, чтобы вы могли вносить в него новую функциональность без переписывания старого, а путем написания нового. Т.е. если вы пишете код, который вам понадобится уже в следующем пуллреквесте «рефакторить» (читай: удалять), то принцип OCP вы скорее всего нарушаете
Сама идея практически понятна для всех программистов. По сути, нас с самого начала учат, что когда нам надо написать функцию, которая складывает 2 \+ 3, мы пишем sum_two_numbers = lambda a,b: a \+ b, sum_two_numbers(2, 3), а не просто «2 \+ 3» в коде. Это базовое умение программиста — обобщать задачи. По сути, OCP очень близко к этой идее, мы здесь достаточным образом обобщаем функциональность, думаем о будущем и позволяем будущим нам или нашим коллегам не переписывая нашего кода добавлять новые функции в наш продукт
Однако, все это становится куда менее понятным, когда мы практикуем этот принцип в «развесистом» ооп коде, который нам приходится писать. Понятно ли как написать класс, который будет расширяться, но не переписываться? Какие функции бизнес захочет завтра? Как сделать так, чтобы модуль, который мы пишем для сегодняшней задачи, нам не понадобилось уже завтра переименовывать и переписывать? На эти вопросы, на самом деле, нет прямых ответов. И, возвращаясь к предыдущему, нам платят не за философию, поэтому превратим это в практическое руководство к действию:

<!-- OCP: практическое руководство к действию – классно придумано. -->

* наша задача продумать возможность расширения ооп кода таким образом, чтобы не понадобилось этот код переписывать завтра
* т.к. это сложно, вы можете отслеживать нарушение принципа — если вы видите, что вы или ваш коллега часто переписывает один и тот же код, считайте что у вас проблема с OCP
* если у вас есть проблема с OCP — ваша задача переписать этот код так, чтобы в следующий раз вам не понадобилось его переписывать вновь
* нет нужды следовать принципу фанатично. Т.е. если вы столкнулись переписыванием одного и того же кода, устранили проблему, но иногда небольшими (определите внутри что такое «небольшими», на мой взгляд 1-5 строк, может 10\) кусками продолжаете его изменять, то в этом, вероятно, нет большой проблемы
<!-- Длинные скобки. Я бы разбил на маленькие или через запятую вставил мнение автора. -->
* нет нужды следовать принципу фанатично. Т.е. если вы столкнулись переписыванием одного и того же кода, устранили проблему, но иногда небольшими (определите внутри что такое «небольшими», на мой взгляд 1-5 строк, может 10\) кусками продолжаете его
изменять, то в этом, вероятно, нет большой проблемы
* лучше не изнурять себя неукоснительным соблюдением принципа потому, что вы можете развить в себе невроз, так и не достигнув сто процентного соблюдения OCP

#### Пример
Expand Down Expand Up @@ -265,6 +275,8 @@ def draw_some_shape(one_shape: ParentShape) -> None:
one_shape.draw_shape()
```

<!-- Не понимаю, почему Protocol лучше. -->

Это нарушение и 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 появляется не когда мы пишем классы, а когда их применяем.

Expand All @@ -289,6 +301,8 @@ listen(Dog()) # Ошибка: 'list' object has no attribute 'upper'

Нарушение LSP принципа произошло в том, что мы написали класс Dog и метод make_sound в нём таким образом, что возвращаемый тип полностью отличен от предка. Если мы добавим сюда аннотации типов, то mypy не даст нам совершить такую ошибку.

<!-- LSP: Пример с пингвином понятный, на пальцах. Добавил бы пример с корректным LSP принципом. Почему нужен пример? Задаюсь вопросом. Если у нас два класса/имплементированных метода друг друга могут заменять, то чем отличаются тогда эти два класса/методы? -->

Вот и ещё пример нарушения LSP:

```python
Expand All @@ -312,6 +326,8 @@ class Penguin(Bird):

## Принцип разделения интерфейсов (Interface Segregation Principle, ISP)

<!-- ISP: Всё понятно -->

> Clients should not be forced to depend on methods that they do not use.

Наша трактовка этого принципа адаптирована для python: интерфейсы (здесь референс к началу ^) не должны содержать методов, которыми не будут пользоваться клиенты; стоит делать раздельные маленькие интерфейсы для разных клиентов, исходя из их потребностей.
Expand Down Expand Up @@ -418,6 +434,8 @@ make_a_copy(scanner=Scanner(), printer=Printer())

## Принцип инверсии зависимостей (Dependency Inversion Principle, DIP)

<!-- DIP: В примерах бы добавил конкретики, почему плохо здесь и хорошо там. Запутался про IoC: разграничил бы понятия чётче. DI – dependency inversion? На мой взгляд, не помешало бы в одно предложение сказать, что утиная типизация – это когда класс реализует ... И в целом бы какой-то конклюжн, чем по итогу принцип полезен -->

> 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.

Expand Down Expand Up @@ -636,3 +654,6 @@ class InMemoryTodoService:
```

Однако если мы не планируем поддерживать несколько имплементаций `TodoService` (например, in-memory и Postgres) и будем тестировать FastAPI-приложение интеграционно (не мокая `TodoService`), то можно обойтись без интерфейса.

<!-- Общее замечание, на мой взгляд. Я бы уменьшил количество пояснений в скобках. Читать тяжело. Можно вынести в отдельное предложение или в квадратные скобки, чтобы от контекста не отвлекать -->
<!-- В целом зашло – интересно, просто, видно, что не GPT сформировал ответ. Примеры интересные, можно было бы местами чуть с разных сторон подать на примере, но уже как посчитаешь нужным. Не хватило заключений или микровыводов. -->