Главная Веб-разработка Принципы SOLID: что это и почему их используют все сеньоры

Принципы SOLID: что это и почему их используют все сеньоры

от admin

SRP, OCP, LSP, ISP, DIP — разбираем основы современной архитектуры с примерами на Java.

SOLID — это пять ключевых принципов проектирования классов в объектно-ориентированном программировании. Они помогают создавать понятный, гибкий и легко поддерживаемый код. Благодаря этим принципам архитектура приложения становится надёжнее и удобнее для развития. В статье мы познакомимся с каждым из них и разберём примеры на Java. Так что берите чашку кофе или чая — и начнём!

Что такое SOLID и зачем это придумали

Принципы SOLID сформулировал американский инженер-программист Роберт С. Мартин. В начале 2000-х он систематизировал подходы к объектно-ориентированному проектированию в статье Design Principles and Design Patterns. Позже, в 2004 году, консультант по разработке Майкл Физерс предложил объединить эти идеи под аббревиатурой SOLID:

  • S — Single Responsibility Principle, принцип единственной ответственности.
  • O — Open-Closed Principle, принцип открытости / закрытости.
  • — Liskov Substitution Principle, принцип подстановки Барбары Лисков.
  • I — Interface Segregation Principle, принцип разделения интерфейсов.
  • D — Dependency Inversion Principle, принцип инверсии зависимостей.

Эти принципы помогают решать типичные проблемы объектно-ориентированных программ:

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

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

В следующих разделах мы разберём все принципы по очереди и потренируемся применять их на практике. Чтобы понять материал, вам понадобятся базовые знания Java и основ ООП. Если вы только начинаете изучать программирование, советуем сначала прочитать эти статьи:

Если вам так удобнее, вместо IntelliJ IDEA можно использовать VS Code с пакетом расширений Extension Pack for Java. Этого будет достаточно для запуска примеров из статьи — мы специально сделали их довольно простыми. Например, поля объявлены без private, геттеры и сеттеры не используются, а вместо реальной логики — просто System.out.println().

В реальных проектах код будет сложнее: с продуманной архитектурой, слоями, интерфейсами, тестами и другими практиками. Но когда вы только знакомитесь с SOLID, такие детали могут отвлекать от сути.

Принцип единственной ответственности: SRP — Single Responsibility Principle

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

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

public class MainSRPViolation { public static void main(String[] args) { // Создаём книгу Book book = new Book(“Clean Code”, “Robert C. Martin”, 40); // Создаём счёт на 3 экземпляра книги Invoice invoice = new Invoice(book, 3); // Печатаем счёт invoice.printInvoice(); // Сохраняем счёт в файл invoice.saveToFile(“invoice.txt”); } } // Книга — просто данные class Book { String name; String authorName; int price; public Book(String name, String authorName, int price) { this.name = name; this.authorName = authorName; this.price = price; } } // Invoice нарушает SRP: отвечает за расчёт, за вывод и сохранение class Invoice { Book book; int quantity; double total; public Invoice(Book book, int quantity) { this.book = book; this.quantity = quantity; this.total = calculateTotal(); } // Считаем сумму public double calculateTotal() { return book.price * quantity; } // Печатаем счёт public void printInvoice() { System.out.println(quantity + “x ” + book.name + ” ” + book.price + “$”); System.out.println(“Total: ” + total + “$”); } // Сохраняем счёт (имитация процесса) public void saveToFile(String filename) { System.out.println(“Сохраняем счёт в файл: ” + filename); } }

Результат вывода:

3x Clean Code 40$ Total: 120.0$ Сохраняем счёт в файл: invoice.txt

На первый взгляд, всё кажется логичным, но на деле этот класс нарушает первый принцип SOLID сразу в нескольких местах:

