Рубрики
Без рубрики

ТВЕРДЫЕ принципы проектирования: Создание стабильных и гибких систем

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

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

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

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

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

ТВЕРДОЕ название представляет собой мнемоническую аббревиатуру, где каждая буква представляет принцип разработки программного обеспечения следующим образом:

  1. Ы для Принципа единой ответственности
  2. O для Открытого/Закрытого Принципа
  3. L для принципа подстановки Лискова
  4. I для Принципа Разделения Интерфейсов
  5. D для Принципа Инверсии Зависимостей

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

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

Принцип единой ответственности является первым принципом проектирования SOLID, представленным буквой “S” и определенным/| Робертом К. Мартином . В нем говорится, что в хорошо спроектированном приложении каждый класс (микросервис, модуль кода) должен иметь только одну единственную ответственность. Ответственность используется в смысле наличия только одной причины для изменений.

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

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

Давайте посмотрим на пример. Я буду использовать Java но вы можете применить принципы SOLID design и к любым другим языкам ООП.

Допустим, мы пишем Java-приложение для книжного магазина. Мы создаем класс Book , который позволяет пользователям получать и устанавливать названия и авторов каждой книги, а также выполнять поиск по книге в инвентаре.

class Book {

    String title;
    String author;

    String getTitle() {
        return title;
    }
    void setTitle(String title) {
        this.title = title;
    }
    String getAuthor() {
        return author;
    }
    void setAuthor(String author) {
        this.author = author;
    }
    void searchBook() {...}

}

Однако приведенный выше код нарушает Принцип единой ответственности, поскольку класс Book имеет две обязанности. Во-первых, он устанавливает данные, относящиеся к книгам ( название и автор ). Во-вторых, он ищет книгу в инвентаре. Методы setter изменяют объект Book , что может вызвать проблемы, когда мы хотим выполнить поиск по той же книге в инвентаре.

Чтобы применить Принцип единой ответственности, нам нужно разделить эти две обязанности. В переработанном коде класс Book будет отвечать только за получение и настройку данных объекта Book .

class Book {

    String title;
    String author;

    String getTitle() {
        return title;
    }
    void setTitle(String title) {
        this.title = title;
    }
    String getAuthor() {
        return author;
    }
    void setAuthor(String author) {
        this.author = author;
    }

}

Затем мы создаем другой класс с именем Inventory View , который будет отвечать за проверку инвентаря. Мы перемещаем сюда метод search Box() и ссылаемся на класс Book в конструкторе.

class InventoryView {

  Book book;

    InventoryView(Book book) {
        this.book = book;
    }

    void searchBook() {...}

}

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

Принцип Открытия/Закрытия

Принцип Open/Closed – это буква “О” из пяти принципов разработки программного обеспечения SOLID. Это был Бертран Мейер , который ввел этот термин в своей книге “Объектно-ориентированное построение программного обеспечения”. Принцип открытости/закрытости гласит, что классы, модули, микросервисы и другие единицы кода должны быть открыты для расширения, но закрыты для модификации.

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

Пример открытого/Закрытого принципа

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

class CookbookDiscount {

    String getCookbookDiscount() {

        String discount = "30% between Dec 1 and 24.";

        return discount;
    }

}

class DiscountManager {

    void processCookbookDiscount(CookbookDiscount discount) {...}

}

Этот код работает нормально до тех пор, пока руководство магазина не сообщит нам, что их скидки на кулинарные книги были настолько успешными, что они хотят продлить их. Теперь они хотят раздавать каждую биографию со скидкой 50% в день рождения субъекта. Чтобы добавить новую функцию, мы создаем новый Скидка на биографию/| класс:

class BiographyDiscount {

    String getBiographyDiscount() {

        String discount = "50% on the subject's birthday.";     

        return discount;

    }
}

Чтобы обработать новый тип скидки, нам также необходимо добавить новую функциональность в класс Discount Manager :

class DiscountManager {

    void processCookbookDiscount(CookbookDiscount discount) {...}

