Применение SOLID принципов в разработке приложений на Java с использованием Spring
Что такое SOLID
SOLID – популярная концепция объектно-ориентированного программирования (ООП), которая побуждает разработчиков (Java, Android) создавать более простые в сопровождении, понятные и гибкие программные системы.
Пять принципов в программировании, составляющих аббревиатуру концепции SOLID:
S – Принцип единственной ответственности (Single Responsibility Principle)
O – Принцип открытости/закрытости (Open/Closed Principle)
L – Принцип подстановки Барбары Лисков (Liskov Substitution Principle)
I – Принцип разделения интерфейсов (Interface Segregation Principle)
D – Принцип инверсии зависимостей (Dependency Inversion Principle).
Важность SOLID в разработке ПО
Принципы SOLID помогают в программировании вносить в код изменения, не вызывая при этом каких-либо серьезных проблем. Также они призваны облегчить поддержку и упросить расширение проектов и их понимание, чтобы получить адаптивное, эффективное, гибкое ПО.
Хотя эти концепции программирования имеют множество преимуществ, их соблюдение приводит к написанию более длинного и сложного кода. Что может продлить процесс проектирования и усложнить разработку.
Важно понимать, что дополнительные затраты стоят того. При правильном исполнении код становится более читаемым, удобным для тестирования и поддержки.
Принцип единой ответственности (Single Responsibility Principle)
Объяснение Single Responsibility Principle
Принцип солид SRP означает – в ООП должна быть только одна причина для инверсии кода, когда, например, модуль отвечает только за одну часть функционала.
SRP – базовая концепция, использование которой упрощает тестирование и поддержку, а также помогает избежать непредвиденных побочных эффектов после изменений в программировании.
Внедрение SRP подразумевает дробление сложных задач на мелкие и более управляемые части, что позволяет легче тестировать, понимать и изменять их, не влияя на систему в целом.
Примеры реализации в Java с Spring
Рассмотрим класс, который содержит код, изменяющий текст. Задача класса — манипулирование текстом:
public class TextManipulator { private String text; public TextManipulator(String text) { this.text = text; } public String getText() { return text; } public void appendText(String newText) { text = text.concat(newText); } public String findWordAndReplace(String word, String replacementWord) { if (text.contains(word)) { text = text.replace(word, replacementWord); } return text; } public String findWordAndDelete(String word) { if (text.contains(word)) { text = text.replace(word, ""); } return text; } public void printText() { System.out.println(textManipulator.getText()); } }В следующем примере у класса две обязанности — манипулирование и печать текста. Наличие метода, который выводит текст в этом классе, нарушает принцип единственной ответственности. Создадим еще один класс для печати текста:
public class TextPrinter { TextManipulator textManipulator; public TextPrinter(TextManipulator textManipulator) { this.textManipulator = textManipulator; } public void printText() { System.out.println(textManipulator.getText()); } public void printOutEachWordOfText() { System.out.println(Arrays.toString(textManipulator.getText().split(" "))); } public void printRangeOfCharacters(int startingIndex, int endIndex) { System.out.println(textManipulator.getText().substring(startingIndex, endIndex)); } }
Теперь можем создавать методы в этом классе для печати текста, причем любого количества.
Принцип открытости/закрытости (Open/Closed Principle)
Объяснение Open/Closed Principle
Принцип открытости-закрытости OCP заключается в утверждении – классы, модули и функции в ООП открыты для расширения кода, но закрыты для инверсий.
Следование OCP необходимо, чтобы написать легко редактируемый код. Критерии кажутся противоречивыми по своей сути, но его смысл простой: можно расширить функциональность, но при этом не изменяя код.
Примеры реализации в Java с Spring
Продолжим предыдущий пример с заказом. Новый паттерн – клиенту отправлено письмо с подтверждением заказа и перед началом обработки заказа нужно добавить действия. Вместо замены class OrderProcessor, расширим его с соблюдением принципов OCP:
public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor { @Override public void process(Order order) { beforeProcessing(); super.process(order); afterProcessing(); } private void beforeProcessing() { // Осуществим некоторые действия перед обработкой заказа } private void afterProcessing() { // Осуществим некоторые действия после обработки заказа } }
Принцип подстановки Барбары Лисков (Liskov Substitution Principle)
Объяснение Liskov Substitution Principle
Концепция подстановки/замены Лискова в солид требует, чтобы каждый производный класс в программировании был заменяем своим родительским. Преимущество в том, что применяя его, вы знаете, чего ожидать от вашего кода.
Во многих отношениях LSP– это просто расширенный принцип открытости-закрытости. Ведь этот способ гарантирует, что производные классы расширяют базовый без инверсии поведения.
Следование LSP помогает избежать в программировании неожиданных результатов и необходимости открывать закрытый class, чтобы внести изменения, приводит к упрощению расширения ПО и позволяет избежать множества проблем во время обновлений и расширений.
Примеры реализации в Java с Spring
Пример: класс, который отвечает за проверку самого заказа и соответствия наличию товаров на складе. Метод класса isValid возвращает true или false:
public class OrderStockValidator { public boolean isValid(Order order) { for (Item item : order.getItems()) { if (! item.isInStock()) { return false; } } return true; } }
Принцип разделения интерфейса (Interface Segregation Principle)
Объяснение Interface Segregation Principle
Принцип ISP предполагает создавать в ООП множество небольших интерфейсов, чем поменьше количеством, но крупных. То есть, не следует заставлять клиентов применять методы, которые им не нужны.
Концепция ISP схожа с единой ответственностью. Но в данном случае речь идет не об одном интерфейсе, выполняющем только одну задачу, а о разбиении всей кодовой базы на несколько компонентов программирования.
Меньшие интерфейсы означают, что разработчикам на Android нужно отдавать предпочтение созданию отдельных компонентов со специфичными для них функциями, композиции вместо наследования и развязке вместо связности. Например, компонент, который отвечает за реализацию прокрутки вверх, не будет переключаться между светлым и темным режимами.
Примеры реализации в Java с Spring
Рассмотрим пример ниже, в котором интерфейсы Printer, Scanner и Fax содержат один метод. Далее определяем класс AllInOnePrinter (реализует интерфейсы Printer, Scanner и Fax) и класс SimplePrinter (реализует Printer).
public interface Printer { void print(); } public interface Scanner { void scan(); } public interface Fax { void fax(); } public class AllInOnePrinter implements Printer, Scanner, Fax { public void print() { // код для печати } public void scan() { // код для сканирования } public void fax() { // код для отправки факса } } public class SimplePrinter implements Printer { public void print() { // код для печати } }
Теперь можно создавать совокупности объектов в зависимости от требований и не затрагивать неисзуемый код. То есть, если нужна только печать документов, используется класс SimplePrinter без создания класса AllInOnePrinter.
Принцип инверсии зависимостей (Dependency Inversion Principle)
Объяснение Dependency Inversion Principle
Принцип инверсии зависимостей DIP в солид предлагает способ независимого разделения программных модулей высокого и низкого уровня.
Разработчикам следует писать код, основанный на общих абстракциях, реализующихся разными способами программирования. Использование концепции DIP помогает сделать код более гибким, маневренным и пригодным для повторного использования.
Примеры реализации в Java с Spring
Допустим, class Switch зависит от интерфейса ISwitchable, а не от конкретного class LightBulb. Модули высокого уровня (Switch) зависят от абстракций (ISwitchable), а модули низкого уровня (LightBulb) реализуют эти абстракции, упрощая замену компонентов:
// Абстракция: открытый интерфейс ISwitchable ISwitchable { void TurnOn (); недействительный TurnOff (); } // Модуль низкого уровня: LightBulb (реализует ISwitchable) public class LightBulb : ISwitchable { public void TurnOn () { Console.WriteLine( "Лампочка включена." ); } public void TurnOff () { Console.WriteLine( "Лампочка выключена." ); } } // Модуль высокого уровня: Switch (зависит от ISwitchable) public class Switch { частное ISwitchable устройство, доступное только для чтения ; общественный коммутатор (ISwitchable устройство) { this .device = устройство; // Внедрение зависимостей через конструктор } общественный недействительный FlipOn () { device.TurnOn(); } общественный недействительный FlipOff () { device.TurnOff(); } } class Program { static void Main () { ISwitchable lamp = new LightBulb(); // Создание экземпляра LightBulb Switch LightSwitch = new Switch(bulb); // Внедряем экземпляр в Switch LightSwitch.FlipOn(); LightSwitch.FlipOff(); } }
Практические примеры применения SOLID принципов
Стоит соблюдать принципы SOLID во всех случаях, когда требуется получить легко поддерживаемый, гибкий, тестируемый, простой для понимания код. Игнорирование может привести к ошибкам, снижению производительности и невозможности добавления новых функций.
Диаграммы классов и их объяснения
В разработке ПО диаграмма классов (class diagram) в UML – основной строительный элемент для моделирования структуры системы, которая наглядно показывает их методы, атрибуты и отношения между объектами.
На картинке пример простого класса:
Назначение class diagram:
- Показать статическую структуру классификаторов в системе
- Предоставить базовые обозначения для других структурных диаграмм, предписанных UML
- Помочь разработчикам
- Моделировать системы с точки зрения бизнеса.
В ООП существует несколько типов отношений в class diagram, каждый из которых служит определенной цели:
- Ассоциация – двунаправленная связь между двумя classes, когда экземпляры одного связаны с экземплярами другого.
- Направленная ассоциация – связь между двумя классами, где направление указывает, что один связан с другим конкретным образом.
- Агрегация представляет собой связь «целое-часть». Это более сильная связь, когда один class (целое) содержит другой class (часть) или состоит из него.
- Композиция – более сильная форма агрегации, которая указывает на значимые отношения владения или зависимости.
- Наследование – форма, где один класс наследует свойства и поведение другого.
- Реализация – форма, где класс реализует функции интерфейса.
- Отношения зависимости – форма, где один class (клиент) использует другой (поставщик) или зависит от него для выполнения определенных задач, а также для доступа к определенным функциям. Класс клиента полагается на услуги classes поставщика, но не владеет им и не создает его экземпляры.
Рекомендации и лучшие практики Java-приложений
Разработка актуальных java приложений – многогранный и непростой процесс, где требуется применение глубоких познаний и постоянное изучение новых инструментов.
Рекомендации для создания лучших Java-приложений:
- Применяйте в ООП трендовые фреймворки и библиотеки, чтобы упростить программирование и обеспечить высокую производительность веб-приложения
- Используйте принципы SOLID и паттерны проектирования
- Применяйте регулярное обновление и оптимизацию приложения, чтобы увеличить его производительность и повысить безопасность.