Баннер мобильный (1) Пройти тест

Принципы SOLID в программировании — что это такое

Какие правила применяют в разработке, чтобы код был понятным и масштабируемым

Разбор

23 апреля 2024

Поделиться

Скопировано
Принципы SOLID в программировании — что это такое

Содержание

    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.

    Полезные ссылки

    1. Что ждет программистов в 2024: новые языки, перспективы и зарплаты
    2. Математика для программистов: какая нужна на самом деле
    3. Алгоритмы сортировки в программировании: виды, описания и сравнения

    Разбор

    Поделиться

    Скопировано
    0 комментариев
    Комментарии