    void processBiographyDiscount(BiographyDiscount discount) {...}

}

Однако, поскольку мы изменили существующую функциональность, мы нарушили принцип “Открыто/закрыто”. Хотя приведенный выше код работает должным образом, он может добавить новые уязвимости в приложение. Мы не знаем, как новое дополнение будет взаимодействовать с другими частями кода, которые зависят от класса Discount Manager . В реальном приложении это означало бы, что нам нужно снова протестировать и развернуть все наше приложение.

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

public interface BookDiscount {

    String getBookDiscount();

}

class CookbookDiscount implements BookDiscount {

    @Override
    public String getBookDiscount() {
        String discount = "30% between Dec 1 and 24.";

        return discount;
    }

}

class BiographyDiscount implements BookDiscount {

    @Override
    public String getBookDiscount() {
        String discount = "50% on the subject's birthday.";

        return discount;
    }

}

Теперь, Менеджер скидок может ссылаться на BookDiscount интерфейс вместо конкретных классов. Когда вызывается метод process Book Discount() , мы можем передать оба Скидка на Кулинарную книгу и Скидка на биографию в качестве аргумента, поскольку оба являются реализацией BookDiscount |/интерфейс.

class DiscountManager {

    void processBookDiscount(BookDiscount discount) {...}
}

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

Приведенный ниже график UML показывает, как выглядит наш пример кода до и после рефакторинга. Слева вы можете видеть, что Менеджер по скидкам зависит от Скидка на Кулинарную книгу и Скидка на биографию/| классы. Справа все три класса зависят от абстрактного слоя Book Discount ( Discount Manager ссылается на него, в то время как Скидка на Кулинарную книгу и Скидка на биографию реализуйте ее).

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

Принцип замещения Лискова является третьим принципом SOLID, представленным буквой “L”. Именно Барбара Лисков представила этот принцип в 1987 году в своем программном выступлении на конференции “Абстракция данных”. Первоначальная формулировка Принципа подстановки Лискова немного сложна, поскольку в ней утверждается, что:

” В компьютерной программе, если S является подтипом T, то объекты типа T могут быть заменены объектами типов (т.е. объекты типа S могут заменять объекты типа T) без изменения каких-либо желательных свойств этой программы (корректность, выполняемая задача и т.д.).”

С точки зрения непрофессионала, в нем говорится, что объект суперкласса должен быть заменен объектами его подклассов, не вызывая проблем в приложении. Таким образом, дочерний класс никогда не должен изменять характеристики своего родительского класса (такие как список аргументов и возвращаемые типы). Вы можете реализовать принцип подстановки Лискова, обратив внимание на правильную иерархию наследования.

Пример принципа подстановки Лискова

Теперь книжный магазин просит нас добавить в приложение новую функцию доставки. Итак, мы создаем класс Book Delivery , который информирует клиентов о количестве мест, где они могут забрать свой заказ:

class BookDelivery {
    String titles;
    int userID;

    void getDeliveryLocations() {...}
}

Тем не менее, в магазине также продаются модные книги в твердом переплете, которые они хотят доставлять только в свои магазины на хай-стрит. Итак, мы создаем новый подкласс Доставка в твердом переплете , который расширяет Доставка книг и переопределяет метод getDeliveryLocations() со своей собственной функциональностью:

class HardcoverDelivery extends BookDelivery {

    @Override
    void getDeliveryLocations() {...}

}

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

class AudiobookDelivery extends BookDelivery {

    @Override
    void getDeliveryLocations() {/* can't be implemented */}
}

Однако мы могли бы изменить некоторые характеристики метода get Delivery Locations() , что нарушило бы принцип подстановки Лискова. После внесения изменений мы не смогли заменить суперкласс Доставка книг подклассом Доставка аудиокниг без нарушения работы приложения.

