HTML Diff
2 added 2 removed
Original 2026-01-01
Modified 2026-02-21
1 <p><a>#статьи</a></p>
1 <p><a>#статьи</a></p>
2 <ul><li>11 апр 2025</li>
2 <ul><li>11 апр 2025</li>
3 <li>0</li>
3 <li>0</li>
4 </ul><p>SRP, OCP, LSP, ISP, DIP - разбираем основы современной архитектуры с примерами на Java.</p>
4 </ul><p>SRP, OCP, LSP, ISP, DIP - разбираем основы современной архитектуры с примерами на Java.</p>
5 <p>Иллюстрация: Оля Ежак для Skillbox Media</p>
5 <p>Иллюстрация: Оля Ежак для Skillbox Media</p>
6 <p>Автор статей о программировании. 14 лет в IT. Умеет рассказывать о технологиях простыми словами. Автор спецпроекта Advertising for Social Change.</p>
6 <p>Автор статей о программировании. 14 лет в IT. Умеет рассказывать о технологиях простыми словами. Автор спецпроекта Advertising for Social Change.</p>
7 <p>SOLID - это пять ключевых принципов проектирования классов в объектно-ориентированном программировании. Они помогают создавать понятный, гибкий и легко поддерживаемый код. Благодаря этим принципам архитектура приложения становится надёжнее и удобнее для развития. В статье мы познакомимся с каждым из них и разберём примеры на Java. Так что берите чашку кофе или чая - и начнём!</p>
7 <p>SOLID - это пять ключевых принципов проектирования классов в объектно-ориентированном программировании. Они помогают создавать понятный, гибкий и легко поддерживаемый код. Благодаря этим принципам архитектура приложения становится надёжнее и удобнее для развития. В статье мы познакомимся с каждым из них и разберём примеры на Java. Так что берите чашку кофе или чая - и начнём!</p>
8 <p><strong>Содержание</strong></p>
8 <p><strong>Содержание</strong></p>
9 <ul><li><a>Что такое SOLID</a></li>
9 <ul><li><a>Что такое SOLID</a></li>
10 <li><a>Принцип единственной ответственности: SRP - single responsibility principle</a></li>
10 <li><a>Принцип единственной ответственности: SRP - single responsibility principle</a></li>
11 <li><a>Принцип открытости - закрытости: OCP - open closed principle</a></li>
11 <li><a>Принцип открытости - закрытости: OCP - open closed principle</a></li>
12 <li><a>Принцип подстановки Барбары Лисков: LSP - Liskov substitution principle</a></li>
12 <li><a>Принцип подстановки Барбары Лисков: LSP - Liskov substitution principle</a></li>
13 <li><a>Принцип разделения интерфейса: ISP - interface segregation principle</a></li>
13 <li><a>Принцип разделения интерфейса: ISP - interface segregation principle</a></li>
14 <li><a>Принцип инверсии зависимостей: DIP - dependency inversion principle</a></li>
14 <li><a>Принцип инверсии зависимостей: DIP - dependency inversion principle</a></li>
15 </ul><p>Принципы SOLID сформулировал американский инженер-программист <a>Роберт С. Мартин</a>. В начале 2000-х он систематизировал подходы к объектно-ориентированному проектированию в <a>статье Design Principles and Design Patterns</a>. Позже, в 2004 году, консультант по разработке<a>Майкл Физерс</a>предложил объединить эти идеи под аббревиатурой SOLID:</p>
15 </ul><p>Принципы SOLID сформулировал американский инженер-программист <a>Роберт С. Мартин</a>. В начале 2000-х он систематизировал подходы к объектно-ориентированному проектированию в <a>статье Design Principles and Design Patterns</a>. Позже, в 2004 году, консультант по разработке<a>Майкл Физерс</a>предложил объединить эти идеи под аббревиатурой SOLID:</p>
16 <ul><li><strong>S</strong> - single responsibility principle, принцип единственной ответственности.</li>
16 <ul><li><strong>S</strong> - single responsibility principle, принцип единственной ответственности.</li>
17 <li><strong>O</strong> - open-closed principle, принцип открытости - закрытости.</li>
17 <li><strong>O</strong> - open-closed principle, принцип открытости - закрытости.</li>
18 <li><strong>L </strong>- Liskov substitution principle, принцип подстановки Барбары Лисков.</li>
18 <li><strong>L </strong>- Liskov substitution principle, принцип подстановки Барбары Лисков.</li>
19 <li><strong>I</strong> - interface segregation principle, принцип разделения интерфейсов.</li>
19 <li><strong>I</strong> - interface segregation principle, принцип разделения интерфейсов.</li>
20 <li><strong>D</strong> - dependency inversion principle, принцип инверсии зависимостей.</li>
20 <li><strong>D</strong> - dependency inversion principle, принцип инверсии зависимостей.</li>
21 </ul><p>Эти принципы помогают решать типичные проблемы объектно-ориентированных программ:</p>
21 </ul><p>Эти принципы помогают решать типичные проблемы объектно-ориентированных программ:</p>
22 <ul><li>Сильно связанные классы: изменение одного затрагивает другие.</li>
22 <ul><li>Сильно связанные классы: изменение одного затрагивает другие.</li>
23 <li>Трудности с тестированием: компоненты тесно связаны друг с другом, из-за чего их сложно тестировать по отдельности.</li>
23 <li>Трудности с тестированием: компоненты тесно связаны друг с другом, из-за чего их сложно тестировать по отдельности.</li>
24 <li>Проблемы с расширяемостью: добавление новых функций часто приводит к переработке уже работающего кода.</li>
24 <li>Проблемы с расширяемостью: добавление новых функций часто приводит к переработке уже работающего кода.</li>
25 <li>Неустойчивость к изменениям: одна правка может сломать всё приложение.</li>
25 <li>Неустойчивость к изменениям: одна правка может сломать всё приложение.</li>
26 </ul><p>Применение SOLID позволяет создавать гибкую архитектуру, в которой каждый компонент приложения выполняет свою конкретную задачу, не вмешиваясь в работу других. Такой подход делает код легче в тестировании, поддержке и доработке, а изменения в одной части системы не приводят к непредвиденным проблемам в других модулях.</p>
26 </ul><p>Применение SOLID позволяет создавать гибкую архитектуру, в которой каждый компонент приложения выполняет свою конкретную задачу, не вмешиваясь в работу других. Такой подход делает код легче в тестировании, поддержке и доработке, а изменения в одной части системы не приводят к непредвиденным проблемам в других модулях.</p>
27 <p>В следующих разделах мы разберём все принципы по очереди и потренируемся применять их на практике. Чтобы понять материал, вам понадобятся базовые знания Java и основ ООП. Если вы только начинаете изучать программирование, советуем сначала прочитать эти статьи:</p>
27 <p>В следующих разделах мы разберём все принципы по очереди и потренируемся применять их на практике. Чтобы понять материал, вам понадобятся базовые знания Java и основ ООП. Если вы только начинаете изучать программирование, советуем сначала прочитать эти статьи:</p>
28 <ul><li><a>Как установить JDK и среду разработки IntelliJ IDEA</a></li>
28 <ul><li><a>Как установить JDK и среду разработки IntelliJ IDEA</a></li>
29 <li><a>Классы и объекты в Java</a></li>
29 <li><a>Классы и объекты в Java</a></li>
30 <li><a>Абстрактные классы в Java и их отличия от интерфейсов: кратко и без воды</a></li>
30 <li><a>Абстрактные классы в Java и их отличия от интерфейсов: кратко и без воды</a></li>
31 </ul><p>Если вам так удобнее, вместо IntelliJ IDEA можно использовать<a>VS Code</a>с пакетом расширений<a>Extension Pack for Java</a>. Этого будет достаточно для запуска примеров из статьи - мы специально сделали их довольно простыми. Например, поля объявлены без private, геттеры и сеттеры не используются, а вместо реальной логики - просто System.out.println().</p>
31 </ul><p>Если вам так удобнее, вместо IntelliJ IDEA можно использовать<a>VS Code</a>с пакетом расширений<a>Extension Pack for Java</a>. Этого будет достаточно для запуска примеров из статьи - мы специально сделали их довольно простыми. Например, поля объявлены без private, геттеры и сеттеры не используются, а вместо реальной логики - просто System.out.println().</p>
32 <p>В реальных проектах код будет сложнее: с продуманной архитектурой, слоями, интерфейсами, тестами и другими практиками. Но когда вы только знакомитесь с SOLID, такие детали могут отвлекать от сути.</p>
32 <p>В реальных проектах код будет сложнее: с продуманной архитектурой, слоями, интерфейсами, тестами и другими практиками. Но когда вы только знакомитесь с SOLID, такие детали могут отвлекать от сути.</p>
33 <p>Принцип единственной ответственности означает, что каждый класс должен отвечать за одну задачу. Меняться он должен только по одной причине: изменилась логика в рамках его ответственности.</p>
33 <p>Принцип единственной ответственности означает, что каждый класс должен отвечать за одну задачу. Меняться он должен только по одной причине: изменилась логика в рамках его ответственности.</p>
34 <p>Допустим, у нас есть класс Book, который хранит информацию о книге. Добавим к нему класс Invoice, отвечающий за оформление счёта в книжном магазине. Давайте посмотрим, как это может выглядеть в коде:</p>
34 <p>Допустим, у нас есть класс Book, который хранит информацию о книге. Добавим к нему класс Invoice, отвечающий за оформление счёта в книжном магазине. Давайте посмотрим, как это может выглядеть в коде:</p>
35 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); } }<p>Результат вывода:</p>
35 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); } }<p>Результат вывода:</p>
36 3x Clean Code 40$ Total: 120.0$ Сохраняем счёт в файл: invoice.txt<p>На первый взгляд, всё кажется логичным, но на деле этот класс нарушает первый принцип SOLID сразу в нескольких местах:</p>
36 3x Clean Code 40$ Total: 120.0$ Сохраняем счёт в файл: invoice.txt<p>На первый взгляд, всё кажется логичным, но на деле этот класс нарушает первый принцип SOLID сразу в нескольких местах:</p>
37 <ul><li>Метод printInvoice() отвечает за вывод счёта. Если нужно изменить формат отображения - например, добавить поддержку PDF или HTML, - придётся редактировать сам класс Invoice, что нарушает принцип единственной ответственности. Логика отображения не должна смешиваться с бизнес-логикой.</li>
37 <ul><li>Метод printInvoice() отвечает за вывод счёта. Если нужно изменить формат отображения - например, добавить поддержку PDF или HTML, - придётся редактировать сам класс Invoice, что нарушает принцип единственной ответственности. Логика отображения не должна смешиваться с бизнес-логикой.</li>
38 <li>Метод saveToFile() отвечает за сохранение счёта в файл. Но если в будущем потребуется сохранять данные, например, в базу данных или отправлять их по API, снова придётся править класс Invoice.</li>
38 <li>Метод saveToFile() отвечает за сохранение счёта в файл. Но если в будущем потребуется сохранять данные, например, в базу данных или отправлять их по API, снова придётся править класс Invoice.</li>
39 </ul><p>В итоге один класс выполняет сразу три задачи: рассчитывает итоговую стоимость, выводит счёт и сохраняет данные в файл. Любое изменение способа вывода или хранения потребует вмешательства в бизнес-логику. Это нарушает принцип SRP и усложняет поддержку кода.</p>
39 </ul><p>В итоге один класс выполняет сразу три задачи: рассчитывает итоговую стоимость, выводит счёт и сохраняет данные в файл. Любое изменение способа вывода или хранения потребует вмешательства в бизнес-логику. Это нарушает принцип SRP и усложняет поддержку кода.</p>
40 Нарушение SRP: Invoice совмещает логику расчёта, вывода и сохранения<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Чтобы соблюдать принцип единственной ответственности, разделим задачи между классами: Invoice будет рассчитывать стоимость заказа, InvoicePrinter - выводить счёт, а InvoicePersistence - сохранять его:</p>
40 Нарушение SRP: Invoice совмещает логику расчёта, вывода и сохранения<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Чтобы соблюдать принцип единственной ответственности, разделим задачи между классами: Invoice будет рассчитывать стоимость заказа, InvoicePrinter - выводить счёт, а InvoicePersistence - сохранять его:</p>
41 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 + "$"); } }<p>Результат вывода:</p>
41 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 + "$"); } }<p>Результат вывода:</p>
42 3x Clean Code 40$ Total: 120.0$ Сохраняем в файл: invoice.txt Содержимое: 3x Clean Code 40$ Total: 120.0$<p>После такого разделения каждый компонент отвечает только за свою задачу. Теперь можно легко менять формат вывода в InvoicePrinter или способ хранения в InvoicePersistence, не затрагивая бизнес-логику в классе Invoice. Это делает код более гибким и простым в поддержке.</p>
42 3x Clean Code 40$ Total: 120.0$ Сохраняем в файл: invoice.txt Содержимое: 3x Clean Code 40$ Total: 120.0$<p>После такого разделения каждый компонент отвечает только за свою задачу. Теперь можно легко менять формат вывода в InvoicePrinter или способ хранения в InvoicePersistence, не затрагивая бизнес-логику в классе Invoice. Это делает код более гибким и простым в поддержке.</p>
43 Каждый класс отвечает за своё: Invoice - за данные и расчёт, InvoicePrinter - за вывод, InvoicePersistence - за сохранение<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Согласно этому принципу, код должен быть открыт для расширения, но закрыт для изменения. Если нужно добавить новую функциональность, лучше реализовать её отдельно, а не переписывать существующий класс. Чаще всего для этого используют интерфейсы или абстрактные классы.</p>
43 Каждый класс отвечает за своё: Invoice - за данные и расчёт, InvoicePrinter - за вывод, InvoicePersistence - за сохранение<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Согласно этому принципу, код должен быть открыт для расширения, но закрыт для изменения. Если нужно добавить новую функциональность, лучше реализовать её отдельно, а не переписывать существующий класс. Чаще всего для этого используют интерфейсы или абстрактные классы.</p>
44 <p>Допустим, у нас уже есть приложение для выставления счетов, и начальник просит добавить сохранение счетов в базу данных. Что приходит в голову в первую очередь? Просто дописать метод saveToDatabase() в уже существующий класс InvoicePersistence:</p>
44 <p>Допустим, у нас уже есть приложение для выставления счетов, и начальник просит добавить сохранение счетов в базу данных. Что приходит в голову в первую очередь? Просто дописать метод saveToDatabase() в уже существующий класс InvoicePersistence:</p>
45 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 или облачную базу данных, придётся снова менять этот класс }<p>Вывод в консоль при запуске кода:</p>
45 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 или облачную базу данных, придётся снова менять этот класс }<p>Вывод в консоль при запуске кода:</p>
46 Сохраняем счёт в файл: invoice.txt Сохраняем счёт в базу данных...<p>На первый взгляд, всё логично, но есть проблема. Если мы захотим добавить другие способы хранения, нам снова придётся менять этот класс. А это противоречит второму принципу SOLID: чтобы расширить функциональность, мы не должны менять уже написанный код.</p>
46 Сохраняем счёт в файл: invoice.txt Сохраняем счёт в базу данных...<p>На первый взгляд, всё логично, но есть проблема. Если мы захотим добавить другие способы хранения, нам снова придётся менять этот класс. А это противоречит второму принципу SOLID: чтобы расширить функциональность, мы не должны менять уже написанный код.</p>
47 Нарушение OCP: при добавлении новых способов сохранения нужно менять InvoicePersistence<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Для соблюдения принципа открытости - закрытости создадим интерфейс InvoicePersistence, а затем реализуем отдельный класс для каждого способа хранения: FilePersistence для файлов и DatabasePersistence для базы данных. Благодаря такому подходу мы сможем при необходимости добавлять новые типы хранилищ, не меняя существующий код:</p>
47 Нарушение OCP: при добавлении новых способов сохранения нужно менять InvoicePersistence<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Для соблюдения принципа открытости - закрытости создадим интерфейс InvoicePersistence, а затем реализуем отдельный класс для каждого способа хранения: FilePersistence для файлов и DatabasePersistence для базы данных. Благодаря такому подходу мы сможем при необходимости добавлять новые типы хранилищ, не меняя существующий код:</p>
48 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("Сохраняем счёт в базу данных..."); } }<p>Если запустить код как есть, в консоли появится сообщение:</p>
48 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("Сохраняем счёт в базу данных..."); } }<p>Если запустить код как есть, в консоли появится сообщение:</p>
49 Сохраняем счёт в файл: invoice.txt<p>Однако, если вы раскомментируете строку с DatabasePersistence, а FilePersistence закомментируете, результат будет другим:</p>
49 Сохраняем счёт в файл: invoice.txt<p>Однако, если вы раскомментируете строку с DatabasePersistence, а FilePersistence закомментируете, результат будет другим:</p>
50 Сохраняем счёт в базу данных...<p>Если позже нам понадобится сохранить счёт другим способом, мы сможем просто добавить новый класс с нужной логикой. При этом существующий код, который уже работает и протестирован, останется без изменений.</p>
50 Сохраняем счёт в базу данных...<p>Если позже нам понадобится сохранить счёт другим способом, мы сможем просто добавить новый класс с нужной логикой. При этом существующий код, который уже работает и протестирован, останется без изменений.</p>
51 Принцип OCP: новые классы (FilePersistence, DatabasePersistence) добавляются без изменения существующего кода<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Принцип подстановки Лисков (LSP) устанавливает важное правило для наследования: если в программе используется базовый класс, то любой его подкласс должен работать так же корректно, как и родительский класс. Подкласс не должен нарушать ожидаемое поведение программы.</p>
51 Принцип OCP: новые классы (FilePersistence, DatabasePersistence) добавляются без изменения существующего кода<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Принцип подстановки Лисков (LSP) устанавливает важное правило для наследования: если в программе используется базовый класс, то любой его подкласс должен работать так же корректно, как и родительский класс. Подкласс не должен нарушать ожидаемое поведение программы.</p>
52 <p>Представьте класс Rectangle, который описывает прямоугольник и вычисляет его площадь. Нам требуется создать класс Square, поскольку квадрат - частный случай прямоугольника с равными сторонами:</p>
52 <p>Представьте класс Rectangle, который описывает прямоугольник и вычисляет его площадь. Нам требуется создать класс Square, поскольку квадрат - частный случай прямоугольника с равными сторонами:</p>
53 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()); } }<p>Казалось бы: если мы меняем ширину квадрата, автоматически меняется и высота, и наоборот. Но что может пойти не так? Например, код, рассчитанный на работу с прямоугольником с фиксированной высотой, может выдать неожиданный результат, если передать ему объект Square:</p>
53 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()); } }<p>Казалось бы: если мы меняем ширину квадрата, автоматически меняется и высота, и наоборот. Но что может пойти не так? Например, код, рассчитанный на работу с прямоугольником с фиксированной высотой, может выдать неожиданный результат, если передать ему объект Square:</p>
54 - Ожидаемая площадь: 20, полученная: 20 Ожидаемая площадь: 50, полученная: 100<p>При вызове AreaFixedHeight.getArea(sq) мы наблюдаем неожиданное поведение: метод рассчитан на работу с объектами Rectangle и предполагает, что изменение высоты никак не влияет на ширину. Однако в Square метод setHeight() переопределён так, что меняет оба параметра одновременно. Это нарушает третий принцип SOLID: поведение подкласса отличается от поведения базового класса, и такая подстановка приводит к ошибкам.</p>
54 + Ожидаемая площадь: 20, полученная: 20 Ожидаемая площадь: 50, полученная: 100<p>При вызове AreaFixedHeight.getArea(sq) мы наблюдаем неожиданное поведение: метод рассчитан на работу с объектами Rectangle и предполагает, что именение высоты никак не влияет на ширину. Однако в Square метод setHeight() переопределён так, что меняет оба параметра одновременно. Это нарушает третий принцип SOLID: поведение подкласса отличается от поведения базового класса, и такая подстановка приводит к ошибкам.</p>
55 Нарушение LSP: класс Square наследуется от Rectangle, но переопределяет методы так, что ломает ожидаемое поведение<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>В нашем случае Square не должен наследоваться от Rectangle, потому что их поведение различается. Вместо этого лучше создать общий интерфейс Shape и реализовать его отдельно в обоих классах, - так мы избегаем проблем с подстановкой и соблюдаем принцип LSP:</p>
55 Нарушение LSP: класс Square наследуется от Rectangle, но переопределяет методы так, что ломает ожидаемое поведение<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>В нашем случае Square не должен наследоваться от Rectangle, потому что их поведение различается. Вместо этого лучше создать общий интерфейс Shape и реализовать его отдельно в обоих классах, - так мы избегаем проблем с подстановкой и соблюдаем принцип LSP:</p>
56 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; } }<p>Лог в консоли:</p>
56 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; } }<p>Лог в консоли:</p>
57 Площадь: 20 Площадь: 25<p>Теперь Rectangle и Square - независимые классы, каждый со своей реализацией интерфейса Shape. Rectangle свободно управляет шириной и высотой, тогда как Square сохраняет равенство всех сторон.</p>
57 Площадь: 20 Площадь: 25<p>Теперь Rectangle и Square - независимые классы, каждый со своей реализацией интерфейса Shape. Rectangle свободно управляет шириной и высотой, тогда как Square сохраняет равенство всех сторон.</p>
58 Соблюдение LSP: Rectangle и Square реализуют общий интерфейс Shape - подстановка работает корректно, без нарушения поведения<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Суть принципа разделения интерфейсов (ISP) заключается в том, что интерфейсы должны быть узкими и специализированными. Вместо одного большого интерфейса лучше создавать несколько маленьких - каждый со своей задачей. За счёт такого подхода классы могут реализовывать только те методы, что действительно нужны для их работы.</p>
58 Соблюдение LSP: Rectangle и Square реализуют общий интерфейс Shape - подстановка работает корректно, без нарушения поведения<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Суть принципа разделения интерфейсов (ISP) заключается в том, что интерфейсы должны быть узкими и специализированными. Вместо одного большого интерфейса лучше создавать несколько маленьких - каждый со своей задачей. За счёт такого подхода классы могут реализовывать только те методы, что действительно нужны для их работы.</p>
59 <p>Представим, что у нас есть интерфейс Employee, в котором собраны три обязанности сотрудника: работать, есть и отдыхать. Давайте напишем программу:</p>
59 <p>Представим, что у нас есть интерфейс Employee, в котором собраны три обязанности сотрудника: работать, есть и отдыхать. Давайте напишем программу:</p>
60 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("Менеджеры не отдыхают..."); } }<p>Результат выполнения программы:</p>
60 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("Менеджеры не отдыхают..."); } }<p>Результат выполнения программы:</p>
61 Разработчик пишет код... Разработчик обедает... Разработчик отдыхает... Менеджер проводит встречи... Менеджеры не обедают... Менеджеры не отдыхают...<p>Каждый класс, который использует интерфейс Employee, должен описывать все три метода. Представим двух сотрудников:</p>
61 Разработчик пишет код... Разработчик обедает... Разработчик отдыхает... Менеджер проводит встречи... Менеджеры не обедают... Менеджеры не отдыхают...<p>Каждый класс, который использует интерфейс Employee, должен описывать все три метода. Представим двух сотрудников:</p>
62 <ul><li>Разработчик (Developer) - работает, обедает и отдыхает.</li>
62 <ul><li>Разработчик (Developer) - работает, обедает и отдыхает.</li>
63 <li>Менеджер (Manager) - только работает, без обедов и перерывов.</li>
63 <li>Менеджер (Manager) - только работает, без обедов и перерывов.</li>
64 </ul><p>В этом случае Manager всё равно вынужден добавлять лишние методы, которые не используются. Это нарушает принцип разделения интерфейсов.</p>
64 </ul><p>В этом случае Manager всё равно вынужден добавлять лишние методы, которые не используются. Это нарушает принцип разделения интерфейсов.</p>
65 Нарушение ISP: интерфейс Employee включает всё сразу, и приходится реализовывать даже ненужные методы<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Давайте разделим интерфейс Employee на несколько узких интерфейсов - так каждый класс сможет реализовать только нужные ему методы:</p>
65 Нарушение ISP: интерфейс Employee включает всё сразу, и приходится реализовывать даже ненужные методы<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Давайте разделим интерфейс Employee на несколько узких интерфейсов - так каждый класс сможет реализовать только нужные ему методы:</p>
66 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("Менеджер проводит встречи..."); } }<p>Информация в консоли:</p>
66 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("Менеджер проводит встречи..."); } }<p>Информация в консоли:</p>
67 Разработчик пишет код... Менеджер проводит встречи...<p>Теперь интерфейс Employee разделён на три отдельных интерфейса: Workable, Lunchable и Breakable. Получается следующее:</p>
67 Разработчик пишет код... Менеджер проводит встречи...<p>Теперь интерфейс Employee разделён на три отдельных интерфейса: Workable, Lunchable и Breakable. Получается следующее:</p>
68 <ul><li>Developer реализует все три - он работает, обедает и отдыхает.</li>
68 <ul><li>Developer реализует все три - он работает, обедает и отдыхает.</li>
69 <li>Manager реализует только Workable - ничего лишнего.</li>
69 <li>Manager реализует только Workable - ничего лишнего.</li>
70 </ul><p>Этот подход полностью соответствует принципу разделения интерфейсов SOLID: каждый класс выполняет только те методы, что ему действительно нужны, избегая пустых или избыточных реализаций.</p>
70 </ul><p>Этот подход полностью соответствует принципу разделения интерфейсов SOLID: каждый класс выполняет только те методы, что ему действительно нужны, избегая пустых или избыточных реализаций.</p>
71 - Соблюдение ISP: интерфейсы разделены по задачам - каждый класс реализует только нужные методы<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Принцип инверсии зависимостей (DIP) означает, что модули высокого уровня длжны зависеть от абстракций, а не от модулей низкого уровня. Под модулями высокого уровня обычно понимают бизнес-логику приложения - например, управление пользователями или обработку заказов. Модули низкого уровня - это конкретные технические реализации: работа с базой данных, API или файловой системой.</p>
71 + Соблюдение ISP: интерфейсы разделены по задачам - каждый класс реализует только нужные методы<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Принцип инверсии зависимостей (DIP) означает, что модули высокого уровня должны зависеть от абстракций, а не от модулей низкого уровня. Под модулями высокого уровня обычно понимают бизнес-логику приложения - например, управление пользователями или обработку заказов. Модули низкого уровня - это конкретные технические реализации: работа с базой данных, API или файловой системой.</p>
72 <p>Если класс напрямую зависит от другой конкретной реализации, его сложно тестировать и модифицировать. Роберт Мартин<a>пишет</a>:</p>
72 <p>Если класс напрямую зависит от другой конкретной реализации, его сложно тестировать и модифицировать. Роберт Мартин<a>пишет</a>:</p>
73 <p>"Если OCP описывает цель объектно-ориентированной архитектуры, то DIP - это основной механизм её достижения".</p>
73 <p>"Если OCP описывает цель объектно-ориентированной архитектуры, то DIP - это основной механизм её достижения".</p>
74 <p>Эти два принципа тесно связаны: чтобы классы были открыты для расширения (OCP), нужно отказаться от жёстких зависимостей в пользу абстракций (DIP). Представьте себе конструктор LEGO: вместо того чтобы детали были наглухо склеены, они соединяются через стандартные разъёмы - как интерфейсы в коде. Благодаря этому можно легко заменять одни блоки на другие, не ломая всю конструкцию.</p>
74 <p>Эти два принципа тесно связаны: чтобы классы были открыты для расширения (OCP), нужно отказаться от жёстких зависимостей в пользу абстракций (DIP). Представьте себе конструктор LEGO: вместо того чтобы детали были наглухо склеены, они соединяются через стандартные разъёмы - как интерфейсы в коде. Благодаря этому можно легко заменять одни блоки на другие, не ломая всю конструкцию.</p>
75 <p>Пусть у нас есть класс OrderService, который отвечает за обработку заказов и напрямую зависит от конкретной реализации - класса MySQLDatabase:</p>
75 <p>Пусть у нас есть класс OrderService, который отвечает за обработку заказов и напрямую зависит от конкретной реализации - класса MySQLDatabase:</p>
76 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; } }<p>Результат выполнения кода:</p>
76 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; } }<p>Результат выполнения кода:</p>
77 Сохраняем заказ в MySQL: Заказ №1<p>В примере выше OrderService привязан к MySQLDatabase: объект создаётся внутри класса и не может быть подменён. Поэтому, чтобы заменить базу данных или протестировать сервис без реального подключения, придётся менять сам OrderService. Это нарушает принцип DIP, поскольку бизнес-логика зависит от конкретной реализации, а не от абстракции.</p>
77 Сохраняем заказ в MySQL: Заказ №1<p>В примере выше OrderService привязан к MySQLDatabase: объект создаётся внутри класса и не может быть подменён. Поэтому, чтобы заменить базу данных или протестировать сервис без реального подключения, придётся менять сам OrderService. Это нарушает принцип DIP, поскольку бизнес-логика зависит от конкретной реализации, а не от абстракции.</p>
78 Нарушение DIP: OrderService зависит от MySQLDatabase, что затрудняет тестирование и переиспользование кода<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Создадим интерфейс, который будет абстракцией для хранения данных.</p>
78 Нарушение DIP: OrderService зависит от MySQLDatabase, что затрудняет тестирование и переиспользование кода<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><p>Создадим интерфейс, который будет абстракцией для хранения данных.</p>
79 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; } }<p>Сообщение в консоли:</p>
79 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; } }<p>Сообщение в консоли:</p>
80 Сохраняем заказ в MySQL: Заказ №1 Сохраняем заказ в MongoDB: Заказ №2<p>Теперь OrderService зависит не от конкретной реализации репозитория, а от абстракции - интерфейса OrderRepository. Такой подход позволяет подключать разные реализации хранилища данных (например, MySQL, MongoDB и другие) без изменения кода самого сервиса. В этом и заключается принцип инверсии зависимостей: модули высокого уровня должны зависеть от абстракций, а не от конкретных реализаций.</p>
80 Сохраняем заказ в MySQL: Заказ №1 Сохраняем заказ в MongoDB: Заказ №2<p>Теперь OrderService зависит не от конкретной реализации репозитория, а от абстракции - интерфейса OrderRepository. Такой подход позволяет подключать разные реализации хранилища данных (например, MySQL, MongoDB и другие) без изменения кода самого сервиса. В этом и заключается принцип инверсии зависимостей: модули высокого уровня должны зависеть от абстракций, а не от конкретных реализаций.</p>
81 Соблюдение DIP: OrderService зависит от интерфейса OrderRepository, а не от конкретной реализации базы данных<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><a>Курс с трудоустройством: "Профессия Java-разработчик + ИИ" Узнать о курсе</a>
81 Соблюдение DIP: OrderService зависит от интерфейса OrderRepository, а не от конкретной реализации базы данных<em>Изображение:<a>Mermaid Chart</a>/ Skillbox Media</em><a>Курс с трудоустройством: "Профессия Java-разработчик + ИИ" Узнать о курсе</a>