Принципы SOLID на примерах Python

Принципы SOLID на примерах Python-кода, с подробным объяснением преимуществ и возможных недостатков каждого принципа.

19К открытий31К показов

Вероятно, вы не раз слышали о так называемых SOLID принципах. Но что на самом деле означает каждый из принципов SOLID и как правильно применять их на практике? Вы найдёте ответы в данной статье.

Что такое SOLID принципы?

Современная разработка требует передовых навыков и глубоких знаний сложных принципов программирования. Одной из наиболее важных структур, используемых сегодня в разработке программного обеспечения, являются принципы SOLID — они же пять основных принципов объектно-ориентированного программирования.

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

Расшифровка SOLID

Аббревиатура SOLID включает в себя название пяти принципов «хорошего дизайна», когда речь идет о разработке программного обеспечения. Акроним SOLID означает следующие принципы:

  1. Принцип единственной ответственности (Single Responsibility Principle – SRP): Каждый класс должен иметь только одну причину для изменения. Это означает, что класс должен быть ответственным только за одну конкретную функцию или задачу. Этот принцип помогает сделать классы более связанными, легко понятными и поддерживаемыми.
  2. Принцип открытости/закрытости (Open/Closed Principle – OCP): Программные сущности, такие как классы, модули и функции, должны быть открыты для расширения, но закрыты для модификации. Вместо изменения существующего кода, следует добавлять новый код для внесения изменений. Это позволяет создавать более стабильные и гибкие системы.
  3. Принцип подстановки Лисков (Liskov Substitution Principle – LSP): Объекты в программе должны быть заменяемыми экземплярами их базовых типов, не нарушая корректность программы. Это означает, что код, который работает с базовым типом, должен работать и с любым его подтипом, не вызывая ошибок или неожиданного поведения. Этот принцип обеспечивает согласованность в использовании наследования и полиморфизма.
  4. Принцип разделения интерфейса (Interface Segregation Principle – ISP): Клиенты не должны зависеть от интерфейсов, которые они не используют. Вместо создания общих интерфейсов следует создавать специфические интерфейсы, предназначенные для конкретных клиентов. Это позволяет избежать излишней связности между компонентами системы и улучшить модульность.
  5. Принцип инверсии зависимостей (Dependency Inversion Principle – DIP): Классы должны зависеть от абстракций, а не от конкретных реализаций. Высокоуровневые модули не должны зависеть от низкоуровневых модулей. Оба типа модулей должны зависеть от абстракций. Этот принцип помогает уменьшить связанность между компонентами системы и повысить их переиспользуемость.

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

Акроним SOLID принципов был сформулирован Робертом С. Мартином, также известным как «Дядя Боб», одним из самых влиятельных разработчиков программного обеспечения в области разработки программного обеспечения.

Хотя многим эти принципы могут показаться сложными, мы постараемся объяснить их простым и понятным способом.

Принцип единой ответственности

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

Предположим, у нас есть файл с именем «автомобиль» с кодом, отвечающим за эксплуатацию, управление, контроль и мониторинг автомобиля. В соответствии с принципом единой ответственности каждая из этих задач должна быть разделена на отдельный файл или компонент. Также стоит отметить, что SRP применяется не только к классам, но и к функциям и методам.

SOLID примеры на Python — принцип единой ответственности

Концепция принципа единой ответственности (SRP) заключается в том, что класс должен иметь только одну причину для изменения. Давайте рассмотрим пример с классом User, который нарушает этот принцип:

			class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email

    def save(self):
        # Логика сохранения пользователя в базе данных
        pass

    def send_email(self, message):
        # Логика отправки электронной почты пользователю
        pass

    def generate_report(self):
        # Логика генерации отчета пользователя
        pass
		

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

Давайте применим принцип единой ответственности, разделив класс User на несколько отдельных классов с единой ответственностью:

			class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email

    def save(self):
        # Логика сохранения пользователя в базе данных
        pass


class EmailSender:
    def send_email(self, user, message):
        # Логика отправки электронной почты пользователю
        pass


class ReportGenerator:
    def generate_report(self, user):
        # Логика генерации отчета пользователя
        pass
		

Теперь каждый класс имеет только одну ответственность. Класс User отвечает только за данные пользователя и сохранение их в базе данных. Класс EmailSender занимается отправкой электронной почты, а класс ReportGenerator отвечает за генерацию отчета пользователя.

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

Визуальное представление SRP

Визуализация SRP, как одного из SOLID принципов, может быть представлена в виде диаграммы классов, где каждый класс имеет только одну ответственность. Каждый класс должен быть ответственным только за выполнение одной конкретной функции или задачи. Если класс имеет несколько ответственностей, это может указывать на нарушение SRP:

			-----------------------
