← Назад к вопросам

Что такое SOLID? Приведите примеры каждого принципа.

2.0 Middle🔥 111 комментариев
#Docker, Kubernetes и DevOps#JVM и управление памятью

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что такое SOLID? Приведите примеры каждого принципа.

Введение

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

Single Responsibility Principle Open/Closed Principle Liskov Substitution Principle Interface Segregation Principle Dependency Inversion Principle


S — Single Responsibility Principle (SRP)

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

Каждый класс должен иметь одну и только одну причину для изменения (одну ответственность).

Пример нарушения SRP

// Плохо: класс отвечает за разные вещи
public class User {
    private String name;
    private String email;
    
    // Ответственность 1: управление данными пользователя
    public void setName(String name) {
        this.name = name;
    }
    
    // Ответственность 2: работа с БД
    public void saveToDatabase() {
        System.out.println("Сохраняю в БД...");
    }
    
    // Ответственность 3: отправка писем
    public void sendEmail(String message) {
        System.out.println("Отправляю email: " + message);
    }
}

Правильное применение SRP

// Ответственность 1: управление данными пользователя
public class User {
    private String name;
    private String email;
    
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
    
    public String getName() { return name; }
    public String getEmail() { return email; }
}

// Ответственность 2: работа с БД
public class UserRepository {
    public void save(User user) {
        System.out.println("Сохраняю " + user.getName() + " в БД");
    }
}

// Ответственность 3: отправка писем
public class EmailService {
    public void sendWelcomeEmail(User user) {
        System.out.println("Отправляю привет: " + user.getEmail());
    }
}

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


O — Open/Closed Principle (OCP)

Принцип открытости/закрытости

Класс должен быть:

  • Открыт для расширения (мы можем добавить новый функционал)
  • Закрыт для модификации (мы не изменяем исходный код)

Пример нарушения OCP

// Плохо: при добавлении нового способа оплаты нужно менять код
public class PaymentProcessor {
    public void processPayment(String type, double amount) {
        if (type.equals("CREDIT_CARD")) {
            processCreditCard(amount);
        } else if (type.equals("PAYPAL")) {
            processPayPal(amount);
        } else if (type.equals("BITCOIN")) {  // Новый тип!
            processBitcoin(amount);            // Меняем класс!
        }
    }
    
    private void processCreditCard(double amount) {
        System.out.println("Обработка кредитной карты: " + amount);
    }
    
    private void processPayPal(double amount) {
        System.out.println("Обработка PayPal: " + amount);
    }
    
    private void processBitcoin(double amount) {
        System.out.println("Обработка Bitcoin: " + amount);
    }
}

Правильное применение OCP

// Интерфейс для расширения
public interface PaymentMethod {
    void pay(double amount);
}

// Конкретные реализации
public class CreditCardPayment implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Обработка кредитной карты: " + amount);
    }
}

public class PayPalPayment implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Обработка PayPal: " + amount);
    }
}

public class BitcoinPayment implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Обработка Bitcoin: " + amount);
    }
}

// Процессор — открыт для расширения, закрыт для модификации
public class PaymentProcessor {
    private PaymentMethod paymentMethod;
    
    public PaymentProcessor(PaymentMethod paymentMethod) {
        this.paymentMethod = paymentMethod;
    }
    
    public void processPayment(double amount) {
        paymentMethod.pay(amount);  // Не нужно менять этот код!
    }
}

// Использование
public class Main {
    public static void main(String[] args) {
        // Добавили Bitcoin — код PaymentProcessor не изменился!
        PaymentProcessor processor = new PaymentProcessor(new BitcoinPayment());
        processor.processPayment(100);
    }
}

Ключевой момент: используем полиморфизм (интерфейсы/абстрактные классы) для расширения без изменения кода.


L — Liskov Substitution Principle (LSP)

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

Подклассы должны быть заменяемы на суперклассы без нарушения корректности программы.

Пример нарушения LSP

// Плохо: Bird.fly() работает, но Penguin не может летать!
public class Bird {
    public void fly() {
        System.out.println("Птица летит");
    }
}

public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Пингвины не летают!");
    }
}

public class Main {
    public static void main(String[] args) {
        Bird bird = new Penguin();  // Компилируется
        bird.fly();                 // Ошибка! Нарушение LSP
    }
}

Правильное применение LSP

// Правильная иерархия
public abstract class Animal {
    public abstract void move();
}

public abstract class Bird extends Animal {
    @Override
    public void move() {
        System.out.println("Птица движется");
    }
}

public class FlyingBird extends Bird {
    public void fly() {
        System.out.println("Летающая птица летит");
    }
}

public class Penguin extends Bird {
    @Override
    public void move() {
        System.out.println("Пингвин плывёт");
    }
}

// Использование
public class Main {
    public static void main(String[] args) {
        Bird bird = new Penguin();
        bird.move();  // "Пингвин плывёт" — корректно!
    }
}

Ключевой момент: наследование должно моделировать отношение "является", а не "притворяется".


I — Interface Segregation Principle (ISP)

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

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

Клиент не должен зависеть от методов, которые он не использует.

Пример нарушения ISP

// Плохо: один большой интерфейс
public interface Worker {
    void work();
    void eat();
    void manage();
    void code();
}

