СТАТЬИ
Прокачай свои навыки Java – стань востребованным специалистом пройдя стажировку!
Вы хотите вырасти до опытного Java-разработчика и работать в крупной IT-компании?
RoadMap IT School поможет вам в освоении новых технологий Java и Spring на реальном примере в форме практической стажировки. Длительность 3 месяца. Команда из 5 человек. Тимлид уровня Senior

Какие принципы лежат в основе SOLID?

Термин SOLID представляет собой акроним для обозначения набора практик проектирования программного кода и построения гибкой и адаптивной программы.
  • Single Responsibility Principle (SRP) — принцип единственной ответственности.
  • Open–Closed Principle (OCP) — принцип открытости/закрытости.
  • Liskov Substitution Principle (LSP) — принцип подстановки Лисков.
  • Interface Segregation Principle (ISP) — принцип разделения интерфейса.
  • Dependency Inversion Principle (DIP) — принцип инверсии зависимостей.
Принципы SOLID представляют собой набор лучших практик для разработки программного обеспечения, сформулированных Робертом С. Мартином (известным как “Дядя Боб”). Они помогают создавать гибкий, легко поддерживаемый и расширяемый код, улучшая модульность и переиспользуемость системы.

Вот описание каждого из них:
  • Принцип единственной ответственности (Single Responsibility Principle - SRP): Каждый класс должен иметь только одну причину для изменения. Это означает, что класс должен быть ответственным только за одну конкретную функцию или задачу.
Если класс выполняет несколько разных задач или отвечает за разные аспекты функциональности, это может привести к сложностям при внесении изменений в код. Изменение в одной части класса может потребовать изменений в других частях, которые не связаны напрямую с первичной задачей. Это усложняет поддержку и тестирование программного кода Java, а также увеличивает риск ошибок.
Для соблюдения SRP рекомендуется разделять большие классы на более мелкие, каждый из которых будет отвечать за свою конкретную задачу. Это делает код более модульным, легким для понимания и поддержки.
Пример класса нарушающего принцип SRP:
public class Employee {
    private String name;
    private String position;
    private double salary;

    public Employee(String name, String position, double salary) {
        this.name = name;
        this.position = position;
        this.salary = salary;
    }

    public double calculatePay() {
        // Логика расчета зарплаты
        return salary * 0.9; // пример расчета с учетом налогов
    }

    public void saveToDatabase() {
        // Логика сохранения данных сотрудника в базу данных
        System.out.println("Saving " + name + " to the database");
    }

    public String generateReport() {
        // Логика генерации отчета о сотруднике
        return "Report for " + name + ": Position - " + position + ", Salary - " + salary;
    }
}
В этом примере класс Employee выполняет три разные задачи:
  • Расчет зарплаты (calculatePay).
  • Сохранение данных в базу данных (saveToDatabase).
  • Генерация отчета (generateReport).
Каждая из этих задач может изменяться по разным причинам, что нарушает принцип SRP. Чтобы исправить это, можно разделить обязанности на отдельные классы. Каждый класс отвечает за свою собственную задачу.
Пример кода на Java не нарушающего принцип SRP:
public class Employee {
    private String name;
    private String position;
    private double salary;

    public Employee(String name, String position, double salary) {
        this.name = name;
        this.position = position;
        this.salary = salary;
    }

    public String getName() {
        return name;
    }

    public String getPosition() {
        return position;
    }

    public double getSalary() {
        return salary;
    }
}

public class Payroll {
    public double calculatePay(Employee employee) {
        return employee.getSalary() * 0.9;
    }
}

public class EmployeeRepository {
    public void saveToDatabase(Employee employee) {
        System.out.println("Saving " + employee.getName() + " to the database");
    }
}

public class ReportGenerator {
    public String generateReport(Employee employee) {
        return "Report for " + employee.getName() + ": Position - " + employee.getPosition() + ", Salary - " + employee.getSalary();
    }
}
  • Принцип открытости/закрытости (Open/Closed Principle - OCP): Программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации. Добавление нового кода для внесения изменений предпочтительнее изменения существующего кода.
