Принципы SOLID: что это и почему их используют все сеньоры
SRP, OCP, LSP, ISP, DIP — разбираем основы современной архитектуры с примерами на Java.


Иллюстрация: Оля Ежак для Skillbox Media
SOLID — это пять ключевых принципов проектирования классов в объектно-ориентированном программировании. Они помогают создавать понятный, гибкий и легко поддерживаемый код. Благодаря этим принципам архитектура приложения становится надёжнее и удобнее для развития. В статье мы познакомимся с каждым из них и разберём примеры на Java. Так что берите чашку кофе или чая — и начнём!
Содержание
- Что такое SOLID
- Принцип единственной ответственности: SRP — Single Responsibility Principle
- Принцип открытости / закрытости: OCP — Open Closed Principle
- Принцип подстановки Барбары Лисков: LSP — Liskov Substitution Principle
- Принцип разделения интерфейса: ISP — Interface Segregation Principle
- Принцип инверсии зависимостей: DIP — Dependency Inversion Principle
Что такое SOLID и зачем это придумали
Принципы SOLID сформулировал американский инженер-программист Роберт С. Мартин. В начале 2000-х он систематизировал подходы к объектно-ориентированному проектированию в статье Design Principles and Design Patterns. Позже, в 2004 году, консультант по разработке Майкл Физерс предложил объединить эти идеи под аббревиатурой SOLID:
- S — Single Responsibility Principle, принцип единственной ответственности.
- O — Open-Closed Principle, принцип открытости / закрытости.
- L — Liskov Substitution Principle, принцип подстановки Барбары Лисков.
- I — Interface Segregation Principle, принцип разделения интерфейсов.
- D — Dependency Inversion Principle, принцип инверсии зависимостей.
Эти принципы помогают решать типичные проблемы объектно-ориентированных программ:
- Сильно связанные классы: изменение одного затрагивает другие.
- Трудности с тестированием: компоненты тесно связаны друг с другом, из-за чего их сложно тестировать по отдельности.
- Проблемы с расширяемостью: добавление новых функций часто приводит к переработке уже работающего кода.
- Неустойчивость к изменениям: одна правка может сломать всё приложение.
Применение SOLID позволяет создавать гибкую архитектуру, в которой каждый компонент приложения выполняет свою конкретную задачу, не вмешиваясь в работу других. Такой подход делает код легче в тестировании, поддержке и доработке, а изменения в одной части системы не приводят к непредвиденным проблемам в других модулях.
В следующих разделах мы разберём все принципы по очереди и потренируемся применять их на практике. Чтобы понять материал, вам понадобятся базовые знания Java и основ ООП. Если вы только начинаете изучать программирование, советуем сначала прочитать эти статьи:
- Как установить JDK и среду разработки IntelliJ IDEA
- Классы и объекты в Java
- Абстрактные классы в 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 и усложняет поддержку кода.

Изображение: 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. Это делает код более гибким и простым в поддержке.

Изображение: 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: чтобы расширить функциональность, мы не должны менять уже написанный код.

Изображение: 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 закомментируете, результат будет другим:
Сохраняем счёт в базу данных...
Если позже нам понадобится сохранить счёт другим способом, мы сможем просто добавить новый класс с нужной логикой. При этом существующий код, который уже работает и протестирован, останется без изменений.

Изображение: 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: поведение подкласса отличается от поведения базового класса, и такая подстановка приводит к ошибкам.

Изображение: 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 сохраняет равенство всех сторон.

Изображение: 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 всё равно вынужден добавлять лишние методы, которые не используются. Это нарушает принцип разделения интерфейсов.

Изображение: 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: каждый класс выполняет только те методы, что ему действительно нужны, избегая пустых или избыточных реализаций.

Изображение: 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, поскольку бизнес-логика зависит от конкретной реализации, а не от абстракции.

Изображение: 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 и другие) без изменения кода самого сервиса. В этом и заключается принцип инверсии зависимостей: модули высокого уровня должны зависеть от абстракций, а не от конкретных реализаций.

Изображение: Mermaid Chart / Skillbox Media
Больше интересного про код — в нашем телеграм-канале. Подписывайтесь!