Чтобы решить эту проблему, нам нужно исправить иерархию наследования. Давайте введем дополнительный уровень, который лучше различает типы доставки книг. Новая Автономная доставка и Онлайн-доставка классы разделяют суперкласс BookDelivery . Мы также перемещаем метод get Delivery Locations() в Автономная доставка и создайте новый метод get Software Options() для класса OnlineDelivery (поскольку он больше подходит для онлайн-доставки).

class BookDelivery {

    String title;
    int userID;

}

class OfflineDelivery extends BookDelivery {

    void getDeliveryLocations() {...}

}

class OnlineDelivery extends BookDelivery {

    void getSoftwareOptions() {...}

}

В переработанном коде Доставка в твердом переплете будет дочерним классом/| Автономная доставка и он переопределит метод get Delivery Locations() со своей собственной функциональностью.

Доставка аудиокниг будет дочерним классом//Онлайн-доставка что является хорошей новостью, так как теперь ей не нужно иметь дело с методом get Delivery Locations() . Вместо этого он может переопределить метод get Software Options() своего родителя с помощью собственной реализации (например, путем перечисления и встраивания доступных аудиоплееров).

class HardcoverDelivery extends OfflineDelivery {

    @Override
    void getDeliveryLocations() {...}

}

class AudiobookDelivery extends OnlineDelivery {

    @Override
    void getSoftwareOptions() {...}

}

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

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

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

Принцип разделения интерфейсов является четвертым принципом проектирования SOLID, представленным буквой “I” в аббревиатуре. Именно Роберт К. Мартин первым определил этот принцип, заявив, что “клиентов не следует заставлять зависеть от методов, которые они не используют. ” Под клиентами он имеет в виду классы, которые реализуют интерфейсы. Другими словами, интерфейсы не должны включать в себя слишком много функций.

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

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

Давайте добавим некоторые действия пользователя в наш книжный интернет-магазин, чтобы клиенты могли взаимодействовать с контентом перед совершением покупки. Для этого мы создаем интерфейс с именем Book Action с тремя методами: see Reviews() , поиск Second Hand() и listenSample() .

public interface BookAction {

    void seeReviews();
    void searchSecondhand();
    void listenSample();

}

Затем мы создаем два класса: Hardcover UI и Пользовательский интерфейс аудиокниги , реализующий интерфейс BookAction со своими собственными функциональными возможностями:

class HardcoverUI implements BookAction {

    @Override
    public void seeReviews() {...}

    @Override
    public void searchSecondhand() {...}

    @Override
    public void listenSample() {...}

}

class AudiobookUI implements BookAction {

    @Override
    public void seeReviews() {...}

    @Override
    public void searchSecondhand() {...}

    @Override
    public void listenSample() {...}

}

Оба класса зависят от методов, которые они не используют, поэтому мы нарушили принцип разделения интерфейсов. Книги в твердом переплете нельзя слушать, поэтому Пользовательский интерфейс в твердом переплете классу не нужен метод listenSample() . Аналогично, у аудиокниг нет подержанных копий, поэтому классу Audiobook UI это тоже не нужно.

Однако, поскольку интерфейс Book Action включает в себя эти методы, все его зависимые классы должны их реализовывать. Другими словами, Book Action – это загрязненный интерфейс, который нам нужно разделить. Давайте расширим его с помощью двух более конкретных подинтерфейсов: Акция в твердом переплете и Звуковое действие

public interface BookAction {

    void seeReviews();

}

public interface HardcoverAction extends BookAction {

    void searchSecondhand();

}

public interface AudioAction extends BookAction {

    void listenSample();

}

Теперь класс Hardcover UI может реализовать HardcoverAction | интерфейс и класс AudiobookUI могут реализовать интерфейс

Таким образом, оба класса могут реализовать метод see Reviews() суперинтерфейса Book Action . Однако, Hardcover UI не должен реализовывать нерелевантный метод listen Sample() и AudioUI также не должен реализовывать searchSecondhand() .

class HardcoverUI implements HardcoverAction {

    @Override
    public void seeReviews() {...}

    @Override
    public void searchSecondhand() {...}

}

