diff --git a/solid.md b/solid.md index 70edb77..92cd08b 100644 --- a/solid.md +++ b/solid.md @@ -62,8 +62,10 @@ SOLID — это набор принципов от Роберта Мартин ### Важные примечания * применять SOLID стоит осознанно, никакого «самостоятельного», «интуитивного» пути нет + * необходимо использовать mypy (или red_knot, когда его сделают) и покрывать весь код 100% аннотациями типов (кроме тех случаев, когда mypy сам выводит типы) * интерфейс — это либо набор публичных методов (без подчеркиваний), либо abc, либо typing.Protocol + * мы трактуем, что SOLID — это про ООП, и применяем его для ООП (но не отрицаем, что какие-то из принципов применимы и за рамками ООП) # Принципы @@ -71,11 +73,15 @@ 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 — это ваша командная договоренность. Вы внутри команды должны договориться о том, что является причиной для изменений. Аргументом почему это нужно делать является код, иллюстрирующий работу с модемом \[вставить сюда код с модемом\]. У нас есть 4 метода и программисты бьются на несколько групп. Кто-то считает, что ответственность едина — мы, ведь, работаем с модемом. Работа с модемом — это ответственность, и причина изменений. Кто-то считает, что работа с каналом связи — это одна причина для изменений, а работа с передачей данных — другая. Если мы с вами будем пытаться решить кто из них прав, то сойдем с ума. А нам платят не за философию, поэтому мы предлагаем вам выбрать (и выбирать в будущем) ваши причины и ответственности внутри команды самостоятельно (кстати, на мой вкус, один класс модема — это вполне SRP). Возникает вопрос — «а как нам договориться внутри команды о трактовке слова причина?». Подход довольно прост — мы можем это уточнять в процессе ревью. А изначально можно выбирать исходя из вашего чувства прекрасного или обратившись в сообщество за примерами. @@ -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 — ваша задача переписать этот код так, чтобы в следующий раз вам не понадобилось его переписывать вновь -* нет нужды следовать принципу фанатично. Т.е. если вы столкнулись переписыванием одного и того же кода, устранили проблему, но иногда небольшими (определите внутри что такое «небольшими», на мой взгляд 1-5 строк, может 10\) кусками продолжаете его изменять, то в этом, вероятно, нет большой проблемы + +* нет нужды следовать принципу фанатично. Т.е. если вы столкнулись переписыванием одного и того же кода, устранили проблему, но иногда небольшими (определите внутри что такое «небольшими», на мой взгляд 1-5 строк, может 10\) кусками продолжаете его +изменять, то в этом, вероятно, нет большой проблемы * лучше не изнурять себя неукоснительным соблюдением принципа потому, что вы можете развить в себе невроз, так и не достигнув сто процентного соблюдения OCP #### Пример @@ -265,6 +275,8 @@ def draw_some_shape(one_shape: ParentShape) -> None: 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 появляется не когда мы пишем классы, а когда их применяем. @@ -289,6 +301,8 @@ listen(Dog()) # Ошибка: 'list' object has no attribute 'upper' Нарушение LSP принципа произошло в том, что мы написали класс Dog и метод make_sound в нём таким образом, что возвращаемый тип полностью отличен от предка. Если мы добавим сюда аннотации типов, то mypy не даст нам совершить такую ошибку. + + Вот и ещё пример нарушения LSP: ```python @@ -312,6 +326,8 @@ class Penguin(Bird): ## Принцип разделения интерфейсов (Interface Segregation Principle, ISP) + + > Clients should not be forced to depend on methods that they do not use. Наша трактовка этого принципа адаптирована для python: интерфейсы (здесь референс к началу ^) не должны содержать методов, которыми не будут пользоваться клиенты; стоит делать раздельные маленькие интерфейсы для разных клиентов, исходя из их потребностей. @@ -418,6 +434,8 @@ 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. @@ -636,3 +654,6 @@ class InMemoryTodoService: ``` Однако если мы не планируем поддерживать несколько имплементаций `TodoService` (например, in-memory и Postgres) и будем тестировать FastAPI-приложение интеграционно (не мокая `TodoService`), то можно обойтись без интерфейса. + + +