SOLID — это аббревиатура от названий принципов объектно-ориентированного программирования (ООП), которые помогают разработчикам писать поддерживаемый и масштабируемый код.
SOLID-принципы разработал Роберт Мартин (также известный как Дядя Боб), американский инженер, программист и автор известной книги «Чистый код». В начале 2000-х годов он сформулировал пять принципов ООП, благодаря которым программисты могут упрощать свой код, избегая сложных и запутанных конструкций.
Как расшифровывается S.O.L.I.D.?
S — Single Responsibility Principle — Принцип единственной ответственности.
O — Open/Closed Principle — Принцип открытости/закрытости.
L — Liskov Substitution Principle — Принцип подстановки Барбары Лисков.
I — Interface Segregation Principle — Принцип разделения интерфейса.
D — Dependency Inversion Principle — Принцип инверсии зависимостей.
Теперь более подробно рассмотрим работу принципов SOLID на примере двух популярных языков — Java и Python.
Принцип единственной ответственности (SRP)
A class should have one, and only one, reason to change. — Один класс решает одну задачу.
Это значит, что каждый класс должен выполнять только одну четко определенную функцию. Если он решает более одной задачи, это может привести к сложностям в поддержке и расширении кода.
Как следовать принципу SRP
Сначала общую задачу нужно декомпозировать на несколько подзадач. Затем каждую подзадачу реализовать в отдельном классе.
Задача
Нужно рассчитать зарплату сотрудников и сформировать отчет.
Решение
Java | |
С нарушением SRP | С учетом SRP |
class Employee { private String name; private double salary; public void calculateSalary() { // Расчет зарплаты } public void generateReport() { // Генерация отчета } } | class Employee { private String name; private double salary; public double calculateSalary() { // Расчет зарплаты } } class ReportGenerator { public void generateReport(Employee employee) { // Генерация отчета } } |
Python | |
class Employee: def __init__(self, name, salary): self.name = name self.salary = salary def calculate_salary(self): # Расчет зарплаты pass def generate_report(self): # Генерация отчета pass | class Employee: def __init__(self, name, salary): self.name = name self.salary = salary def calculate_salary(self): # Расчет зарплаты pass class ReportGenerator: def generate_report(self, employee): # Генерация отчета Pass |
Класс Employee отвечает за две задачи — расчет зарплаты и генерацию отчета. | Класс Employee отвечает за расчет зарплаты сотрудника, а класс ReportGenerator — за генерацию отчета о зарплате для конкретного сотрудника. |
Принцип открытости/закрытости (OCP)
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. — Программные сущности (классы, модули, функции и т.д.) должны быть открыты для расширения, но закрыты для модификации.
Это значит, что программные компоненты нужно спроектировать таким образом, чтобы их можно было расширять новым функционалом, не меняя исходный код. Изменения при добавлении нового кода могут сломать уже работающую логику.
Как следовать принципу OCP
В этом помогут абстракции, интерфейсы и наследование. Это позволит добавить новый функционал через расширение, а не модификацию существующего кода.
Задача
У ресторана есть система управления заказами. Нужно сделать так, чтобы она поддерживала различные виды платежей: наличные, кредитные карты и мобильные платежи.
Решение
Java | |
С нарушением OCP | С учетом OCP |
class Order { public void processPayment(String paymentType) { if (paymentType.equals(«cash»)) { // Обработка наличных } else if (paymentType.equals(«credit_card»)) { // Обработка кредитной карты } else if (paymentType.equals(«mobile_payment»)) { // Обработка мобильного платежа } } } | interface PaymentProcessor { void processPayment(); } class CashPaymentProcessor implements PaymentProcessor { public void processPayment() { // Обработка наличных } } class CreditCardPaymentProcessor implements PaymentProcessor { public void processPayment() { // Обработка кредитной карты } } class MobilePaymentProcessor implements PaymentProcessor { public void processPayment() { // Обработка мобильного платежа } } |
Python | |
С нарушением OCP | С учетом OCP |
class Order: def process_payment(self, payment_type): if payment_type == «cash»: # Обработка наличных pass elif payment_type == «credit_card»: # Обработка кредитной карты pass elif payment_type == «mobile_payment»: # Обработка мобильного платежа pass | class PaymentProcessor: def process_payment(self): pass class CashPaymentProcessor(PaymentProcessor): def process_payment(self): # Обработка наличных pass class CreditCardPaymentProcessor(PaymentProcessor): def process_payment(self): # Обработка кредитной карты pass class MobilePaymentProcessor(PaymentProcessor): def process_payment(self): # Обработка мобильного платежа pass |
Логика по разным способам оплаты находится в одном классе Order. | PaymentProcessor является интерфейсом. Его реализуют классы для каждого способа оплаты: CashPaymentProcessor, CreditCardPaymentProcessor и MobilePaymentProcessor. |
Принцип подстановки Барбары Лисков (LSP)
Derived classes must be substitutable for their base classes. — Производные классы должны заменять свои базовые классы.
Это значит, что объекты базовых классов должны быть заменяемы объектами производных классов без изменения ожидаемого поведения программы.
Базовый класс — это класс, от которого производные классы наследуют свойства и методы.
Как следовать принципу LSP
Производный класс должен сохранять все свойства базового класса и не изменять их семантику. Нужно добиться того, чтобы объекты производных классов могли безопасно заменять друг друга и базовый класс.
Задача
Предположим, что есть система для работы с геометрическими фигурами, в которой есть базовый класс Shape и производные классы Circle и Square.
Решение
Java | |
С нарушением LSP | С учетом LSP |
class Shape { public double area() { return 0; } } class Circle extends Shape { private double radius; @Override public double area() { return Math.PI * radius * radius; } } class Square extends Shape { private double side; @Override public double area() { return side * side; } } | abstract class Shape { public abstract double area(); } class Circle extends Shape { private double radius; @Override public double area() { return Math.PI * radius * radius; } } class Square extends Shape { private double side; @Override public double area() { return side * side; } } |
Python | |
С нарушением LSP | С учетом LSP |
class Shape: def area(self): return 0 class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): return 3.14 * (self.radius ** 2) class Square(Shape): def __init__(self, side): self.side = side def area(self): return self.side ** 2 | from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): pass class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): return 3.14 * (self.radius ** 2) class Square(Shape): def __init__(self, side): self.side = side def area(self): return self.side ** 2 |
Нарушение принципа LSP происходит в классе Shape. В нем реализован метод area(), который возвращает 0. Такая реализация нарушает ожидаемое поведение, поэтому объекты Square и Circle не могут быть заменены объектами базового класса Shape без нарушения функциональности. | Метод area() определен как абстрактный в базовом классе Shape, гарантируя, что все производные классы должны его реализовать. Тогда объекты производных от Shape классов можно использовать вместо него в любом месте кода. |
Принцип разделения интерфейса (ISP)
Clients should not be forced to depend upon interfaces that they do not use. — Клиенты не должны зависеть от интерфейсов, которые они не используют.
Это значит, что нужно создавать только небольшие и узконаправленные интерфейсы, не перегруженные ненужными методами.
Как следовать принципу ISP
Каждый интерфейс должен существовать для определенных задач и содержать только те методы, которые эти задачи решают.
Задача
Предположим, у нас есть система управления документами с интерфейсом Document, содержащим методы для работы с документами, такие как open(), save() и close(). Однако одним клиентам требуется только возможность открывать и закрывать документы, а другим — только сохранять и закрывать их.
Решение
Java | |
С нарушением ISP | С учетом ISP |
interface Document { void open(); void save(); void close(); } class TextEditor implements Document { public void open() { // Открыть документ } public void save() { // Сохранить документ } public void close() { // Закрыть документ } } | interface Openable { void open(); } interface Savable { void save(); } interface Closable { void close(); } class TextEditorOne implements Openable, Closable { public void open() { // Открыть документ } public void close() { // Закрыть документ } } class TextEditorTwo implements Savable, Closable { public void save() { // Сохранить документ } public void close() { // Закрыть документ } } |
Python | |
С нарушением ISP | С учетом ISP |
class Document: def open(self): pass def save(self): pass def close(self): pass class TextEditor(Document): def open(self): # Открыть документ pass def save(self): # Сохранить документ pass def close(self): # Закрыть документ pass | class Openable: def open(self): pass class Savable: def save(self): pass class Closable: def close(self): pass class TextEditorOne(Openable, Closable): def open(self): # Открыть документ pass def close(self): # Закрыть документ Pass class TextEditorTwo(Savable, Closable): def save(self): # Сохранить документ pass def close(self): # Закрыть документ Pass |
Методы open(), save() и close() находятся в одном интерфейсе — Document. Класс TextEditor реализует этот интерфейс со всеми его методами. | Для каждой задачи есть отдельный интерфейс: Openable с методом open(), Savable с методом save() и Closable с методом close(), каждый из которых реализован в классе TextEditor. |
Принцип инверсии зависимостей (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions. — Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей, но детали должны зависеть от абстракций.
Это значит, что код должен быть организован таким образом, чтобы зависимости между компонентами программы были основаны на абстракциях, а не на конкретных реализациях. Таким образом, компоненты легко заменить или изменить без воздействия на другие части системы.
Как следовать принципу DIP
— Использовать интерфейсы или абстрактные классы для определения зависимостей между компонентами.
— Следовать остальным принципам SOLID, чтобы создавать хорошо структурированные и модульные системы.
— Применять шаблоны проектирования, такие как Dependency Injection (Внедрение зависимостей) или Inversion of Control (Инверсия управления), чтобы передавать зависимости извне вместо того, чтобы создавать их внутри компонентов.
Внедрение зависимостей: объект не создает свои зависимости самостоятельно, они предоставляются ему извне, например через конструктор, методы или свойства.
Инверсия управления: управление частью приложения переносится на внешний фреймворк или контейнер, управляющий жизненным циклом объектов.
Задача
Нужно разработать приложение для работы с базой данных студентов. Каждый студент имеет имя, возраст и список предметов, которые он изучает.
Решение
Java | |
С нарушением DIP | С учетом DIP |
class StudentRepository { public void save(Student student) { // Сохранение в базе данных } public Student findById(int id) { // Поиск в базе данных return null; } } class StudentService { private StudentRepository repository; // Зависимость от конкретной реализации репозитория public StudentService() { this.repository = new StudentRepository(); } public void addStudent(Student student) { // Логика добавления студента repository.save(student); } public Student findStudent(int id) { // Логика поиска студента return repository.findById(id); } } | interface StudentRepository { void save(Student student); Student findById(int id); } class DatabaseStudentRepository implements StudentRepository { public void save(Student student) { // Реализация сохранения в базе данных } public Student findById(int id) { // Реализация поиска в базе данных return null; } } class StudentService { private StudentRepository repository; // Инверсия зависимости через конструктор public StudentService(StudentRepository repository) { this.repository = repository; } public void addStudent(Student student) { // Логика добавления студента repository.save(student); } public Student findStudent(int id) { // Логика поиска студента return repository.findById(id); } } |
Python | |
С нарушением DIP | С учетом DIP |
class StudentRepository: def save(self, student): # Сохранение в базе данных pass def findById(self, id): # Поиск в базе данных return None class StudentService: def __init__(self): # Зависимость от конкретной реализации репозитория self.repository = StudentRepository() def add_student(self, student): # Логика добавления студента self.repository.save(student) def find_student(self, id): # Логика поиска студента return self.repository.findById(id) | class StudentRepository: def save(self, student): # Реализация сохранения в базе данных pass def findById(self, id): # Реализация поиска в базе данных return None class StudentService: def __init__(self, repository): # Инверсия зависимости через конструктор self.repository = repository def add_student(self, student): # Логика добавления студента self.repository.save(student) def find_student(self, id): # Логика поиска студента return self.repository.findById(id) |
Зависимость StudentService от StudentRepository определена внутри класса. | Зависимость StudentRepository передается в StudentService через конструктор. Интерфейс StudentRepository позволяет избежать конкретной реализации хранилища студентов (например, базы данных) в классе StudentService. Вместо этого реализацию базы можно сделать в классе DatabaseStudentRepository без изменения кода StudentService. |