|   Класс 1          |
|---------------------|
| - Метод 1           |
| - Метод 2           |
| - Метод 3           |
-----------------------
           |
           |
-----------------------
|   Класс 2          |
|---------------------|
| - Метод 4           |
| - Метод 5           |
-----------------------
           |
           |
-----------------------
|   Класс 3          |
|---------------------|
| - Метод 6           |
| - Метод 7           |
-----------------------
		

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

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

Резюмируя

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

Принцип открытого-закрытого

Теперь рассмотрим принцип открытого-закрытого. Он предполагает, что программные компоненты должны быть открыты для расширения и закрыты для любых модификаций. Другими словами, когда создаются новые функции, код следует расширять, не затрагивая его существующей структуры. Для этого разработчики могут использовать комбинацию наследования и полиморфизма. Важным преимуществом соблюдения принципа «открыто-закрыто» является снижение риска непредвиденных ошибок при внесении изменений.

SOLID примеры на Python — принцип открытого-закрытого

Принцип открытого-закрытого (Open/Closed Principle – OCP) заключается в том, что программные сущности, такие как классы, модули и функции, должны быть открыты для расширения, но закрыты для модификации. Давайте рассмотрим пример с классами Shape и AreaCalculator, где будем применять принцип OCP:

			from abc import ABC, abstractmethod
from math import pi


class Shape(ABC):
    @abstractmethod
    def calculate_area(self):
        pass


class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def calculate_area(self):
        return self.width * self.height


class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def calculate_area(self):
        return pi * self.radius ** 2


class AreaCalculator:
    def calculate_total_area(self, shapes):
        total_area = 0
        for shape in shapes:
            total_area += shape.calculate_area()
        return total_area
		

В этом примере у нас есть абстрактный класс Shape, который определяет метод calculate_area(). Затем у нас есть два класса-наследника Rectangle и Circle, которые реализуют этот метод для расчета площади прямоугольника и круга соответственно.

Класс AreaCalculator отвечает за вычисление общей площади для набора фигур. Он использует SOLID принцип OCP, поскольку он открыт для расширения новыми типами фигур, но закрыт для модификации своей основной логики. Если мы хотим добавить новый тип фигуры, например, треугольник, мы можем создать новый класс Triangle, реализующий метод calculate_area(), и передать его в AreaCalculator.calculate_total_area() без изменения самого AreaCalculator:

			class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def calculate_area(self):
        return 0.5 * self.base * self.height


# Использование
shapes = [Rectangle(4, 5), Circle(3), Triangle(6, 2)]
calculator = AreaCalculator()
total_area = calculator.calculate_total_area(shapes)
print(total_area)
		

Таким образом, принцип открытого-закрытого позволяет нам добавлять новые типы фигур, расширяя функциональность, без изменения существующего кода в AreaCalculator. Это делает код более гибким и устойчивым к изменениям.

Резюмируя

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

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

Принцип подстановки Лисков — еще одна важная концепция. Он предполагает, что объекты подклассов должны быть полностью взаимозаменяемы с объектами своих родительских классов, т. е. любой дочерний класс должен вести себя точно так же, как родительский класс. Это также известно как «полиморфизм подтипов» и помогает разработчикам создавать более управляемый и надежный код.

SOLID примеры на Python — принцип подстановки Барбары Лисков

Принцип подстановки Барбары Лисков (Liskov Substitution Principle – LSP) гласит, что объекты должны быть заменяемыми экземплярами их базовых типов без нарушения корректности программы. Давайте рассмотрим пример с классами Rectangle (Прямоугольник) и Square (Квадрат), чтобы проиллюстрировать принцип LSP:

			class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def set_width(self, width):
        self.width = width

    def set_height(self, height):
        self.height = height

    def get_area(self):
        return self.width * self.height


class Square(Rectangle):
    def set_width(self, width):
        self.width = width
        self.height = width

    def set_height(self, height):
        self.width = height
        self.height = height
		

В этом примере класс Rectangle представляет прямоугольник с методами для установки ширины и высоты, а также для получения площади.

Класс Square наследуется от Rectangle и переопределяет методы set_width() и set_height(). В случае квадрата ширина и высота всегда должны быть одинаковыми, поэтому при установке одного измерения класс Square автоматически устанавливает и другое измерение равным ему.

Однако данный пример нарушает SOLID принцип LSP. Рассмотрим следующий код:

			def print_area(rectangle):
    rectangle.set_width(4)
    rectangle.set_height(5)
    area = rectangle.get_area()
    print(f"Area: {area}")


rectangle = Rectangle(4, 5)
square = Square(4)

print_area(rectangle)  # Результат: Area: 20
print_area(square)  # Результат: Area: 16 (ожидалось 20)
		

