Статьи

SOLID Принципы проектирования

Вступление:

Роберт К. Мартин определил пять принципов объектно-ориентированного проектирования:

  • Принцип единой ответственности
  • О закрытый принцип
  • Принцип замещения Лискова
  • Интерфейс Сегрегации и
  • Принцип инверсии зависимости

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

Использование этих принципов может помочь нам сэкономить много усилий в будущем по мере роста размера нашего приложения.

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

Как следует из названия, принцип единой ответственности (SRP) гласит, что каждый класс должен делать только одну вещь. Другими словами, для изменения класса не должно быть более одной причины.

Как мы знаем, большие системы обычно имеют тысячи классов. Если для какого-либо нового требования нужно затронуть несколько классов, у нас больше шансов представить ошибки, нарушив другую функциональность.

Принцип единой ответственности дает нам следующие преимущества:

  • Меньше сцепления: поскольку каждый класс будет делать только одно, зависимостей будет гораздо меньше
  • Легче тестировать: с большей вероятностью код будет проще тестировать, так как гораздо меньше тестов будет охватывать систему целиком

Модельные классы нашей системы обычно всегда следуют принципу SRP. Скажем так, нам нужно изменить состояние пользователей в нашей системе, мы коснемся только класса User :

1
2
3
4
5
6
7
8
public class User {
  
    private int id;
    private String name;
    private List<Address> addresses;
     
    //constructors, getters, setters
}

И так по принципу SRP.

Открытый Закрытый Принцип:

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

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

Допустим, в нашей системе есть класс EventPlanner, который долго работает на наших производственных серверах:

01
02
03
04
05
06
07
08
09
10
11
12
public class EventPlanner {
  
    private List<String> participants;
    private String organizer;
  
    public void planEvent() {
        System.out.println("Planning a simple traditional event");
        ...
    }
  
    ...
}

Но теперь мы планируем использовать ThemeEventPlanner , который будет планировать события с использованием случайной темы, чтобы сделать их более интересными. Вместо прямого перехода к существующему коду и добавления логики для выбора темы события и ее использования лучше расширить наш производственно-стабильный класс:

1
2
3
4
5
public class ThemeEventPlanner extends EventPlanner {
    private String theme;
  
    ...
}

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

Принцип замещения Лискова:

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

Чтобы мы могли достичь этого, объекты наших подклассов должны вести себя точно так же, как и объекты суперкласса.

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

Давайте посмотрим на приведенный ниже пример:

01
02
03
04
05
06
07
08
09
10
11
12
public class Bird {
    public void fly() {
        System.out.println("Bird is now flying");
    }
}
  
public class Ostrich extends Bird {
    @Override
    public void fly() {
       throw new IllegalStateException("Ostrich can't fly");
    }
}

Хотя СтраусПтица , он все равно не может летать, и это явное нарушение принципа подстановки Лискова (LSP). Кроме того, коды, включающие логику проверки типов, являются четким признаком того, что были установлены неправильные отношения.

Существует два способа рефакторинга кода в соответствии с LSP:

  • Устранить неправильные отношения между объектами
  • Используйте принцип « Говори, не спрашивай », чтобы исключить проверку типов и приведение

Допустим, у нас есть некоторый код, включающий проверки типов:

1
2
3
4
5
6
7
//main method code
for(User user : listOfUsers) {
    if(user instanceof SubscribedUser) {
        user.offerDiscounts();
    }
    user.makePurchases();
}

Используя принцип «Говори, не спрашивай» , мы изменим код выше, чтобы он выглядел так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class SubscribedUser extends User {
    @Override
    public void makePurchases() {
        this.offerDiscounts();
        super.makePurchases();
    }
  
    public void offerDiscounts() {...}
}
  
//main method code
for(User user : listOfUsers) {
    user.makePurchases();
}

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

Согласно принципу разделения интерфейса, клиенты не должны быть вынуждены иметь дело с методами, которые они не используют. Мы должны разбить больший интерфейс на меньшие, где это необходимо.

Допустим, у нас есть интерфейс ShoppingCart :

1
2
3
4
5
6
7
8
public interface ShoppingCart {
  
    void addItem(Item item);
    void removeItem(Item item);
    void makePayment();
    boolean checkItemAvailability(Item item);
    
}

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

Так что это хорошая идея, чтобы сломать вышеуказанный интерфейс как:

01
02
03
04
05
06
07
08
09
10
11
12
public interface BaseShoppingCart {
    void addItem(Item item);
    void removeItem(Item item);
}
  
public interface PaymentProcessor {
    void makePayment();
}
  
public interface StockVerifier {
    boolean checkItemAvailability(Item item);
}

Принцип сегрегации интерфейса (ISP) также усиливает другие принципы:

  • Принцип единой ответственности: классы, которые реализуют меньшие интерфейсы, обычно более сфокусированы и обычно имеют одну цель
  • Принцип подстановки Лискова: при меньших интерфейсах у нас больше шансов, что классы реализуют их, чтобы полностью заменить интерфейс

Инверсия зависимостей:

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

Модули высокого уровня говорят нам, что должно делать программное обеспечение . Авторизация пользователя и оплата являются примерами модулей высокого уровня.

С другой стороны, низкоуровневые модули сообщают нам, как программное обеспечение должно выполнять различные задачи, то есть включает детали реализации. Некоторые примеры низкоуровневых модулей включают безопасность (OAuth), сети, доступ к базе данных, IO и т. Д.

Давайте напишем интерфейс UserRepository и его класс реализации:

01
02
03
04
05
06
07
08
09
10
public interface UserRepository {
    List<User> findAllUsers();
}
public class UserRepository implements UserRepository {
  
    public List<User> findAllUsers() {
        //queries database and returns a list of users
        ...
    }
}

Здесь мы извлекли абстракцию модуля в интерфейсе.

Теперь скажем, у нас есть модуль UserAuthorization высокого уровня, который проверяет, авторизован ли пользователь для доступа к системе или нет. Мы будем использовать только ссылку интерфейса UserRepository :

1
2
3
4
5
6
7
8
9
public class UserAuthorization {
  
    ...
  
    public boolean isValidUser(User user) {
        UserRepository repo = UserRepositoryFactory.create();
        return repo.getAllUsers().stream().anyMatch(u -> u.equals(user));
    }
}

Кроме того, мы используем фабричный класс для создания экземпляра UserRepository .

Обратите внимание, что мы полагаемся только на абстракцию, а не на конкретность. И так, мы можем легко добавить больше реализаций UserRepository без особого влияния на наш модуль высокого уровня.

Как это элегантно!

Вывод:

В этом уроке мы обсудили принципы проектирования SOLID. Мы также рассмотрели примеры кода на Java для каждого из этих принципов.

Опубликовано на Java Code Geeks с разрешения Шубхры Шриваставы, партнера нашей программы JCG . Смотрите оригинальную статью здесь: SOLID Design Принципы

Мнения, высказанные участниками Java Code Geeks, являются их собственными.