public class Developer implements Worker {
    @Override
    public void work() { System.out.println("Разработка"); }
    
    @Override
    public void eat() { System.out.println("Обед"); }
    
    @Override
    public void manage() { 
        // Developer не управляет!
        throw new UnsupportedOperationException();
    }
    
    @Override
    public void code() { System.out.println("Кодирование"); }
}

public class Manager implements Worker {
    @Override
    public void work() { System.out.println("Работа"); }
    
    @Override
    public void eat() { System.out.println("Обед"); }
    
    @Override
    public void manage() { System.out.println("Управление"); }
    
    @Override
    public void code() { 
        // Manager не кодит!
        throw new UnsupportedOperationException();
    }
}

Правильное применение ISP

// Разделяем на специализированные интерфейсы
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Manageable {
    void manage();
}

public interface Codeable {
    void code();
}

// Каждый класс реализует только нужные интерфейсы
public class Developer implements Workable, Eatable, Codeable {
    @Override
    public void work() { System.out.println("Работаю"); }
    
    @Override
    public void eat() { System.out.println("Ем"); }
    
    @Override
    public void code() { System.out.println("Кодирую"); }
}

public class Manager implements Workable, Eatable, Manageable {
    @Override
    public void work() { System.out.println("Работаю"); }
    
    @Override
    public void eat() { System.out.println("Ем"); }
    
    @Override
    public void manage() { System.out.println("Управляю"); }
}

Ключевой момент: класс реализует только те методы, которые ему нужны.


D — Dependency Inversion Principle (DIP)

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

  • Высокоуровневые модули не должны зависеть от низкоуровневых модулей
  • Оба должны зависеть от абстракций
  • Зависимости должны быть инъецированы, а не созданы внутри класса

Пример нарушения DIP

// Плохо: высокоуровневый класс зависит от низкоуровневого
public class MySQLDatabase {
    public void save(String data) {
        System.out.println("Сохраняю в MySQL: " + data);
    }
}

public class UserService {
    private MySQLDatabase database = new MySQLDatabase();  // Жёсткая зависимость!
    
    public void saveUser(String userName) {
        database.save(userName);
    }
}

// Проблема: если захотим MongoDBDatabase, нужно менять UserService!

Правильное применение DIP

// Абстракция (интерфейс)
public interface Database {
    void save(String data);
}

// Конкретные реализации
public class MySQLDatabase implements Database {
    @Override
    public void save(String data) {
        System.out.println("Сохраняю в MySQL: " + data);
    }
}

public class MongoDBDatabase implements Database {
    @Override
    public void save(String data) {
        System.out.println("Сохраняю в MongoDB: " + data);
    }
}

// Высокоуровневый класс зависит от абстракции
public class UserService {
    private Database database;  // Зависит от интерфейса!
    
    // Инъекция зависимости через конструктор
    public UserService(Database database) {
        this.database = database;
    }
    
    public void saveUser(String userName) {
        database.save(userName);
    }
}

// Использование
public class Main {
    public static void main(String[] args) {
        // Легко менять реализацию!
        Database db = new MongoDBDatabase();
        UserService service = new UserService(db);
        service.saveUser("John");
    }
}

Ключевые техники:

  • Constructor Injection (инъекция через конструктор)
  • Setter Injection (инъекция через setter)
  • Interface Injection (инъекция через интерфейс)

Практический пример: применение всех принципов

// 1. SRP: каждый класс отвечает за одно
public interface UserRepository {
    void save(User user);
}

public interface EmailService {
    void sendEmail(String email, String message);
}

// 2. OCP: легко добавлять новые реализации
public class JdbcUserRepository implements UserRepository {
    @Override
    public void save(User user) {
        System.out.println("Сохраняю в БД: " + user.getName());
    }
}

// 3. LSP: подклассы правильно наследуют
public class User {
    private String name;
    private String email;
    
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
    
    public String getName() { return name; }
    public String getEmail() { return email; }
}

// 4. ISP: разделённые интерфейсы
public interface Sendable {
    void send();
}

public class RegistrationService implements Sendable {
    private UserRepository userRepository;  // DIP: инъекция
    private EmailService emailService;      // DIP: инъекция
    
    // 5. DIP: зависимости инъецируются
    public RegistrationService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
    
    public void register(String name, String email) {
        User user = new User(name, email);
        userRepository.save(user);
        emailService.sendEmail(email, "Добро пожаловать!");
    }
    
    @Override
    public void send() {
        // ...
    }
}

Таблица принципов

ПринципСутьВыигрыш
SОдна ответственностьЛегче тестировать, изменять
OРасширение без измененийНовый функционал без риска
LПравильное наследованиеПолиморфизм работает корректно
IРазделённые интерфейсыКлассы не зависят от ненужных методов
DЗависит от абстракцийГибкость, тестируемость, слабая связанность

Вывод

На интервью при ответе на SOLID:

  1. Объясните каждый принцип кратко (одно предложение)
  2. Приведите реальный пример (хорошо и плохо)
  3. Упомяните выигрыш — почему это важно
  4. Покажите код — иллюстрируйте примерами

SOLID принципы — это фундамент качественного кода на Java и в других ООП языках.

Что такое SOLID? Приведите примеры каждого принципа. | PrepBro