SOLID - DmitryGontarenko/usefultricks GitHub Wiki

Использование принципов SOLID может помочь в создании более удобного, понятного и гибкого программного обеспечения.

В принципы SOLID входит:

  1. Single Responsibility Principle (Принцип единственной ответственности)
  2. Open Closed Principle (Принцип открытости/закрытости)
  3. Liskov’s Substitution Principle (Принцип подстановки Барбары Лисков)
  4. Interface Segregation Principle (Принцип разделения интерфейса)
  5. Dependency Inversion Principle (Принцип инверсии зависимостей)

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

Принцип гласит, что для каждого класса должно быть определено единственное назначение. Все ресурсы, необходимые для его существования, должны быть инкапсулированы в этом классе и быть направлены на одну задачу.
Другими словами, класс должен нести ответственность только на одну операцию, а не быть "универсальным".

Рассмотрим на примере. Представим что у нас есть класс, который полностью отвечает за обработку заказа:

public class OrderProcessor {
    public void process(Order order){
        if (save(order)) {
            sendConfirmationEmail(order);
        }
    }

    private boolean save(Order order) {
        // сохранение заказа в БД
    }

    private void sendConfirmationEmail(Order order) {
        // уведомление клиента
    }
}

Получается, что класс OrderProcessor, по сути, содержит в себе сразу три разные обязанности - обработка заказа, способ сохранения и отправка уведомления. Каждая "обязанность" должна быть вынесена в одельный класс или модуль.

public class OrderRepository {
    public boolean save(Order order) {
        // сохранение заказа в БД
    }
}

public class EmailNotofication {
    public void sendEmail(Order order) {
        // уведомление клиента
    }
}

public class OrderProcessor {
    public void process(Order order){
        OrderRepository repository = new OrderRepository ();
        EmailNotofication notification = new EmailNotofication ();

        if (repository.save(order)) {
            notification.sendEmail(order);
        }
    }
}

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

Принцип гласит, что программные сущности (классы, модули, функции и т.п.) должны быть открыты для расширения, но закрыты для изменения.
Принцип служит для того, что бы иметь возможность добавлять/изменять поведение класса, не внося изменения в текущие операции, которые он уже выполняет.
Это осуществляется с помощью расширения класса и переопределения некоторых его функций.

Рассмотрим на примере. Представим, что мы хотим изменить логику нашего класса OrderProcessor, добавляя некую логику ДО и ПОСЛЕ обработки заказа:

public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {
    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }
}

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

Принцип гласит, что объекты в программе можно заменить их наследниками без изменения свойств программы.
Это означает, что класс, разработанный путем расширения на основании базового класса, должен переопределять его методы так, чтобы не нарушалась функциональность с точки зрения ожидаемого поведения.

Рассмотрим на примере. Представим, что у нас есть класс, который отвечает за валидацию заказа и проверяет, все ли из товаров заказа находятся на складке:

public class OrderStockValidator {
    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if (!item.isInStock()) {
                return false;
            }
        }
        return true;
    }
}

Теперь предположим, что помимо наличия товара на складке, нужно проверять и все ли товары упакованы:

public class OrderStockAndPackValidator extends OrderStockValidator {
    @Override
    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if (!item.isInStock() && !item.isPacked()){
                throw new IllegalStateException(
                     String.format("Order %d is not valid", order.getId())
                );
            }
        }
        return true;
    }
}

В данной реализации будет нарушен принцип LSP, так как после добавления функционала вместо того, чтобы вернуть false, если заказ не прошел валидацию, метод начал бросать исключение IllegalStateException - это будет считаться нарушением контракта для метода isValid().

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

Принцип гласит, что клиенты не должны быть вынуждены реализовывать методы, которые они не будут использовать.
Лучше разделить слишком "крупные" интерфейсы на более мелкие, класс должен быть ограничен лишь тем минимумом операций, которые необходимы для осуществления его функции.

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

public interface NotificationService {
    void smsNotification();
    void emailNotification();
}

Этот пример в данном случае нарушает принцип ISP. Если пользователю данного интерфейса нужно будет реализовать только один из методов - он будет вынужден или реализовывать оба метода, или оставлять второй метод пустым.
Решением этой проблемы будет разделение существующего интерфейса на два более мелких.

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

Принцип гласит, что зависимости внутри системы должны строятся на основе абстракций.
Когда класс зависит от абстракий, а не от конкретных реализаций, можно без труда менять его поведение, внедряя нужные зависимости.
Хорошим примером этого принципа служит Spring Framework, где все модули выполнены в виде отдельных компонентов (бинов) и могут работать вместе.

Источники

JavaRush. Пять основных принципов дизайна классов (S.O.L.I.D.) в Java
Habr. Принципы SOLID в картинках

YAGNI

YAGNI (You aren`t gonna need it, вам это не понадобится) - принцип разработки ПО, который подразумевает отказ от добавления функциональности, в которой нет непосредственной необходимости прямо сейчас.
Создание функциональности "на всякий случай" может привести к нежелательным последствиям:

  • Тратится время не только на создание, но и на тестирование, документирование новой функциональности;
  • Пока новые функции действительно не нужны, их трудно протестировать. Из за этого в будущем они могут неправильно работать;
  • Не документированный функционал может как минимум неиспользованным (и будет увеличивать размер кода), а как максимум использован неправильно, что может повлечь за собой ошибки или риски при работе программы.