  • Метод printInvoice() отвечает за вывод счёта. Если нужно изменить формат отображения — например, добавить поддержку PDF или HTML, — придётся редактировать сам класс Invoice, что нарушает принцип единственной ответственности. Логика отображения не должна смешиваться с бизнес-логикой.
  • Метод saveToFile() отвечает за сохранение счёта в файл. Но если в будущем потребуется сохранять данные, например, в базу данных или отправлять их по API, снова придётся править класс Invoice.

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

Принципы SOLID: что это и почему их используют все сеньоры

Нарушение SRP: Invoice совмещает логику расчёта, вывода и сохранения
Изображение: Mermaid Chart / Skillbox Media

Чтобы соблюдать принцип единственной ответственности, разделим задачи между классами: Invoice будет рассчитывать стоимость заказа, InvoicePrinter — выводить счёт, а InvoicePersistence — сохранять его:

public class MainSRPRefactored { public static void main(String[] args) { // Создаём книгу Book book = new Book(“Clean Code”, “Robert C. Martin”, 40); // Создаём счёт на 3 книги Invoice invoice = new Invoice(book, 3); // Печатаем счёт InvoicePrinter printer = new InvoicePrinter(invoice); printer.print(); // Сохраняем счёта InvoicePersistence persistence = new InvoicePersistence(invoice); persistence.saveToFile(“invoice.txt”); } } // Книга — просто данные class Book { public String name; public String authorName; public int price; public Book(String name, String authorName, int price) { this.name = name; this.authorName = authorName; this.price = price; } } // Счёт — расчёт суммы и данные заказа class Invoice { public Book book; public int quantity; public double total; public Invoice(Book book, int quantity) { this.book = book; this.quantity = quantity; this.total = book.price * quantity; } } // Вывод счёта — отдельная задача class InvoicePrinter { private Invoice invoice; public InvoicePrinter(Invoice invoice) { this.invoice = invoice; } public void print() { System.out.println(invoice.quantity + “x ” + invoice.book.name + ” ” + invoice.book.price + “$”); System.out.println(“Total: ” + invoice.total + “$”); } } // Сохранение счёта — отдельная задача class InvoicePersistence { private Invoice invoice; public InvoicePersistence(Invoice invoice) { this.invoice = invoice; } public void saveToFile(String filename) { System.out.println(“Сохраняем в файл: ” + filename); System.out.println(“Содержимое:”); System.out.println(invoice.quantity + “x ” + invoice.book.name + ” ” + invoice.book.price + “$”); System.out.println(“Total: ” + invoice.total + “$”); } }

Результат вывода:

3x Clean Code 40$ Total: 120.0$ Сохраняем в файл: invoice.txt Содержимое: 3x Clean Code 40$ Total: 120.0$

После такого разделения каждый компонент отвечает только за свою задачу. Теперь можно легко менять формат вывода в InvoicePrinter или способ хранения в InvoicePersistence, не затрагивая бизнес-логику в классе Invoice. Это делает код более гибким и простым в поддержке.

Принципы SOLID: что это и почему их используют все сеньоры

Каждый класс отвечает за своё: Invoice — за данные и расчёт, InvoicePrinter — за вывод, InvoicePersistence — за сохранение
Изображение: Mermaid Chart / Skillbox Media

Принцип открытости / закрытости: OCP — Open Closed Principle

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

Допустим, у нас уже есть приложение для выставления счетов, и начальник просит добавить сохранение счетов в базу данных. Что приходит в голову в первую очередь? Просто дописать метод saveToDatabase() в уже существующий класс InvoicePersistence:

public class MainOCPViolation { public static void main(String[] args) { // Каждый раз при добавлении нового способа сохранения // приходится менять класс InvoiceSaver, — это нарушение OCP Book book = new Book(“Clean Code”, “Robert C. Martin”, 40); Invoice invoice = new Invoice(book, 3); InvoiceSaver saver = new InvoiceSaver(invoice); // Сохраняем счёт в файл saver.saveToFile(“invoice.txt”); // Сохраняем счёт в базу данных saver.saveToDatabase(); } } // Книга — просто данные class Book { String name; String authorName; int price; public Book(String name, String authorName, int price) { this.name = name; this.authorName = authorName; this.price = price; } } // Счёт — хранит данные и рассчитывает сумму class Invoice { Book book; int quantity; double total; public Invoice(Book book, int quantity) { this.book = book; this.quantity = quantity; this.total = book.price * quantity; } } // Saver нарушает OCP — он жёстко привязан к способам сохранения class InvoiceSaver { Invoice invoice; public InvoiceSaver(Invoice invoice) { this.invoice = invoice; } // Сохранение в файл public void saveToFile(String filename) { System.out.println(“Сохраняем счёт в файл: ” + filename); } // Сохранение в базу данных public void saveToDatabase() { System.out.println(“Сохраняем счёт в базу данных…”); } // Если разработчику нужно будет добавить в проект MongoDB, API или облачную базу данных, придётся снова менять этот класс }

Вывод в консоль при запуске кода:

Сохраняем счёт в файл: invoice.txt Сохраняем счёт в базу данных…

На первый взгляд, всё логично, но есть проблема. Если мы захотим добавить другие способы хранения, нам снова придётся менять этот класс. А это противоречит второму принципу SOLID: чтобы расширить функциональность, мы не должны менять уже написанный код.

Принципы SOLID: что это и почему их используют все сеньоры

Нарушение OCP: при добавлении новых способов сохранения нужно менять InvoicePersistence
Изображение: Mermaid Chart / Skillbox Media

Для соблюдения принципа открытости / закрытости создадим интерфейс InvoicePersistence, а затем реализуем отдельный класс для каждого способа хранения: FilePersistence для файлов и DatabasePersistence для базы данных. Благодаря такому подходу мы сможем при необходимости добавлять новые типы хранилищ, не меняя существующий код:

package refactored.ocp; public class MainOCPRefactored { public static void main(String[] args) { // Создаём книгу Book book = new Book(“Clean Code”, “Robert C. Martin”, 40); // Создаём счёт на 3 книги Invoice invoice = new Invoice(book, 3); // Выбираем способ сохранения — в данном случае в файл // Используем интерфейс, не трогая код Invoice или Main InvoicePersistence persistence = new FilePersistence(); persistence.save(invoice); // Хотим сохранить в базу? Просто создаём другую реализацию: // InvoicePersistence persistence = new DatabasePersistence(); // persistence.save(invoice); } } // Книга — просто набор данных class Book { public String name; public String authorName; public int price; public Book(String name, String authorName, int price) { this.name = name; this.authorName = authorName; this.price = price; } } // Счёт — хранит данные и считает итоговую сумму class Invoice { public Book book; public int quantity; public double total; public Invoice(Book book, int quantity) { this.book = book; this.quantity = quantity; this.total = book.price * quantity; } } // Интерфейс для всех способов сохранения interface InvoicePersistence { void save(Invoice invoice); } // Сохраняем счёт в файл class FilePersistence implements InvoicePersistence { @Override public void save(Invoice invoice) { System.out.println(“Сохраняем счёт в файл: invoice.txt”); } } // Сохраняем счёт в базу данных class DatabasePersistence implements InvoicePersistence { @Override public void save(Invoice invoice) { System.out.println(“Сохраняем счёт в базу данных…”); } }

Если запустить код как есть, в консоли появится сообщение:

Сохраняем счёт в файл: invoice.txt

Однако, если вы раскомментируете строку с DatabasePersistence, а FilePersistence закомментируете, результат будет другим:

Сохраняем счёт в базу данных…

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

Принципы SOLID: что это и почему их используют все сеньоры

Принцип OCP: новые классы (FilePersistence, DatabasePersistence) добавляются без изменения существующего кода
Изображение: Mermaid Chart / Skillbox Media

Принцип подстановки Барбары Лисков: LSP — Liskov Substitution Principle

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

Читать также:
Из чего состоит компьютер: секреты процессора, видеокарты и блока питания

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

public class MainLSPViolation { public static void main(String[] args) { // Прямоугольник — всё работает как ожидалось Rectangle rc = new Rectangle(2, 3); AreaFixedHeight.getArea(rc); // Ожидаемая площадь: 20 // Квадрат — это наследник прямоугольника, но он меняет поведение setWidth и setHeight Rectangle sq = new Square(); sq.setWidth(5); // Ожидаем, что изменится только ширина // Но у квадрата меняются сразу обе стороны — ширина и высота AreaFixedHeight.getArea(sq); // Ожидаемая площадь: 50, но получим другую } } // Прямоугольник — базовый класс с шириной и высотой class Rectangle { protected int width, height; public Rectangle() {} public Rectangle(int width, int height) { this.width = width; this.height = height; } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public int getArea() { return width * height; } } // Квадрат меняет поведение родителя и ломает логику class Square extends Rectangle { public Square() {} public Square(int size) { width = height = size; } @Override public void setWidth(int width) { // Меняем ширину и высоту одновременно super.setWidth(width); super.setHeight(width); } @Override public void setHeight(int height) { // Меняем высоту и ширину — снова ломаем контракт super.setHeight(height); super.setWidth(height); } } // Метод расчёта площади прямоугольника с фиксированной высотой class AreaFixedHeight { static void getArea(Rectangle r) { int width = r.getWidth(); // Сохраняем начальную ширину r.setHeight(10); // Меняем только высоту System.out.println(“Ожидаемая площадь: ” + (width * 10) + “, полученная: ” + r.getArea()); } }

Казалось бы: если мы меняем ширину квадрата, автоматически меняется и высота, и наоборот. Но что может пойти не так? Например, код, рассчитанный на работу с прямоугольником с фиксированной высотой, может выдать неожиданный результат, если передать ему объект Square:

Ожидаемая площадь: 20, полученная: 20 Ожидаемая площадь: 50, полученная: 100

При вызове AreaFixedHeight.getArea(sq) мы наблюдаем неожиданное поведение: метод рассчитан на работу с объектами Rectangle и предполагает, что изменение высоты никак не влияет на ширину. Однако в Square метод setHeight() переопределён так, что меняет оба параметра одновременно. Это нарушает третий принцип SOLID: поведение подкласса отличается от поведения базового класса, и такая подстановка приводит к ошибкам.

Принципы SOLID: что это и почему их используют все сеньоры

Нарушение LSP: класс Square наследуется от Rectangle, но переопределяет методы так, что ломает ожидаемое поведение
Изображение: Mermaid Chart / Skillbox Media

В нашем случае Square не должен наследоваться от Rectangle, потому что их поведение различается. Вместо этого лучше создать общий интерфейс Shape и реализовать его отдельно в обоих классах, — так мы избегаем проблем с подстановкой и соблюдаем принцип LSP:

package refactored.lsp; public class MainLSPRefactored { public static void main(String[] args) { // Прямоугольник и квадрат реализуют один интерфейс Shape rectangle = new Rectangle(2, 10); // прямоугольник 2 × 10 Shape square = new Square(5); // квадрат 5 × 5 // Метод printArea() работает с любой фигурой printArea(rectangle); // Площадь: 20 printArea(square); // Площадь: 25 } // Универсальный метод для обработки любой фигуры static void printArea(Shape shape) { System.out.println(“Площадь: ” + shape.getArea()); } } // Интерфейс с методом для вычисления площади interface Shape { int getArea(); } // Прямоугольник: ширина × высота class Rectangle implements Shape { private int width, height; public Rectangle(int width, int height) { this.width = width; this.height = height; } @Override public int getArea() { return width * height; } } // Квадрат: стороны равны class Square implements Shape { private int size; public Square(int size) { this.size = size; } @Override public int getArea() { return size * size; } }

Лог в консоли:

Площадь: 20 Площадь: 25

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

Принципы SOLID: что это и почему их используют все сеньоры

Соблюдение LSP: Rectangle и Square реализуют общий интерфейс Shape — подстановка работает корректно, без нарушения поведения
Изображение: Mermaid Chart / Skillbox Media

Принцип разделения интерфейсов: ISP — Interface Segregation Principle

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

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

public class MainISPViolation { public static void main(String[] args) { // Два сотрудника с разным поведением Employee dev = new Developer(); Employee manager = new Manager(); // Все сотрудники вызывают одни и те же методы dev.work(); dev.eat(); dev.relax(); manager.work(); manager.eat(); manager.relax(); } } // Интерфейс объединяет все обязанности — без разделения по ролям interface Employee { void work(); void eat(); void relax(); } // Разработчику подходят все методы class Developer implements Employee { public void work() { System.out.println(“Разработчик пишет код…”); } public void eat() { System.out.println(“Разработчик обедает…”); } public void relax() { System.out.println(“Разработчик отдыхает…”); } } // Менеджер вынужден реализовывать лишние методы class Manager implements Employee { public void work() { System.out.println(“Менеджер проводит встречи…”); } public void eat() { System.out.println(“Менеджеры не обедают…”); } public void relax() { System.out.println(“Менеджеры не отдыхают…”); } }

Результат выполнения программы:

Разработчик пишет код… Разработчик обедает… Разработчик отдыхает… Менеджер проводит встречи… Менеджеры не обедают… Менеджеры не отдыхают…

Каждый класс, который использует интерфейс Employee, должен описывать все три метода. Представим двух сотрудников:

  • Разработчик (Developer) — работает, обедает и отдыхает.
  • Менеджер (Manager) — только работает, без обедов и перерывов.

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

Принципы SOLID: что это и почему их используют все сеньоры

Нарушение ISP: интерфейс Employee включает всё сразу, и приходится реализовывать даже ненужные методы
Изображение: Mermaid Chart / Skillbox Media

Давайте разделим интерфейс Employee на несколько узких интерфейсов — так каждый класс сможет реализовать только нужные ему методы:

package refactored.isp; public class MainISPRefactored { public static void main(String[] args) { // Разработчик реализует все необходимые интерфейсы Workable dev = new Developer(); dev.work(); // Дополнительно можно вызвать обед и перерыв: // ((Lunchable) dev).eatLunch(); // ((Breakable) dev).takeBreak(); // Менеджер реализует только нужный интерфейс Workable manager = new Manager(); manager.work(); } } // Интерфейс для работы interface Workable { void work(); } // Интерфейс для обеда interface Lunchable { void eatLunch(); } // Интерфейс для перерыва interface Breakable { void takeBreak(); } // Разработчик работает, ест и отдыхает class Developer implements Workable, Lunchable, Breakable { @Override public void work() { System.out.println(“Разработчик пишет код…”); } @Override public void eatLunch() { System.out.println(“Разработчик обедает…”); } @Override public void takeBreak() { System.out.println(“Разработчик отдыхает…”); } } // Менеджер только работает class Manager implements Workable { @Override public void work() { System.out.println(“Менеджер проводит встречи…”); } }

Информация в консоли:

Разработчик пишет код… Менеджер проводит встречи…

Теперь интерфейс Employee разделён на три отдельных интерфейса: Workable, Lunchable и Breakable. Получается следующее:

  • Developer реализует все три — он работает, обедает и отдыхает.
  • Manager реализует только Workable — ничего лишнего.

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

Принципы SOLID: что это и почему их используют все сеньоры

Соблюдение ISP: интерфейсы разделены по задачам — каждый класс реализует только нужные методы
Изображение: Mermaid Chart / Skillbox Media

Принцип инверсии зависимостей: DIP — Dependency Inversion Principle

Принцип инверсии зависимостей (DIP) означает, что модули высокого уровня должны зависеть от абстракций, а не от модулей низкого уровня. Под модулями высокого уровня обычно понимают бизнес-логику приложения — например, управление пользователями или обработку заказов. Модули низкого уровня — это конкретные технические реализации: работа с базой данных, API или файловой системой.

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

«Если OCP описывает цель объектно-ориентированной архитектуры, то DIP — это основной механизм её достижения».

Эти два принципа тесно связаны: чтобы классы были открыты для расширения (OCP), нужно отказаться от жёстких зависимостей в пользу абстракций (DIP). Представьте себе конструктор LEGO: вместо того чтобы детали были наглухо склеены, они соединяются через стандартные разъёмы — как интерфейсы в коде. Благодаря этому можно легко заменять одни блоки на другие, не ломая всю конструкцию.

Пусть у нас есть класс OrderService, который отвечает за обработку заказов и напрямую зависит от конкретной реализации — класса MySQLDatabase:

public class MainDIPViolation { public static void main(String[] args) { // OrderService напрямую зависит от конкретной базы данных (MySQL) OrderService orderService = new OrderService(); // Сохраняем заказ orderService.saveOrder(new Order(“Заказ №1”)); } } // OrderService зависит от реализации MySQLDatabase class OrderService { private MySQLDatabase database; public OrderService() { // Создание конкретной реализации внутри класса this.database = new MySQLDatabase(); } public void saveOrder(Order order) { database.save(order); } } // Хранилище на базе MySQL — модуль низкого уровня class MySQLDatabase { public void save(Order order) { System.out.println(“Сохраняем заказ в MySQL: ” + order.description); } } // Данные о заказе class Order { public String description; public Order(String description) { this.description = description; } }

Результат выполнения кода:

Сохраняем заказ в MySQL: Заказ №1

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

Принципы SOLID: что это и почему их используют все сеньоры

Нарушение DIP: OrderService зависит от MySQLDatabase, что затрудняет тестирование и переиспользование кода
Изображение: Mermaid Chart / Skillbox Media

Создадим интерфейс, который будет абстракцией для хранения данных.

package refactored.dip; public class MainDIPRefactored { public static void main(String[] args) { // Две разные реализации репозитория OrderRepository mysqlRepo = new MySQLDatabase(); OrderRepository mongoRepo = new MongoDBDatabase(); // OrderService работает через интерфейс — не зависит от конкретной базы OrderService orderService1 = new OrderService(mysqlRepo); OrderService orderService2 = new OrderService(mongoRepo); // Сохраняем заказы с разными источниками данных orderService1.saveOrder(new Order(“Заказ №1”)); orderService2.saveOrder(new Order(“Заказ №2”)); } } // Абстракция для хранилища заказов interface OrderRepository { void save(Order order); } // Реализация для MySQL class MySQLDatabase implements OrderRepository { @Override public void save(Order order) { System.out.println(“Сохраняем заказ в MySQL: ” + order.description); } } // Реализация для MongoDB class MongoDBDatabase implements OrderRepository { @Override public void save(Order order) { System.out.println(“Сохраняем заказ в MongoDB: ” + order.description); } } // OrderService — бизнес-логика, зависит только от интерфейса class OrderService { private final OrderRepository repository; public OrderService(OrderRepository repository) { this.repository = repository; } public void saveOrder(Order order) { repository.save(order); } } // Класс с данными о заказе class Order { public String description; public Order(String description) { this.description = description; } }

Сообщение в консоли:

Сохраняем заказ в MySQL: Заказ №1 Сохраняем заказ в MongoDB: Заказ №2

Теперь OrderService зависит не от конкретной реализации репозитория, а от абстракции — интерфейса OrderRepository. Такой подход позволяет подключать разные реализации хранилища данных (например, MySQL, MongoDB и другие) без изменения кода самого сервиса. В этом и заключается принцип инверсии зависимостей: модули высокого уровня должны зависеть от абстракций, а не от конкретных реализаций.

Принципы SOLID: что это и почему их используют все сеньоры

Соблюдение DIP: OrderService зависит от интерфейса OrderRepository, а не от конкретной реализации базы данных
Изображение: Mermaid Chart / Skillbox Media

Похожие статьи