Это означает, что после создания и утверждения класса Java или модуля, его исходный код не должен изменяться при добавлении новой функциональности. Вместо этого, новая функциональность должна добавляться через расширение класса или модуля, используя механизмы наследования, композиции или интерфейсов. Такой подход позволяет избежать необходимости вносить изменения в существующий код при добавлении новых возможностей, что делает систему более стабильной и надежной.
Применение принципа OCP достигается через использование таких паттернов проектирования, как “Strategy” или “Factory”, а также через использование интерфейсов и абстрактных классов для определения общих контрактов и структур. Это позволяет легко добавлять новые функциональные возможности, сохраняя при этом существующую структуру и логику кода.

Давайте ниже рассмотрим пример кода на Java, демонстрирующий принцип OCP. Обратите внимание на следующие моменты. Класс Employee является абстрактным и определяет метод calculateSalary(). Классы FullTimeEmployee и ContractEmployee расширяют Employee и реализуют метод calculateSalary() по-своему. Класс SalaryCalculator использует массив объектов Employee для расчета общей зарплаты.
// Базовый класс для расчета зарплаты сотрудников
public abstract class Employee {
    public abstract double calculateSalary();
}
// Класс для штатных сотрудников
public class FullTimeEmployee extends Employee {
    private double baseSalary;

    public FullTimeEmployee(double baseSalary) {
        this.baseSalary = baseSalary;
    }
    @Override
    public double calculateSalary() {
        return baseSalary;
    }
}
// Класс для контрактных сотрудников
public class ContractEmployee extends Employee {
    private double hourlyRate;
    private int hoursWorked;

    public ContractEmployee(double hourlyRate, int hoursWorked) {
        this.hourlyRate = hourlyRate;
        this.hoursWorked = hoursWorked;
    }
    @Override
    public double calculateSalary() {
        return hourlyRate * hoursWorked;
    }
}
// Класс для расчета общей зарплаты всех сотрудников
public class SalaryCalculator {
    public double calculateTotalSalary(Employee[] employees) {
        double totalSalary = 0;
        for (Employee employee : employees) {
            totalSalary += employee.calculateSalary();
        }
        return totalSalary;
    }
}
Принцип подстановки Лисков (Liskov Substitution Principle - LSP) один из самых сложных для понимания в SOLID. Давайте наберемся терпения и начнем разбираться. Этот принцип был предложен Барбарой Лисков(англ. Barbara Liskov). Объекты в программе должны быть заменяемыми экземплярами их базовых типов, не нарушая корректность программы. И теперь более понятным языком: “Производные классы должны быть заменимыми для своих базовых классов без изменения ожидаемого поведения программы”.
Основная идея LSP заключается в том, что если программа корректно работает с базовым классом, то она должна продолжать корректно работать и при замене базового класса на любой из его дочерних классов. Это означает, что дочерние классы должны сохранять все свойства и методы родительского класса, не изменяя их семантику.

Разберем пример нарушения LSP на примере проблемы “квадрат/прямоугольник”.:
public class Rectangle {
    private int width;
    private int height;

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width); // Нарушение принципа LSP
    }
    @Override
    public void setHeight(int height) {
        super.setHeight(height);
        super.setWidth(height); // Нарушение принципа LSP
    }
}
В этом примере класс Square наследует Rectangle, но переопределяет методы setWidth и setHeight таким образом, что изменение одной стороны квадрата автоматически изменяет другую сторону. Это нарушает принцип подстановки Лисков(LSP), так как Square не может быть использован вместо Rectangle без изменения поведения программы.
Чтобы исправить это и следовать принципу LSP, можно использовать композицию вместо наследования. Теперь Square использует Rectangle через композицию, и изменение одной стороны квадрата не нарушает принцип LSP.
public class Rectangle {
    private int width;
    private int height;

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

public class Square {
    private Rectangle rectangle;

    public Square(int side) {
        this.rectangle = new Rectangle();
        this.rectangle.setWidth(side);
        this.rectangle.setHeight(side);
    }

    public int getSide() {
        return rectangle.getWidth();
    }

    public void setSide(int side) {
        rectangle.setWidth(side);
        rectangle.setHeight(side);
    }