В этом примере мы вызываем функцию print_area(), которая ожидает объект типа Rectangle. Когда мы передаем rectangle, результат площади правильный (20), потому что ширина и высота были установлены независимо. Однако, когда мы передаем square, ожидаемая площадь должна быть также 20, но фактически получаем 16. Это происходит из-за изменения поведения методов set_width() и set_height() в классе Square.

Таким образом, класс Square не является полностью заменяемым объектом для класса Rectangle, нарушая принцип LSP. Чтобы исправить это, мы можем пересмотреть дизайн иерархии классов, чтобы избежать нарушения принципа LSP, или использовать интерфейсы и абстракции для достижения корректного поведения при замене объектов.

Резюмируя

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

Принцип разделения интерфейса

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

SOLID примеры на Python — принцип разделения интерфейса

Принцип разделения интерфейса (Interface Segregation Principle – ISP) гласит, что клиенты не должны зависеть от интерфейсов, которые они не используют. Вместо создания общих интерфейсов следует создавать специфические интерфейсы для конкретных клиентов. Давайте рассмотрим пример с интерфейсами для различных устройств вывода ввода:

			from abc import ABC, abstractmethod


class InputDevice(ABC):
    @abstractmethod
    def read_input(self):
        pass


class OutputDevice(ABC):
    @abstractmethod
    def write_output(self, data):
        pass


class Keyboard(InputDevice):
    def read_input(self):
        # Логика чтения ввода с клавиатуры
        pass


class Mouse(InputDevice):
    def read_input(self):
        # Логика чтения ввода с мыши
        pass


class Monitor(OutputDevice):
    def write_output(self, data):
        # Логика вывода данных на монитор
        pass


class Printer(OutputDevice):
    def write_output(self, data):
        # Логика вывода данных на принтер
        pass
		

В этом примере у нас есть абстрактные классы InputDevice и OutputDevice, представляющие интерфейсы для устройств ввода и вывода соответственно. Затем мы определяем конкретные классы Keyboard, Mouse, Monitor и Printer, которые реализуют соответствующие методы.

Применяя SOLID  принцип ISP, мы разделяем интерфейсы на более специфические, чтобы клиенты могли зависеть только от интерфейсов, которые они используют. Например, если клиенту нужен только ввод с клавиатуры, он может зависеть только от интерфейса InputDevice и использовать класс Keyboard:

			def process_input(device):
    data = device.read_input()
    # Логика обработки ввода


keyboard = Keyboard()
process_input(keyboard)
		

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

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

Резюмируя

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

Принцип инверсии зависимостей

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

SOLID примеры на Python — принцип инверсии зависимостей

Принцип инверсии зависимостей (Dependency Inversion Principle – DIP) гласит, что классы должны зависеть от абстракций, а не от конкретных реализаций. Высокоуровневые модули не должны зависеть от низкоуровневых модулей. Оба типа модулей должны зависеть от абстракций. Давайте рассмотрим пример с классами Notification и EmailSender, чтобы проиллюстрировать принцип DIP:

			from abc import ABC, abstractmethod


class Notification(ABC):
    @abstractmethod
    def send_notification(self, message):
        pass


class EmailSender(Notification):
    def send_notification(self, message):
        # Логика отправки уведомления по электронной почте
        pass


class SMSNotification(Notification):
    def send_notification(self, message):
        # Логика отправки уведомления по SMS
        pass


class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email
        self.notification_service = EmailSender()

    def send_notification(self, message):
        self.notification_service.send_notification(message)
		

В этом примере класс User зависит от конкретной реализации EmailSender в качестве сервиса уведомлений. Это создает прямую связь между User и EmailSender, что делает классы сложнее для тестирования и внесения изменений.

Чтобы применить SOLID принцип DIP, мы изменяем User, чтобы он зависел от абстракции Notification, а не от конкретной реализации:

			class User:
    def __init__(self, username, email, notification_service):
        self.username = username
        self.email = email
        self.notification_service = notification_service

    def send_notification(self, message):
        self.notification_service.send_notification(message)
		

Теперь User принимает объект notification_service, реализующий интерфейс Notification, через конструктор. Это позволяет передавать различные реализации уведомлений, такие как EmailSender или SMSNotification, без изменения самого User:

			email_sender = EmailSender()
user = User("John", "john@example.com", email_sender)
user.send_notification("Hello!")

sms_notification = SMSNotification()
user = User("Jane", "jane@example.com", sms_notification)
user.send_notification("Hi there!")
		

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

Резюмируя

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

Заключение

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

Принципы SOLID на примерах Python 1

Примечание Рейтинг степени важности в разработке (0-10) является относительной оценкой и может различаться в зависимости от конкретной ситуации и контекста разработки.

Надеемся, что принципы SOLID на Python примерах оказались полезны и доступны для понимания.

Следите за новыми постами
Следите за новыми постами по любимым темам
19К открытий31К показов