Принципы SOLID в Python
Индиана Джонс ненавидит змей, но когда узнал о Питоне, передумал.
Всем привет. Не так давно я писал о принципах SOLID и эта статья стала для меня самой популярной. Вот почему я решил показать вам это в коде, чтобы вы поняли, как это работает на практике.
- Предыдущая часть
- Давайте начнем
- Спой мне песню о принципе единой ответственности.
- So.
Как я уже говорил в предыдущей статье, этот принцип говорит нам, что объект должен выполнять ТОЛЬКО одну работу. Давайте посмотрим на класс.
Как видите, мы можем вынуть из нашей коробки какой-то предмет, лежащий внутри. Эта функция работает со свойствами ящика. В этом же классе мы можем продать коробку. Вы когда-нибудь видели коробку, которая продает сама себя?
Для этого применим деление и создадим класс продавца.
Ой. Существует принцип открытого-закрытого
Я мог бы усложнить, но по своей сути этот принцип все же является разделением объектов, и главное сделать это преждевременно. Давайте перейдем к коду.
Как видите, мы не стали делать отдельное свойство класса, кризисный это период или нет. Поэтому никакой дополнительной обработки мы не делали.
Лисков говорит: «Существующие в нашей программе сущности должны легко заменяться на подтипы, и программа должна работать корректно».
Поскольку мы уже привязаны к классам, сформулируем этот принцип применительно к ним.
Дочерний класс должен соответствовать их родительскому классу. Вот и все, ребята.
Например, у нас есть ящик основного класса, а его подклассы — «коробка с чем-то». Если нам нужно посчитать все предметы внутри ящиков, у нас не должно возникнуть проблем с тем, что какой-то класс не соответствует ящику. Для этого мы должны указать функцию «получить объекты» в каждом классе. Ведь если ящик закрыт и такой возможности нет, мы не сможем считать.
Принцип разделения интерфейсов.
Давайте будем краткими. «Клиент» не должен пострадать, если вы решите вернуть сразу несколько сущностей в одной и той же функции. Попробуйте разделить код на отдельные интерфейсы, каждый из которых будет возвращать разные значения.
Погружение в принцип инверсии зависимостей.
Это довольно простой принцип. Сразу приведу аналогию. Независимо от того, есть у звезды смерти эта уязвимая дыра или нет, так или иначе она остается звездой смерти. Даже если звезда смерти имеет другой цвет, но все равно остается звездой смерти, она все равно будет звездой смерти.
Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Классы верхнего и нижнего уровня должны зависеть от одних и тех же абстракций. Абстракции не должны зависеть от частей. Части должны зависеть от абстракций.
На примере коробки. У нас есть класс «контейнер», который может содержать некоторые объекты. Однако контейнером может быть как коробка, так и бутылка. Таким образом, мы наследуем от контейнера способность что-то вмещать, но мы также наделяем коробку и бутылку определенными новыми свойствами и функциями.
Заключение
Внимание! Спасибо за внимание. Придерживайтесь этих принципов, и ваш код будет намного лучше структурирован, что позволит вам избежать ошибок, когда вам придется переписывать все с нуля.
Друзья, количество подписчиков для меня очень важно в данный момент. Для меня это отличный стимул писать новые статьи. Так что подписывайтесь и делитесь.
Удачи в техническом творчестве.
Давайте оставаться на связи. ЛинкедИн | Средний
Больше контента на plainenglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Получите эксклюзивный доступ к возможностям написания и советам в нашем сообществе Discord.
PYTHON SOLID ПРИНЦИПЫ
Принципы SOLID — это набор принципов проектирования объектно-ориентированного кода, которые помогают создавать более гибкие, расширяемые и поддерживаемые приложения.
Принцип единственной ответственности (Single Responsibility Principle) — каждый класс должен иметь только одну ответственность. Это означает, что он должен отвечать за только один аспект системы, и если этот аспект меняется, то класс должен меняться только по этому поводу.
Принцип открытости/закрытости (Open/Closed Principle) — программные сущности (классы, модули, функции и т.д.) должны быть открыты для расширения, но закрыты для модификации. Это означает, что если нужно добавить новый функционал, не нужно менять существующий код, а можно добавить новый класс, который наследует функционал старого.
Принцип подстановки Барбары Лисков (Liskov Substitution Principle) — базовые классы должны быть заменяемы своими производными классами. Это означает, что любой экземпляр производного класса должен можно использовать вместо экземпляра базового класса, не нарушая работу программы.
Принцип разделения интерфейса (Interface Segregation Principle) — клиенты не должны зависеть от интерфейсов, которые они не используют. Если класс реализует несколько интерфейсов, но использует только часть их методов, то нужно разделить интерфейсы на более мелкие, чтобы каждый класс имел только ту функциональность, которая ему нужна.
Принцип инверсии зависимости (Dependency Inversion Principle) — модули верхнего уровня не должны зависеть от модулей нижнего уровня, а оба типа модулей должны зависеть от абстракций. Это означает, что нужно программировать на уровне абстракций, а не реализаций конкретных классов.
class Rectangle: def __init__(self, width, height): self._width = width self._height = height def area(self): return self._width * self._heightclass Square(Rectangle): def __init__(self, side): super().__init__(side, side)r = Rectangle(2, 3)s = Square(2)print(r.area())print(s.area())
КАК ПИСАТЬ ХОРОШИЙ КОД? SOLID В ПИТОНЕ
- SOLID принципы на Python: SRP — Принцип единственной ответственности
- SOLID принципы на Python: DIP — Принцип инверсии зависимостей / Dependency Inversion Principle
- SOLID принципы: SRP (Принцип единственной ответственности, Single Responsibility Principle)
- Принципы SOLID / С Примерами На Python
SOLID-принципы. Введение в ООП на Python.
- Принципы SOLID — На примере Python
- SOLID ПРИНЦИПЫ простым языком (много примеров)
- Степан Саржан собеседование junior python разработчик. Вопросы из Тинькова
- SOLID принципы на Python: OCP — Принцип открытости / закрытости
BLGPG-617A8DD3D0E8-23-10-13-18
Новые материалы:
SOLID принципы с примерами на Python
Примечания
- Под клиентами подразумеваются программные сущности, использующие другие программные сущности.
- Этот файл является переводом статьи с сайта medium.com пользователя DeeKey, ссылка в завершении файла.
SOLID — это мнемоническая аббревиатура для набора принципов проектирования, созданных для разработки программного обеспечения при помощи объектно-ориентированных языков. Принципы SOLID направленны на содействие разработки более простого, надежного и обновляемого кода. Каждая буква в аббревиатуре SOLID соответствует одному принципу разработки.
При правильной реализации это делает ваш код более расширяемым, логичным и легким для чтения.
Для понимания SOLID принципов, вы должны хорошо понимать как, используются интерфейсы.
Я попытаюсь объяснить принципы SOLID на примере Python в как можно более простой форме, чтобы даже новички смогли разобраться. Чтобы было очень легко взять представленные примеры и применить их на Python.
Рассмотрим каждый принцип один за другим:
1. Single Responsibility Principle (Принцип единственной обязанности)
Принцип единственной обязанности требует того, чтобы один класс выполнял только одну работу. Таким образом, если у класса есть более одной работы, он становится зависимым. Изменение поведения одной работы класса приводит к изменению в другой.
# Below is Given a class which has two responsibilities
class User: def __init__(self, name: str): self.name = name def get_name(self) -> str: pass def save(self, user: User): pass
Мы имеем класс User, который ответственен за две работы — свойства пользователя и управление базой данных.
Если в приложении будет изменен функционал управления базой данных для пользователя, тогда классы использующие свойства класса User тоже придется доработать и перекомпилировать, чтобы компенсировать новые изменения. Это как домино эффект, уроните одну кость, и она уронит все за ней следом.
Мы же просто разделим класс. Мы создадим ещё один класс, который возьмет на себя одну ответственность — управление базой данных пользователя.
class User: def __init__(self, name: str): self.name = name def get_name(self): pass class UserDB: def get_user(self, id) -> User: pass def save(self, user: User): pass
Распространённым решением этой проблемы является применение шаблона проектирования Фасад. Ознакомиться с паттерном Фасад вы можете здесь. User класс был бы фасадом для управления базой данных пользователя и управления свойствами пользователя.
2. Open-Closed Principle (Принцип открытости/закрытости)
Программные сущности (классы, модули, функции) должно быть открыты для расширения, но не модификации.
Давайте представим, что у вас есть магазин, и вы даете скидку в 20% для ваших любимых покупателей используя класс Discount. Если бы вы решаете удвоить 20-ти процентную скидку для VIP клиентов, вы могли бы изменить класс следующим образом:
class Discount: def __init__(self, customer, price): self.customer = customer self.price = price def give_discount(self): if self.customer == 'fav': return self.price * 0.2 if self.customer == 'vip': return self.price * 0.4
Но нет, это нарушает OCP. OCP запрещает это. Например, если мы хотим дать новую скидку для другого типа покупателей, то это требует добавления новой логики. Чтобы следовать OCP, мы добавим новый класс, который будет расширять Discount. И в этом новом классе реализуем требуемую логику:
class Discount: def __init__(self, customer, price): self.customer = customer self.price = price def get_discount(self): return self.price * 0.2 class VIPDiscount(Discount): def get_discount(self): return super().get_discount() * 2
Если вы решите дать скидку супер VIP пользователям, то это будет выглядеть так:
class SuperVIPDiscount(VIPDiscount): def get_discount(self): return super().get_discount() * 2
Расширяйте, но не модифицируйте.
3. Liskov Substitution Principle (Принцип подстановки Лисков)
Главная идея, стоящая за Liskov Substitution Principle в том, что для любого класса клиент должен иметь возможность использовать любой подкласс базового класса, не замечая разницы между ними, и следовательно, без каких-либо изменений поведения программы при выполнении. Это означает, что клиент полностью изолирован и не подозревает об изменениях в иерархии классов.
Более формально:
Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.
Проще говоря, это значит, что подкласс, дочерний класс должны соответствовать их родительскому классу или супер классу.
class User(): def __init__(self, color, board): create_pieces() self.color = color self.board = board def move(self, piece:Piece, position:int): piece.move(position) chessmate_check() board = ChessBoard() user_white = User(«white», board) user_black = User(«black», board) pieces = user_white.pieces horse = helper.getHorse(user_white, 1) user.move(horse)
LSP это основа хорошего объектно-ориентированного проектирования программного обеспечения, потому что он следует одному из базовых принципов ООП — полиморфизму. Речь о том, чтобы создавать правильные иерархии, такие, что классы, производные от базового являлись полиморфными для их родителя по отношению к методам их интерфейсов.
Ещё интересно отметить, как этот принцип относится к примеру предыдущего принципа. Если мы пытаемся расширить класс новым несовместимым классом, то все сломается.
Взаимодействие с клиентом будет нарушено, и как результат, такое расширение будет невозможно (или, для того чтобы сделать это возможным, нам пришлось бы нарушить другой принцип и модифицировать код клиента, который должен быть закрыт для модификации, такое крайне нежелательно и неприемлемо).
Тщательное обдумывание новых классов в соответствии с LSP помогает нам расширять иерархию классов правильно. Также, LSP способствует OCP.
Создавайте тонкие интерфейсы, которые ориентированы на клиента. Клиенты не должны зависеть от интерфейсов, которые они не используют. Этот принцип устраняет недостатки реализации больших интерфейсов.
Чтобы полностью проиллюстрировать это, мы возьмем классический пример, потому что он очень показательный и легок для понимания. Классический пример:
class IShape: def draw(self): raise NotImplementedError class Circle(IShape): def draw(self): pass class Square(IShape): def draw(self): pass class Rectangle(IShape): def draw(self): pass
Еще один приятный трюк заключается в том, что в нашей бизнес-логике отдельный класс может реализовать несколько интерфейсов, если необходимо. Таким образом, мы может предоставить единую реализацию для всех общих методов между интерфейсами.
Сегрегированные интерфейсы заставляют нас больше думать о нашем коде с точки зрения клиента, что приведет нас к меньшей зависимости и более легкому тестированию.
Таким образом, мы не только сделали наш код лучше для клиента, но также это облегчило нам понимание, тестирование и реализацию кода для нас самих.
5. Dependecy Inversion Principle (Принцип инверсии зависимостей)
Зависимость должна быть от абстракций, а не от конкретики. Модули верхних уровней не должны зависеть от модулей нижних уровней. Классы и верхних, и нижних уровней должны зависеть от одних и тех же абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Наступает момент в разработке, когда наше приложение в основном состоит из модулей. Когда такое происходит, нам необходимо улучшать код используя внедрение зависимостей. Функционирование компонентов высокого уровня зависит от компонентов низкого уровня. Для создания определенного поведения вы можете использовать наследование или интерфейсы.
SOLID — Python — Дока
В основе SOLID — пять универсальных и применимых к любому ООП-языку принципов. Все они направлены на то, чтобы привести ваш код к слабой связанности и сильной связности.
- Single Responsibility — принцип единственной ответственности.
- Open-Closed — принцип открытости/закрытости.
- Liskov Substitution — принцип подстановки Барбары Лисков.
- Interface Segregation — принцип разделения интерфейса.
- Dependency Inversion — принцип инверсии зависимостей.
Слабая связанность означает, что модуль должен иметь как можно меньше зависимостей от других. Такой модуль легко переиспользовать и удобно тестировать.
Сильная связность означает, что все классы и методы, отвечающие за близкую функциональность, должны быть сгруппированы друг с другом. Размазанная по проекту логика или, наоборот, слишком близко соседствующие методы для разных задач, превратят ваш проект в запутанный клубок.
- Классы должны иметь одну и только одну причину для изменений.
- или
- Каждый класс должен отвечать только за одну операцию.
- Допустим, перед вами стоит задача: написать код, который будет готовить еду. Если структурировать код, отталкиваясь только от задания, у вас, скорее всего, получится примерно такой класс:
class KitchenRobot: def choose_food(self): pass def buy_food(self): pass def carry_food(self): pass def choose_dish(self): pass def prepare_ingredients(self): pass def cook_dish(self): pass def set_the_table(self): pass def wash_tableware(self): pass def clear_kitchen(self): pass def start(self): pass class KitchenRobot: def choose_food(self): pass def buy_food(self): pass def carry_food(self): pass def choose_dish(self): pass def prepare_ingredients(self): pass def cook_dish(self): pass def set_the_table(self): pass def wash_tableware(self): pass def clear_kitchen(self): pass def start(self): pass
У этого класса много ответственных задач: он должен уметь покупать, готовить еду, накрывать на стол, а также убирать после себя. При этом код ещё должен выполнять действия в правильном порядке.
Но если любой из этих процессов изменится, вам придётся править этот код: например, вместо многоразовой посуды, вы начнёте пользоваться одноразовой, тогда её мыть не надо или купите полуфабрикаты, тогда подготавливать ингредиенты не надо.
Чтобы проверить, нарушен ли принцип SRP, попробуйте описать то, чем занимается этот класс, в одном предложении. Получится что-то вроде: «Он стирает, сушит и гладит одежду». Наличие перечисления и союзов «и» — один из признаков возможного нарушения принципа единой ответственности.
Давайте избавимся от перечисления, разбив код на несколько отдельных классов.
class FoodSupply: def choose_food(self): pass def buy_food(self): pass def carry_food(self): pass def start(self): passclass DishCook: def choose_dish(self): pass def prepare_ingredients(self): pass def cook_dish(self): pass def start(self): passclass Waiter: def set_the_table(self): pass def start(self): passclass Cleaning: def wash_tableware(self): pass def clear_kitchen(self): pass def start(self): passclass KitchenRobot: def start(self): pass class FoodSupply: def choose_food(self): pass def buy_food(self): pass def carry_food(self): pass def start(self): pass class DishCook: def choose_dish(self): pass def prepare_ingredients(self): pass def cook_dish(self): pass def start(self): pass class Waiter: def set_the_table(self): pass def start(self): pass class Cleaning: def wash_tableware(self): pass def clear_kitchen(self): pass def start(self): pass class KitchenRobot: def start(self): pass
Теперь закупкой еды занимается класс FoodSupply, готовкой — класс DishCook, накрыванием на стол — класс Waiter, уборку — класс Cleaning, а за запуск процесса отвечает KitchenRobot. Если в классе FoodSupply найдётся ошибка, ваши исправления не затронут работающий код, потому что он находится в другом классе.
Программные сущности должны быть открыты для расширения, но закрыты для изменения.
Идея в том, что однажды написанный класс не должен никаким образом изменяться. Если вам требуются изменения, создайте класс-наследник и пишите код в нём.
Представим у нас есть класс, который описывает работу очереди. Там есть методы получения элемента из очереди и добавление элемента в очередь. Спустя некоторое время, у нас появилась необходимость очищать очередь. Чтобы следовать принципу Open-Closed, нам надо наследовать свойства от существующего класса и добавить туда наши новые методы поведения.
class PrimalQueue: def get_from_queue(self): pass def set_in_queue(self): passclass MutateQueue(PrimalQueue): def reset_queue(self): pass class PrimalQueue: def get_from_queue(self): pass def set_in_queue(self): pass class MutateQueue(PrimalQueue): def reset_queue(self): pass
- Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
- или
- Если П является подтипом Т, то любые объекты типа Т, присутствующие в программе, могут заменяться объектами типа П без негативных последствий для функциональности программы.
Этот принцип указывает, что наследники должны уметь всё то, что умеют их родители. Вы можете быть уверены, что код, который успешно работает с классом File, будет корректно работать с его «ребёнком» PdfFileи «внуком» EncryptedPdfFile.
Пример такой реализаций, можно посмотреть в примере к принципу Open-Closed.
Программные сущности не должны зависеть от методов, которые они не используют.
Когда ваш класс реализует интерфейс, ему могут достаться методы, которые совсем не нужны для его работы. Несмотря на это, их всё равно придётся реализовывать, иначе интерфейс будет считаться не соблюдённым. Если интерфейс по какой-то причине изменит сигнатуру этих методов или добавит новый, вам придётся вносить изменения в класс.
Для примера рассмотрим абстрактный класс Bird, который обязывает реализовать методы fly(), eat() и build_nest(). Создадим на его основе несколько птиц:
from abc import ABC, abstractmethodclass Bird(ABC): @abstractmethod def fly(self): pass @abstractmethod def build_nest(self): pass @abstractmethod def eat(self): passclass Eagle(Bird): def fly(self): «»» Лететь быстро и высоко «»» def build_nest(self): «»» Затаскивание веток на скалу «»» def eat(self): «»» Поедание вкусных мясных кусочков «»»class Colibri(Bird): def fly(self): «»» Лететь, выписывая «восьмёрки» «»» def build_nest(self): «»» построить гнездо из травинок и пуха «»» def eat(self): «»» Пить нектар «»» from abc import ABC, abstractmethod class Bird(ABC): @abstractmethod def fly(self): pass @abstractmethod def build_nest(self): pass @abstractmethod def eat(self): pass class Eagle(Bird): def fly(self): «»» Лететь быстро и высоко «»» def build_nest(self): «»» Затаскивание веток на скалу «»» def eat(self): «»» Поедание вкусных мясных кусочков «»» class Colibri(Bird): def fly(self): «»» Лететь, выписывая «восьмёрки» «»» def build_nest(self): «»» построить гнездо из травинок и пуха «»» def eat(self): «»» Пить нектар «»»
Пока всё идёт хорошо. Теперь попробуем добавить Пингвина, который не умеет летать.
class Pinguin(Bird): def build_nest(self): «»» Строить гнездо из камней «»» def eat(self): «»» Ловить рыбу «»» class Pinguin(Bird): def build_nest(self): «»» Строить гнездо из камней «»» def eat(self): «»» Ловить рыбу «»»
При создании экземпляра Pinguin вы получите ошибку TypeError: Can't instantiate abstract class Pinguin with abstract methods fly.Абстрактный класс заставляет вас реализовывать ненужный пингвину метод. Чтобы избавиться от этой проблемы, разбейте класс на Bird, FlyingBird и NestingBird, распределив методы между ними. Теперь реализация пингвина может выглядеть так:
class Pinguin(NestingBird, Bird): … class Pinguin(NestingBird, Bird): …
Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Принцип инверсии зависимостей предлагает избавиться от использования конструкторов явно заданных классов. Вместо этого высокоуровневый модуль должен объявить интерфейс, в котором он нуждается. Это даст ему возможность пользоваться любым из низкоуровневых модулей, реализовавших его требования.
class Endpoint: def __init__(self): self.value = Cache().get_value(key=»key»)class Cache: def __init__(self): … def get_value(self, key: str): return … class Endpoint: def __init__(self): self.value = Cache().get_value(key=»key») class Cache: def __init__(self): … def get_value(self, key: str): return …
Высокоуровневый класс Endpoint создаёт экземпляр класса Cache. У такого кода есть несколько проблем:
- Вы не можете заменить Cache на DummyCache или другую реализацию, не изменив класс.
- Вы не сможете протестировать свой код, не выполнив код из связанных классов.
Теперь применим к этому коду принцип инверсии зависимостей, добавив абстрактные классы между связями.
from abc import ABC, abstractmethodclass AbstractCache(ABC): @abstractmethod def get_value(self, key: str): …class Endpoint: def __init__(self, specific_cache: AbstractCache): self.value = specific_cache.get_value(key=»key»)class Cache(AbstractCache): def __init__(self): … def get_value(self, key: str): return … from abc import ABC, abstractmethod class AbstractCache(ABC): @abstractmethod def get_value(self, key: str): … class Endpoint: def __init__(self, specific_cache: AbstractCache): self.value = specific_cache.get_value(key=»key») class Cache(AbstractCache): def __init__(self): … def get_value(self, key: str): return …
SOLID Principles
SOLID is an acronym for a set of design principles that were introduced by Robert C. Martin, also known as Uncle Bob.
SOLID principles are a set of design principles that can help developers create maintainable and flexible code.
Table of Contents
Single Responsibility Principle (SRP)
The Single Responsibility Principle states that a class should have only one reason to change. In other words, a class
should have only one responsibility. This principle helps to keep the code organized and easy to maintain.
- Each class or function should have only one responsibility or reason to change.
- Use classes and functions to group related functionality.
Open/Closed Principle (OCP)
The Open/Closed Principle states that a class should be open for extension but closed for modification. This means that
you should be able to extend the behavior of a class without modifying its source code. This principle helps to ensure
that changes to one part of the codebase don’t have unintended consequences elsewhere.
- Classes should be open for extension but closed for modification.
- Use inheritance, polymorphism, and composition to achieve this.
The Liskov Substitution Principle states that subtypes should be substitutable for their base types. In other words, if
you have a method that takes a base type as a parameter, you should be able to pass in any subtype of that base type
without breaking the code. This principle helps to ensure that the code is flexible and can be easily extended.
- Subtypes must be substitutable for their base types.
- Ensure that child classes can be used in place of their parent classes without causing errors or unexpected behavior.
Interface Segregation Principle (ISP)
The Interface Segregation Principle states that clients should not be forced to depend on interfaces they don’t use. In
other words, you should split up large interfaces into smaller, more specific interfaces so that clients can depend on
only what they need. This principle helps to ensure that the code is modular and easy to maintain.
- Clients should not be forced to depend on interfaces they don’t use.
- Use multiple small interfaces rather than one large interface.
Dependency Inversion Principle (DIP)
The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Instead, both
should depend on abstractions. This principle helps to ensure that the code is flexible and can be easily extended.
- Depend on abstractions, not concrete implementations.
- Use dependency injection to provide dependencies to classes and functions.
When using SOLID principles, you can create code that is more maintainable, flexible, and testable. It helps you avoid
tight coupling between components, minimize code duplication, and increase the re-usability of your code.
SOLID Principles with Python Code Examples
SOLID is a mnemonic acronym of 5 acronyms themselves: SRP — Single Responsibility Principle, OCP — Open-Closed Principle, LSP — Liskov Substitution Principle, **
ISP** — Interface Segregation Principle, DIP — Dependency Inversion Principle.
These ideas were first mentioned by Robert C. Martin (Uncle Bob) in his
paper «Design Principles and Design Patterns». Later, Michael C. Feathers coined the SOLID acronym, which
was then re-used by Uncle Bob in the chapter «Design Principles» in his book «Clean Architecture».
Single Responsibility Principle (SRP)
There should never be more than one reason for a class to change
SRP: The Single Responsibility Principle — Robert C. Martin
The single responsibility principle was coined by Robert C. Martin himself but he also pays credit to Tom DeMarco's concept of cohesion, which he described
in «Structured Analysis and System Specification».
It is the first of the SOLID principles and encourages to give classes only a
single and definite reason to change. If you can think of more than a single reason for the class to change, the class has more than one responsibility.
SRP is often confused
with the «Do one thing»-rule At a first glance, this concept seems rather abstract and useless, but the example that is
provided in the initial paper is kind of helpful:
Imagine a rectangle class that has the two public methods draw and area. The draw method should return coordinates or any graphical representation. The area method returns the area
of the current rectangle instance. The rectangle class thereby has two responsibilities. It's responsible for calculating the area and responsible to draw itself.
def __init__(self, height: float, width: float): def draw(self) -> VisualRepresentation: return visual_representation(self) return self.height*self.width
If you are confused by the types behind the arguments in the constructor, you should have a look at Python type hints.
However,
changes in the rectangle class would affect the GUI as well as the geometric calculation app. Changes in the rectangle class could potentially break the whole system or lead to
unpredictable bugs.
The solution Martin proposes in this specific case is to split the class into two separate ones:
This separation leads to a single responsibility for each the geometric rectangle and the rectangle class. Changes in the draw method now can no longer affect the way they are
calculated.
class GeometricRectangle(object): def __init__(self, height: float, width: float):class Rectangle(GeometricRectangle): def draw(self) -> VisualRepresentation: return visual_representation(self)
The attentive readers might have noticed that this concept's strict and mindless usage will also lead to poor software design
Применение принципов SOLID в Python — Python
Вкратце, что такое принципы SOLID?
Принципы кодирования SOLID — это аббревиатура, созданная Робертом К. Мартином и относящаяся к 5 различным конвенциям кодирования.
Он говорит о том, что, следуя этим принципам, можно повысить надежность, структуру и логическую последовательность вашего кода.
Принципы таковы
- Принцип единой ответственности (ПЕ)
- Принцип открытости-закрытости (OCP)
- Принцип замещения Лискова (ПЗЛ)
- Принцип разделения интерфейсов (ISP)
- Принцип инверсии зависимостей (DIP)
Это список лучших практик, разработанных в течение многих лет и сгруппированных в акронимы, а также другие термины, такие как: DRY не повторяй, или KISS сохраняй его маленьким и простым.
Принцип единой ответственности
- «У класса должна быть только одна причина для изменений».
- это означает, что каждая функция в коде должна иметь только одну и только одну единственную ответственность, короче говоря, ваша функция должна делать только одну вещь, если в функции делается более двух вещей, ее необходимо разделить.
- Пример без единой ответственности:
def get_num_and_bigger(list_of_items):
list_of_numbers = []
# create list of only numbers
for item in list_of_items:
if isinstance(item, int):
list_of_numbers.
append(item)
print(list_of_numbers)
# find bigger number
bigger_number = max(list_of_numbers)
print(bigger_number)
get_num_and_bigger([1, 2, «pepe», 9, 10, 6, 7, 8])
Пример с единой ответственностью:
def get_only_numbers(list_of_items):
list_of_numbers = []
for item in list_of_items:
if isinstance(item, int):
list_of_numbers.append(item)
return list_of_numbers
def get_bigger_number(list_of_numbers):
bigger_number = max(list_of_numbers)
return bigger_number
def main(list_of_items):
# get list of only numbers
list_of_numbers = get_only_numbers(list_of_items)
# get bigger number
bigger_number = get_bigger_number(list_of_numbers)
print(bigger_number)
Принцип «открыто-закрыто
- «Программные сущности… должны быть открыты для расширения, но закрыты для модификации».
- Вкратце это означает, что то, что вы уже разработали, не нужно переписывать или, как мы знаем, лучше рефакторить, вы просто добавляете то, что вам нужно сейчас, новое расширение.
- Но это не означает, что вы не должны изменять существующий код, когда это необходимо, это не единственная абсолютная истина, и как все в мире разработки имеет «это зависит».
- У нас есть следующий пример, вас просят сравнить два числа и получить большее, для этого мы создаем функцию для сравнения, но что происходит, требования меняются, теперь нас просят вычислить большее из трех чисел.
- Пример сравнения двух чисел
def compare_two_numbers(a, b): #2 3 1
if a > b:
return a
return b
Пример рефакторинга без применения принципа «открыто-закрыто
def compare_two_or_three_numbers(a, b, c=None):
bigger = b
if a > b:
bigger = a
if c is not None and c > bigger:
bigger = c
return bigger
Пример использования этого принципа
def compare_three_numbers(a, b, c):
more_bigger = compare_two_numbers(compare_two_numbers(a, b), c)
*Принцип замещения Лискова
Этот принцип является одним из самых сложных для понимания, он говорит нам, что в какой-то момент в нашем коде мы создаем класс и создаем дочерние классы, что дочерние классы должны иметь возможность заменить родительский и код должен продолжать работать таким же образом.
Среди всех принципов SOLID этот является самым заумным для понимания и объяснения. Для этого принципа не существует стандартного «шаблонного» решения, где его следует применять, и трудно предоставить «стандартный пример» для демонстрации.
*Принцип разделения интерфейсов
- «Много интерфейсов, ориентированных на конкретного заказчика, лучше, чем один интерфейс общего назначения».
- Этот принцип говорит нам, что класс должен иметь необходимый интерфейс и избегать методов, которые не работают или не нужны, чтобы быть частью этого класса, и это существует потому, что часто методы, которые не нужны, наследуются от классов.
- Пример метода без лисковской подстановки, у нас есть проблема, что пингвин — это Ave, который наследуется от класса, но в данном случае он не может летать.
class Bird:
def fly(self):
return 'I can fly!'
def walk(self):
return 'I can walk!'
class Penguin(Bird):
def fly(self):
raise NotImplementedError('Cannot fly')
Пример с методом подстановки Лискова, теперь мы создаем новый класс, который наследуется от Bird, который является Bird, который может летать.
# method liskov substitution principle
class BirdNew:
def walk(self):
return 'I can walk!'
class BirdCanFly(BirdNew):
def fly(self):
return 'I can fly!'
class PenguinNew(BirdNew):
def walk(self):
return super().walk()
class Duck(BirdCanFly):
def fly(self):
return super().fly()
*Принцип обратного действия зависимости
«Абстракции не должны зависеть от деталей. Детализация должна зависеть от абстракции. Модули высокого уровня не должны зависеть от модулей низкого уровня. И то, и другое должно зависеть от абстракций».
Другими словами, модули высокого уровня не должны зависеть от модулей низкого уровня, оба должны зависеть от абстракций.
class Engine(object):
def __init__(self):
pass
def accelerate(self):
pass
def getRPM(self):
currentRPM = 0
#…
return currentRPM
class Vehicle(object):
def __init__(self):
self._engine = Engine()
def getEngineRPM(self):
return self._engine.getRPM()
Приведенный выше код иллюстрирует «обычный» способ определения взаимодействия между классами. Как мы видим, существует класс Vehicle, который содержит объект класса Engine.
Класс Vehicle получает обороты двигателя, вызывая метод getEngineRPM объекта Engine и возвращая его результат.
Этот случай соответствует зависимости, верхний модуль Vehicle зависит от нижнего модуля Engine, что порождает чрезвычайно связанный и трудно тестируемый код.
Чтобы отделить зависимость Engine от Vehicle, мы должны сделать так, чтобы класс Vehicle перестал отвечать за инстанцирование объекта Engine, инжектируя его в качестве параметра конструктора, избегая тем самым того, чтобы ответственность ложилась на сам класс. Таким образом, мы разделим оба объекта, оставив класс в таком виде:
class Vehicle(object):
def __init__(self, engine):
self._engine = engine
def getEngineRPM(self):
return self._engine.getRPM()
if __name__ == '__main__':
vehicle = Vehicle(Engine())
print(vehicle.getEngineRPM())