    public int getArea() {
        return rectangle.getArea();
    }
}
  • Принцип разделения интерфейса (Interface Segregation Principle - ISP): Клиенты не должны зависеть от интерфейсов, которые они не используют. Вместо создания общих интерфейсов следует создавать специфические интерфейсы, предназначенные для конкретных клиентов.
Основная идея ISP заключается в том, что большие и общие интерфейсы следует разделять на более мелкие и специализированные, чтобы каждый клиентский класс зависел только от тех методов интерфейса, которые ему действительно нужны. Это позволяет избежать ситуации, когда клиентские классы вынуждены реализовывать методы, которые им не нужны, что приводит к ненужному увеличению сложности кода и затрудняет его поддержку.

Разберем пример кода на Java нарушающего принцип ISP:
public interface Worker {
    void work();
    void eat();
}

public class HumanWorker implements Worker {
    @Override
    public void work() {
        System.out.println("Human working");
    }
    @Override
    public void eat() {
        System.out.println("Human eating");
    }
}

public class RobotWorker implements Worker {
    @Override
    public void work() {
        System.out.println("Robot working");
    }
    @Override
    public void eat() {
        // Нарушение принципа ISP: Робот не ест, но вынужден реализовывать этот метод
        throw new UnsupportedOperationException("Robots do not eat");
    }
}

В этом примере интерфейс Worker содержит методы work и eat. Класс RobotWorker вынужден реализовывать метод eat, который ему не нужен, что нарушает принцип ISP. Чтобы исправить это и следовать принципу ISP, необходимо разделить интерфейс Worker на более мелкие, специфичные интерфейсы. В коде ниже класс RobotWorker реализует только нужный ему интерфейс Workable, и не вынужден реализовывать не нужный ему метод eat.

public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public class HumanWorker implements Workable, Eatable {
    @Override
    public void work() {
        System.out.println("Human working");
    }

   @Override
    public void eat() {
        System.out.println("Human eating");
    }
}

public class RobotWorker implements Workable {
    @Override
    public void work() {
        System.out.println("Robot working");
    }
}
  • Принцип инверсии зависимостей (Dependency Inversion Principle - DIP). Второй по сложности для понимания принцип SOLID. Сейчас разберемся. Итак, что гласит этот принцип? Классы должны зависеть от абстракций, а не от конкретных реализаций. Высокоуровневые модули не должны зависеть от низкоуровневых модулей. Оба типа модулей должны зависеть от абстракций.
Основная идея DIP заключается в том, что зависимости между программными компонентами должны быть инвертированы таким образом, чтобы высокоуровневые компоненты зависели от абстракций, а не от конкретных реализаций. Это позволяет снизить связанность компонентов и упростить их замену и тестирование.

Пример класса на Java, нарушающего принцип DIP:

public class PasswordReminder {
    private MySQLConnection dbConnection;

    public PasswordReminder() {
        this.dbConnection = new MySQLConnection();
    }

    // Другие методы, использующие dbConnection
}
В этом примере класс PasswordReminder напрямую зависит от конкретной реализации MySQLConnection. Если позже потребуется изменить базу данных, придется изменять и класс PasswordReminder, что нарушает принцип DIP.

Чтобы исправить это, нужно использовать интерфейс для абстрагирования зависимости. Класс PasswordReminder зависит от абстракции DatabaseConnection, а не от конкретной реализации MySQLConnection. Это позволяет легко менять реализацию базы данных без изменения самого класса PasswordReminder

public interface DatabaseConnection {
    void connect();
    // Другие методы для работы с базой данных
}

public class MySQLConnection implements DatabaseConnection {
    @Override
    public void connect() {
        // Реализация подключения к MySQL
    }
}

public class PasswordReminder {
    private DatabaseConnection dbConnection;

    public PasswordReminder(DatabaseConnection dbConnection) {
        this.dbConnection = dbConnection;
    }

    // Другие методы, использующие dbConnection
}
В заключении:
SOLID принципы это основа надёжной архитектуры Java-приложений. SOLID формируют основу для создания гибкой и легко поддерживаемой архитектуры программного обеспечения Java. Их применение в разработке позволяет создавать масштабируемые проекты, устойчивые к изменениям требований и обеспечивающие высокую производительность. Это улучшает понимание и тестируемость написанного кода. Соблюдение этих принципов позволяет разработчикам Java создавать качественные и надёжные приложения, которые легко поддерживать и развивать.
Улучши свои карьерные возможности.
Пройди стажировку и стань Middle разработчиком Java
Получи практический опыт разработки Java проекта в Agile команде с наставником. Ты можешь попробовать прямо сейчас!
Made on
Tilda