SOLID – это набор из пяти основных принципов объектно-ориентированного программирования, предложенный Робертом Мартином. Эти принципы помогают создавать максимально понятные, легко поддерживаемые и расширяемые программы.
Что означает аббревиатура SOLID
- S – Single Responsibility Principle – принцип единственной ответственности.
- O – Open-Closed Principle – принцип открытости-закрытости
- L – Liskov Substitution Principle – принцип подстановки Барбары Лисков
- I – Interface Segregation Principle – принцип разделения интерфейсов
- D – Dependency Inversion Principle – принцип инверсии зависимостей
Связь с ООП
SOLID является частью философии ООП, ее продолжением. Эти принципы напрямую используют возможности объектно-ориентированного программирования.
- S – Single Responsibility Principle – классы и методы
- O – Open-Closed Principle – наследование и полиморфизм
- L – Liskov Substitution Principle – наследование и правильная иерархия классов
- I – Interface Segregation Principle – интерфейсы, абстрактные классы, множественное наследование
- D – Dependency Inversion Principle – абстрактные классы и интерфейсы, внедрение зависимостей
Single Responsibility Principle
Принцип единственной ответственности. Класс должен иметь только одну причину для изменения. Класс должен решать только одну задачу.
Это обеспечивает удобство сопровождения и тестирования. А изменения в одной части программы не затрагивают другие.
Пример правильного использования
Здесь каждый класс решает одну конкретную задачу, поэтому его легко менять, поддерживать и тестировать.
class UserManager:
def create_user(self, username):
pass
class EmailSender:
def send_email(self, email, message):
pass
Пример нарушения принципа
Здесь класс объединяет две разные функциональности – управление пользователем и отправка сообщений.
class UserManager:
def create_user(self, username):
pass
def send_email(self, email, message):
pass
Open-Closed Principle
Принцип открытости-закрытости.
Программные сущности (классы, модули) должны быть открыты для расширения и закрыты для изменений.
Это позволяет расширять функционал без изменения существующего кода и снижает риск случайного внесения ошибок при изменении уже существующего кода.
Пример правильного использования
В данном случае для нового типа клиентов можно создать новый класс, не меняя старый.
class DiscountCalculator:
def calculate(self):
return 0.1
class VipDiscountCalculator(DiscountCalculator):
def calculate(self):
return 0.2
Пример нарушения принципа
Здесь при добавлении нового типа клиентов придется менять метод.
class DiscountCalculator:
def calculate(self, customer_type):
if customer_type == "regular":
return 0.1
elif customer_type == "vip":
return 0.2
Liskov Substitution Principle
Принцип подстановки Барбары Лисков. Этот принцип довольно труден для восприятия. Его удобно разобрать на нескольких примерах (см. ниже). Принцип гласит:
Объекты дочерних классов должны корректно заменять объекты родительского класса без изменения корректности работы программы.
Это обеспечивает правильное переопределение методов и наследование классов. Устраняет неожиданные ошибки при замене объектов одного класса другим.
Пример кода для задачи управления разными видами автомобилей.
class Vehicle:
"""Это родительский класс, задающий общие методы для дочерних классов"""
def start_engine(self):
print("Двигатель запущен")
def stop_engine(self):
print("Двигатель остановлен")
def drive(self):
raise NotImplementedError("Этот метод должен быть реализован в дочернем классе")
class Car(Vehicle):
"""Дочерний класс, переопределяет метод drive под легковой автомобиль"""
def drive(self):
print("Легковой автомобиль едет быстро и плавно")
class Truck(Vehicle):
"""Дочерний класс, переопределяет метод drive под грузовой автомобиль"""
def drive(self):
print("Грузовик едет медленно, но перевозит большой груз")
class Bus(Vehicle):
"""Дочерний класс, переопределяет метод drive под автобус"""
def drive(self):
print("Автобус едет по маршруту и останавливается на остановках")
# Запустим все доступные методы для дочерних классов, включая методы из родительского класса
vehicles = [Car(), Truck(), Bus()]
for vehicle in vehicles:
print(vehicle.__class__.__name__)
vehicle.start_engine()
vehicle.drive()
vehicle.stop_engine()
print()
# Output
# Car
# Двигатель запущен
# Легковой автомобиль едет быстро и плавно
# Двигатель остановлен
#
# Truck
# Двигатель запущен
# Грузовик едет медленно, но перевозит большой груз
# Двигатель остановлен
#
# Bus
# Двигатель запущен
# Автобус едет по маршруту и останавливается на остановках
# Двигатель остановлен
Зачем вообще нужен родительский класс, если его заменяет дочерний?
Ответ: родительский класс задает общий интерфейс и общее поведение для всех дочерних классов. Если такого интерфейса не требуется или у классов нет общих элементов поведения, то им не нужен родительский класс.
Interface Segregation Principle
Принцип разделения интерфейсов. Клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше иметь несколько узких интерфейсов (классов), чем один широкий.
Этот принцип убирает ненужные зависимости, снижает сложность и увеличивает гибкость кода.
Пример правильной реализации
class Printer:
def print_document(self):
pass
class Scanner:
def scan_document(self):
pass
class MultiFunctionPrinter(Printer, Scanner):
pass
Пример неправильной реализации
Если у нас будет принтер, который не может сканировать, его объекту придется реализовывать ненужный метод scan_document.
class Printer:
def print_document(self):
pass
def scan_document(self):
pass
Dependency Inversion Principle
Принцип инверсии зависимостей. Высокоуровневые модули не должны зависеть от низкоуровневых. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Принцип упрощает замену компонентов, делает код гибким и расширяемым.
Пример правильной реализации принципа
# Этот родительский класс не зависит от реализации дочерних классов
class Database:
def save(self, data):
pass
class MySQLDatabase(Database):
def save(self, data):
print("Сохраняем в MySQL")
class PostgreSQLDatabase(Database):
def save(self, data):
print("Сохраняем в PostgresSQL")
# Этот класс зависит от абстракции Database
# В нем легко менять одну базу данных на другую
# Добавление новой базы данных (нового класса) не потребует изменений в UserManager
class UserManager:
def __init__(self, db: Database):
self.db = db
mysql = MySQLDatabase()
pgsql = PostgreSQLDatabase()
um = UserManager(mysql)
print(um.db.__class__.__name__)
Пример неправильной реализации
class MySQLDatabase:
def save(self, data):
print("Сохраняем в MySQL")
# Здесь UserManager жестко зависит от конкретной реализации базы данных
class UserManager:
def __init__(self):
self.db = MySQLDatabase()