class AudiobookUI implements AudioAction {

    @Override
    public void seeReviews() {...}

    @Override
    public void listenSample() {...}

}

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

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

Принцип Инверсии зависимостей является пятым принципом проектирования SOLID, представленным последней буквой “D” и введенным Робертом К. Мартином. Цель принципа инверсии зависимостей состоит в том, чтобы избежать тесно связанного кода, поскольку он легко нарушает работу приложения. Принцип гласит, что:

“Модули высокого уровня не должны зависеть от модулей низкого уровня. И то, и другое должно зависеть от абстракций.”

“Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций”.

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

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

Роберт К. Мартин также упоминает, что Принцип инверсии зависимостей представляет собой специфическую комбинацию принципов подстановки Open/Closed и Liskov.

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

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

Чтобы реализовать новую функциональность, мы создаем класс Book более низкого уровня и класс Shelf более высокого уровня. Класс Book позволит пользователям просматривать обзоры и читать образцы каждой книги, которую они хранят на своих полках. Класс Shelf позволит им добавить книгу на свою полку и настроить полку.

class Book {

    void seeReviews() {...}
    void readSample() {...}

}

class Shelf {

    Book book;

    void addBook(Book book) {...}
    void customizeShelf() {...}

}

Все выглядит нормально, но поскольку высокоуровневый класс Shelf зависит от низкоуровневого класса Book , приведенный выше код нарушает принцип инверсии зависимостей. Это становится очевидным, когда магазин просит нас разрешить покупателям также добавлять DVD-диски на свои полки. Чтобы выполнить требование, мы создаем новый DVD класс:

class DVD {

    void seeReviews() {...}
    void watchSample() {...}

}

Теперь мы должны изменить класс Shelf , чтобы он также мог принимать DVD-диски. Однако это явно нарушило бы принцип Открытости/Закрытости. Решение состоит в том, чтобы создать уровень абстракции для классов более низкого уровня ( Book и DVD ). Мы сделаем это, представив Продукт интерфейс, который будут реализовывать оба класса.

public interface Product {

    void seeReviews();
    void getSample();

}

class Book implements Product {

    @Override
    public void seeReviews() {...}

    @Override
    public void getSample() {...}

}

class DVD implements Product {

    @Override
    public void seeReviews() {...}

    @Override
    public void getSample() {...}

}

Теперь Shelf может ссылаться на Продукт интерфейс вместо его реализаций ( Книга и DVD ). Переработанный код также позволяет нам позже вводить новые типы продуктов (например, Magazine ), которые клиенты также могут разместить на своих полках.

class Shelf {

    Product product;

    void addProduct(Product product) {...}

    void customizeShelf() {...}

}

Приведенный выше код также следует принципу подстановки Лискова, поскольку тип Product может быть заменен обоими его подтипами ( Book и DVD ) без нарушения работы программы. В то же время мы также реализовали принцип инверсии зависимостей, поскольку в переработанном коде классы высокого уровня также не зависят от классов низкого уровня.

Как вы можете видеть слева от графика UML ниже, высокоуровневый класс Shelf зависит от низкоуровневого класса Book до рефакторинга. Не применяя Принцип инверсии зависимостей, мы также должны сделать его зависимым от низкоуровневого класса DVD . Однако после рефакторинга как высокоуровневые, так и низкоуровневые классы зависят от абстрактного Продукта интерфейс ( |/Полка ссылается на него, в то время как Книга и DVD реализовать его).

Как вы должны внедрять принципы надежного проектирования?

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

Когда вы нарушаете принцип НАДЕЖНОГО проектирования, Java и другие скомпилированные языки могут выдавать Исключение , но это происходит не всегда. Проблемы с архитектурой программного обеспечения трудно обнаружить, но расширенное диагностическое программное обеспечение, такое как APM tools , может предоставить вам много полезных советов.

Оригинал: “https://dev.to/azaleamollis/solid-design-principles-building-stable-and-flexible-systems–2ph7”