
Если вы не знаете, что такое SOLID, то можете даже не идти на собеседование, да, да, я серьезно ;). Поэтому давайте разбираться:
SOLID — это набор принципов, следуя которым, программный код будет более чистым и гибким. Т.е. это не какае-то библиотека или технология, это просто правила, которым должен следовать любой адекватные разработчик, не зависимо на чем он программирует.
- S — Single-responsibility principle — принцип единой ответственности
- O — Open-closed principle — принцип открытости/закрытости.
- L — Liskov substitution principle — принцип подстановки Барбары Лисков
- I — Interface segregation principle — принцип разделения интерфейса
- D — Dependency Inversion Principle — принцип инверсии зависимостей
Запомнить названия по первости будет сложно, да к этому и не нужно стремиться. Главное понимать содержимое и те идеи, которые предлагаются в каждом из них. Я покажу принципы SOLID на примере языка Java, однако смысл применим к любому языку программирования.
S — Single-responsibility principle
Принцип единой ответственности означает, что один класс или файл должен иметь только одну цель и одно единственное назначение. Вы не имеете права создавать классы и файлы, которые представляют собой «комбайн» умеющий делать все.
Например, если ваш класс создан, что-бы отображать данные на экране, то не нужно размещать в этом классе логику получения этих данных из интернета.
Дело в том, что может получится ситуация, что меняя логику загрузки данных из интернета, вы случайно испортите логику отображения данных на экране. Поэтому работу с интернетом и отображением необходимо разделять на два разных класса.
Также, не забываем, что у вас есть интерфейсы и абстрактные классы, с помощью которых вы можете передавать интерфейс логики с интернетом, в класс для отображения. В итоге реализация интерфейса будет конкретная для конкретного случая, т.е. вам достаточно поменять реализацию интерфейса или абстрактного класса, если логика загрузки данных изменится.
Что-бы проверить, соответствует ли ваш класс этому принципу, задайте себе вопрос: Что может случится, из-за чего мне потребуется изменить данный класс?. Если ответов несколько, значит необходимо разделить класс на несколько.
Советую обратить внимание на следующие приемы, которые помогают соблюдать данный принцип:
- Разработка через тестирование (TDD)
- Паттерн «Выделение класса»(Extract Class)
- Паттерн «Фасад»(Facade)
- Паттерн Паттерн «Прокси»(Proxy)
- Паттерн «DAO»
O — Open-closed principle
Принцип открытости/закрытости означает, что программные сущности(классы, интерфейсы и т. д.) должны быть открыты для расширения, но закрыты для модификации.
Например, у вас есть класс, который выполняет определенные функции. Если вам понадобилось добавить дополнительный функционал, то необходимо создать наследника этого класса или использовать композицию. Но, изменять исходный класс запрещено. Это необходимо, что-бы не испортить код, использующий этот класс.
В ряде случаев рекомендуется избегать наследования и применять композицию, что-бы избежать сложных структур данных и сделать код еще более независимым.
Одним словом, изменять код базового класса строго настрого запрещено!
L — Liskov substitution principle
Принцип подстановки Барбары Лисков, самый не понятный принцип из-за названия :). Но все достаточно просто и немного похоже на предыдущий принцип. Принцип гласит, что поведение методов в дочернем классе должно следовать принципам базового класса, а не изменять их. То есть, дочерний класс переопределяя методы или переменные, не должен менять заложенную логику базового класса.
Например:
class Rectangle {
private int width;
private int height;
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
}
class Square implements Rectangle {
public void setSize(int size) {
super.setWidth(size);
super.setHeight(size);
}
}
Выглядит немного странно, правда? Класс Square(Квадрат) наследуется от Rectangle(прямоугольник), а так как у квадрата все стороны равны, в классе Square мы просто задаем ширину и высоту, одну и ту же. В итоге, мы испортили изначальную идею класса Rectangle, в который заложена логика, что стороны могут отличаться. Получается, не меняя базовый класс, мы умудрились нарушить данный принцип.
В этом примере Square должен быть отдельным классом и ни в коем случае не наследоваться от Rectangle. То есть, поведение методов не должно изменяться. Если написано, что метод возвращает ширину, значит он и должен возвращать ширину, а не что-то другое.
I — Interface segregation principle
Принцип разделения интерфейса. Тут все очень просто. Лучше создавать много отдельных узкоспециализированных интерфейсов, чем один, который включает в себя много функций. Это позволит сделать архитектуру более гибкой, и позволит использовать интерфейсы по отдельности. Этот принцип похож на самый первый, принцип единой ответственности.
Например:
interface ItemClick {
void onClick()
void onLongClick()
}
Итак, есть интерфейс, который требует реализовать два метода: короткое нажатие и длинное нажатие. Но что, если нам необходимо только короткое нажатие? В этом случае у нас будет, что-то такое:
class MyClass implements ItemClick {
void onClick() {
// тут реализуем необходимую логику
}
void onLongClick() {
// а вот тут нам нечего не надо делать, но все равно приходится
// реализовать этот метод потому, что этого требует интерфейс
}
}
Что-бы этого избежать необходимо сделать два разных интерфейса, которые можно применять по отдельности.
interface ItemClick {
void onClick()
}
interface ItemLongClick {
void onLongClick()
}
Так же посмотрите на пример, который был в статье про Композицию, мы создали новый интерфейс, вместо того, чтобы расширять существующий. Это позволяет использовать эти интерфейсы, как отдельно, так и вместе, что делает архитектуру более гибкой и изменяемой. Если бы мы создали новый метод в существующем классе, то нам пришлось бы реализовать его в классе, который его уже использует, а это значит, мы вмешиваемся в существующий код.
D — Dependency Inversion Principle
Модули верхних уровней не должны зависеть от модулей нижних уровней. Если по простому, то нужно делать код так, что-бы этот код имел как можно меньше зависимостей и не было круговых зависимостей, например модуль A зависит от модуля B, а модуль B зависит от модуля A.
Например, у вас есть следующие модули в приложении: presentation(модуль для показа чего либо на экране) и data(модуль для хранения и получения данных). Соответственно presentation будет зависеть от модуля data, так как ему необходимо получать данные для отображения их на экране, но вот модулю data совсем не нужно ничего знать о presentation, ему абсолютно все равно, как эти данные будут отображаться, модуль data просто предоставляет данные.
Обновлено 31 мая 2020
Перевод статьи «The SOLID Principles of Object-Oriented Programming Explained in Plain English».

Принципы SOLID это пять принципов объектно-ориентированного программирования. Это набор правил и наилучших подходов, которым нужно следовать при создании структуры классов.
Эти пять принципов помогают нам понять необходимость определенных шаблонов проектирования и разобраться в архитектуре ПО в целом. Так что я думаю, что эту тему обязательно должен изучить каждый разработчик.
Из этой статьи вы узнаете все необходимое для начала применения принципов SOLID в ваших проектах.
Мы начнем с небольшого погружения в историю самого термина, а затем перейдем к подробностям: всем «почему?» и «как?» каждого принципа. Рассматривать все это будем на примере создания класса и его постепенного улучшения.
Сбегайте за чашкой кофе или чая, и приступим!
Бэкграунд
Принципы SOLID впервые были представлены знаменитым Дядюшкой Бобом — Робертом Мартином — в его работе «Design Principles and Design Patterns» в 2000 году. Но сам акроним SOLID ввел в оборот Майкл Фезерс, и случилось это несколько позже.
Дядюшка Боб также является автором книг-бестселлеров «Чистый код» и «Чистая архитектура», а кроме того он еще и один из членов «Agile Alliance».
В общем, не удивительно, что все эти концепции чистого кода, объектно-ориентированной архитектуры и шаблонов проектирования как-то связаны и взаимно дополняют друг друга.
Все они служат одной цели:
«Создавать понятный, читаемый, тестируемый код, над которым смогут совместно работать многие разработчики».
Давайте рассмотрим принципы SOLID, а заодно расшифруем акроним:
- Single Responsibility Principle («Принцип единой ответственности», SRP)
- Open-Closed Principle («Принцип открытости-закрытости», OCP)
- Liskov Substitution Principle («Принцип подстановки Барбары Лисков», LSP)
- Interface Segregation Principle («Принцип разделения интерфейса», ISP)
- Dependency Inversion Principle («Принцип инверсии зависимостей», DIP)
Принцип единственной ответственности
Принцип единственной ответственности гласит, что класс должен делать какое-то одно действие и, соответственно, для его изменения должна быть только одна причина.
Можно сказать и более «техническим» языком: влиять на спецификацию класса должно только какое-то одно потенциальное изменение в спецификации программы (логика базы данных, логика журналирования и т. п.).
Это означает, что если наш класс — контейнер для данных, скажем, класс Book или Student, и в нем есть какие-то поля, относящиеся к этой сущности, они должны меняться только при изменении модели данных.
Следовать принципу единственной ответственности очень важно. Во-первых, когда несколько разных команд могут работать над одним проектом и редактировать один и тот же класс по многим причинам, это может привести к несовместимости модулей.
Во-вторых, следование этому принципу облегчает контроль версий. Скажем, у нас есть класс для обработки операций базы данных, и мы видим изменение в этом файле в коммитах на GitHub. Если мы последовательно придерживаемся принципа SRP, мы можем быть уверены, что это изменение касается хранения данных или вещей, связанных с базой данных.
Еще пример — конфликты слияния. Они происходят, когда разные команды меняют один и тот же файл. Но если мы следуем SRP, у нас таких конфликтов будет значительно меньше, поскольку файлы могут меняться только по какой-то одной причине. А если конфликты все же будут, их будет легче разрешить.
Распространенные ловушки и антипаттерны
В этом разделе мы рассмотрим некоторые частые ошибки, связанные с принципом единственной ответственности. И, конечно, мы поговорим о том, как такие ошибки исправлять.
Рассмотрим код простой программы для выставления счетов в книжном магазине. Начнем с определения класса Book, который будет использоваться в нашем инвойсе.
class Book {
String name;
String authorName;
int year;
int price;
String isbn;
public Book(String name, String authorName, int year, int price, String isbn) {
this.name = name;
this.authorName = authorName;
this.year = year;
this.price = price;
this.isbn = isbn;
}
}
Это простой класс с несколькими полями, ничего особенного. Я не делаю поля приватными, чтобы нам не пришлось иметь дело с геттерами и сеттерами. Лучше сосредоточимся на логике.
Давайте теперь создадим класс Invoice, где будет содержаться логика для создания инвойса и подсчета общей суммы. Предположим, что наш магазин торгует исключительно книгами.
public class Invoice {
private Book book;
private int quantity;
private double discountRate;
private double taxRate;
private double total;
public Invoice(Book book, int quantity, double discountRate, double taxRate) {
this.book = book;
this.quantity = quantity;
this.discountRate = discountRate;
this.taxRate = taxRate;
this.total = this.calculateTotal();
}
public double calculateTotal() {
double price = ((book.price - book.price * discountRate) * this.quantity);
double priceWithTaxes = price * (1 + taxRate);
return priceWithTaxes;
}
public void printInvoice() {
System.out.println(quantity + "x " + book.name + " " + book.price + "$");
System.out.println("Discount Rate: " + discountRate);
System.out.println("Tax Rate: " + taxRate);
System.out.println("Total: " + total);
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
}
Вот наш класс Invoice. В нем также содержатся несколько полей, касающихся выставления счета, и три метода:
calculateTotal— для подсчета общей суммы,printInvoice— для вывода инвойса в консоль,saveToFile— для записи инвойса в файл.
Прежде чем читать дальше, подумайте, что не так с дизайном этого класса.
Итак, в чем же проблема? Дело в том, что наш класс во многом нарушает принцип единственной ответственности.
Первое нарушение — наш метод printInvoice, содержащий логику вывода. SRP гласит, что наш класс должен иметь единственную причину для изменения. Таковой причиной для нашего класса должно быть изменение системы подсчета.
Но при существующей архитектуре, если мы захотим изменить формат вывода, нам придется изменить наш класс. Вот почему нам не следует смешивать в одном классе логику вывода с бизнес-логикой.
В нашем классе есть еще один метод, нарушающий SRP, — метод saveToFile. Смешивание долгоживущей логики с бизнес-логикой это еще одна распространенная ошибка.
Думайте об этом не только как о записи в файл: это могло бы быть сохранение в базу данных, создание вызова API или еще что-нибудь подобное.
Вы можете спросить, как же нам это все исправить.
Мы можем создать новые классы для вывода и для долгоживущей логики, чтобы нам больше не пришлось изменять класс Invoice по этим причинам.
Мы создаем два класса — InvoicePrinter и InvoicePersistence — и перемещаем туда наши методы.
public 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("Discount Rate: " + invoice.discountRate);
System.out.println("Tax Rate: " + invoice.taxRate);
System.out.println("Total: " + invoice.total + " $");
}
}
public class InvoicePersistence {
Invoice invoice;
public InvoicePersistence(Invoice invoice) {
this.invoice = invoice;
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
}
Теперь наша структура классов подчинена принципу единственной ответственности. Каждый класс отвечает за какой-то единственный аспект приложения. Отлично!
Принцип открытости-закрытости
Принцип открытости-закрытости требует, чтобы классы были открыты для расширения, но закрыты для модификации.
Под модификацией подразумевается изменение кода существующих классов, а под расширением — добавление нового функционала.
В общем, этот принцип имеет в виду следующее: у нас должна быть возможность добавлять новый функционал, не трогая существующий код класса. Это связано с тем, что когда мы модифицируем существующий код, мы рискуем создать потенциальные баги. Поэтому следует по возможности избегать прикасаться к протестированному и надежному (в целом) коду в продакшене.
Но как же добавить новый функционал, не прикасаясь к классу? Обычно это делается при помощи интерфейсов и абстрактных классов.
Теперь, разобравшись с основами этого принципа, давайте применим его на практике — все в том же приложении для создания инвойсов.
Скажем, пришел начальник и сказал, что инвойсы должны сохраняться в базе данных, чтобы их можно было легко найти. Что ж, это должно быть легко!
Мы создаем базу данных, подключаем ее и добавляем сохраняющий метод в наш класс InvoicePersistence:
public class InvoicePersistence {
Invoice invoice;
public InvoicePersistence(Invoice invoice) {
this.invoice = invoice;
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
public void saveToDatabase() {
// Saves the invoice to database
}
}
К сожалению, мы (т. е., ленивый разработчик из книжного магазина) не спроектировали классы таким образом, чтобы в будущем они были легко расширяемыми. Поэтому, чтобы добавить новую фичу, нам нужно модифицировать класс InvoicePersistence.
Если бы наш дизайн подчинялся принципу открытости-закрытости, нам бы не понадобилось изменять этот класс.
Итак, будучи ленивым, но смышленым разработчиком из книжного магазина, мы видим проблему дизайна и решаем произвести рефакторинг кода, чтобы он соответствовал принципу открытости-закрытости.
interface InvoicePersistence {
public void save(Invoice invoice);
}
Мы меняем тип InvoicePersistence на Interface и добавляем метод для сохранения. Таким образом этот метод будет реализован в каждом долгоживущем классе.
public class DatabasePersistence implements InvoicePersistence {
@Override
public void save(Invoice invoice) {
// Save to DB
}
}
public class FilePersistence implements InvoicePersistence {
@Override
public void save(Invoice invoice) {
// Save to file
}
}
Теперь наша классовая структура выглядит следующим образом:

Наша долгоживущая логика легко расширяема. Если босс попросит нас добавить еще одну базу данных, причем одна из баз будет MySQL, а другая — MongoDB, мы сможем сделать это с легкостью.
Вы, возможно, думаете, что мы могли бы просто создать несколько классов, обойдясь без интерфейса, и добавить сохраняющий метод в каждый из них.
Но представьте, что мы расширим наше приложение, и у нас появится несколько долгоживущих классов, например, InvoicePersistence, BookPersistence, и мы создадим класс PersistenceManager для управления всеми долгоживущими классами:
public class PersistenceManager {
InvoicePersistence invoicePersistence;
BookPersistence bookPersistence;
public PersistenceManager(InvoicePersistence invoicePersistence,
BookPersistence bookPersistence) {
this.invoicePersistence = invoicePersistence;
this.bookPersistence = bookPersistence;
}
}
Теперь при помощи полиморфизма мы можем передать в этот класс любой класс, реализующий интерфейс InvoicePersistence. Именно эту гибкость и обеспечивают интерфейсы.
Принцип подстановки Барбары Лисков
Принцип подстановки Барбары Лисков гласит, что подклассы должны заменять свои базовые классы.
Это означает следующее. Если у нас есть класс B, являющийся подклассом класса A, у нас должна быть возможность передать объект класса B любому методу, который ожидает объект класса A, причем этот метод не должен выдать в таком случае какой-то странный output.
Это ожидаемое поведение, потому что когда мы используем наследование, мы предполагаем, что дочерний класс наследует все, что есть у суперкласса. Дочерний класс расширяет поведение, но никогда не сужает его.
Если класс не подчиняется принципу подстановки Барбары Лисков, это приводит к неприятным ошибкам, которые трудно обнаружить.
Принцип Лисков понять легко, но разглядеть в коде трудно. Давайте посмотрим пример.
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;
}
}
У нас есть простой класс Rectangle и функция getArea, возвращающая площадь прямоугольника.
Мы решили создать другой класс — для квадратов. Вы наверняка знаете, что квадрат это частный случай прямоугольника, в котором ширина равна высоте.
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);
}
}
Наш класс Square расширяет класс Rectangle. Мы устанавливаем в конструкторе одинаковое значение высоты и ширины, но мы не хотим, чтобы какой-либо клиент (кто-то, использующий наш класс в своем коде) менял высоту или ширину, нарушая свойство квадрата.
Поэтому мы переопределяем сеттеры, чтобы устанавливать оба свойства при изменении любого из них. Но этим мы нарушили принцип подстановки Лисков.
Давайте создадим основной класс для тестирования функции getArea.
class Test {
static void getAreaTest(Rectangle r) {
int width = r.getWidth();
r.setHeight(10);
System.out.println("Expected area of " + (width * 10) + ", got " + r.getArea());
}
public static void main(String[] args) {
Rectangle rc = new Rectangle(2, 3);
getAreaTest(rc);
Rectangle sq = new Square();
sq.setWidth(5);
getAreaTest(sq);
}
}
Тестировщик в вашей команде создал тестирующую функцию getAreaTest и говорит, что ваша функция getArea не проходит тест для квадратных объектов.
В первом тесте мы создаем прямоугольник с шириной 2 и высотой 3, а затем вызываем getAreaTest. Результат, как и ожидалось, 20. Но когда мы переходим к квадрату, что-то идет не так. Это происходит потому, что вызов функции setHeight в тесте устанавливает также ширину, и результат получается не тот, что ожидался.
Принцип разделения интерфейса
Разделение подразумевает, что вещи нужно хранить отдельно друг от друга, а принцип разделения интерфейса касается (сюрприз!) разделения интерфейсов.
Этот принцип гласит: много клиентоориентированных интерфейсов лучше, чем один интерфейс общего назначения. Клиенты не должны принуждаться к реализации функций, которые им не нужны.
Этот принцип прост как для понимания, так и для применения, так что давайте рассмотрим пример.
public interface ParkingLot {
void parkCar(); // Decrease empty spot count by 1
void unparkCar(); // Increase empty spots by 1
void getCapacity(); // Returns car capacity
double calculateFee(Car car); // Returns the price based on number of hours
void doPayment(Car car);
}
class Car {
}
Мы смоделировали очень упрощенную парковку. Это такая парковка, где вы платите почасово. Теперь представьте, что мы хотим реализовать бесплатную парковку.
public class FreeParking implements ParkingLot {
@Override
public void parkCar() {
}
@Override
public void unparkCar() {
}
@Override
public void getCapacity() {
}
@Override
public double calculateFee(Car car) {
return 0;
}
@Override
public void doPayment(Car car) {
throw new Exception("Parking lot is free");
}
}
Интерфейс нашей парковки состоит из двух частей: логики, касающейся парковки (паркующаяся машина, уезжающая машина, получение информации о вместимости), и логики, касающейся оплаты.
Но это слишком специфично. Из-за этого наш класс FreeParking вынужден реализовывать методы, касающиеся оплаты, а эти методы для него нехарактерны. Давайте разделим интерфейсы.

Теперь мы разделили нашу парковку. С нашей новой моделью мы можем пойти еще дальше: разделить PaidParkingLot, чтоб поддерживать разные типы оплаты.
Теперь наша модель куда более гибкая и расширяемая. Клиентам не нужно реализовывать никакую нерелевантную логику, потому что в интерфейсе парковки мы предоставили только функционал, касающийся самой парковки.
Принцип инверсии зависимостей
Принцип инверсии зависимостей гласит, что наши классы должны зависеть от интерфейсов или абстрактных классов, а не от конкретных классов и функций.
В своей статье (2000 год) Дядюшка Боб подытожил формулировку этого принципа так:
«Если OCP обозначает цель объектно-ориентированной архитектуры, то DIP — основной механизм».
(OCP — принцип открытости-закрытости, а DIP — принцип инверсии зависимостей, — прим. ред. Techrocks).
Эти два принципа, безусловно, связаны друг с другом, и мы уже применяли этот шаблон, когда обсуждали принцип открытости-закрытости.
Нам нужно, чтобы наши классы были открыты для расширения, поэтому мы реорганизовали наши зависимости, чтобы зависеть от интерфейсов, а не от конкретных классов. Наш класс PersistenceManager зависит от InvoicePersistence, а не от классов, реализующих этот интерфейс.
Заключение
В этой статье мы познакомились с историей принципов SOLID, после чего попытались разобраться в «почему?» и «как?» каждого принципа. Мы даже провели рефакторинг простенького приложения для выставления счетов, чтобы его код подчинялся принципам SOLID.
Не забывайте об этих принципах, когда проектируете, пишете и изменяете свой код. Благодаря этому он станет более чистым, расширяемым и тестируемым.

Классы являются строительными блоками любого Java-приложения. Если эти блоки не сильны, здание (то есть приложение) столкнется с трудным периодом в будущем. На самом деле это означает, что когда область применения расширяется или приложение сталкивается с некоторыми проблемами проектирования при производстве или обслуживании, не очень хорошо написанное может привести к очень сложным ситуациям.
С другой стороны, набор тщательно разработанных и написанных классов может ускорить скачки в процессе кодирования, уменьшая при этом количество ошибок.
В этом уроке мы будемиспользование5 самых рекомендуемых принципов дизайнаПримерыОбсуждатьПринцип SOLID в Java,Мы должны помнить эти принципы при написании уроков. Они также составляют то, что вы должны следовать при разработке классов приложенийЛучшие практики。
оглавление
1. Принцип единоличной ответственности
2. Открытый и закрытый принцип
3. Лисковский принцип замещения
4. Принцип изоляции интерфейса
5. Принцип обращения зависимостей

5 принципов проектирования Java-классов
Давайте углубимся в них один за другим.
1. Принцип единоличной ответственности
Название принципа говорит само за себя:
«У класса должна быть только одна ответственность»
Другими словами, мы должны писать, изменять и поддерживать класс только для одной цели. Если это модельный класс, то он должен строго представлять только одного участника / сущность. Это позволит нам гибко вносить изменения в будущем, не беспокоясь о влиянии этих изменений на другую организацию.
Точно так же, если мы пишем класс service / manager, он должен содержать только часть вызова метода и ничего больше. Даже не полезные глобальные функции, связанные с модулями. Лучше разделить их в другом глобально доступном файле класса. Это поможет сохранить класс для конкретной цели, и мы можем только определить видимость класса для определенного модуля.
1.1. Пример принципа единой ответственности
Мы можем использовать большое количество классов принципа единой ответственности во всех популярных библиотеках Java. Например, в log4j у нас есть разные классы и методы ведения журнала, разные классы имеют уровни ведения журнала и так далее.
В нашем коде уровня приложения мы определяем классы моделей для представления объектов реального времени, таких как люди, сотрудники, учетные записи и т. Д. Большинство из этих классов являются примерами принципов SRP, потому что когда нам нужно изменить статус человека, мы изменяем только класс человека. и многое другое.
В приведенном примере у нас есть два классаPersonс участиемAccount, Оба несут единоличную ответственность за хранение своей конкретной информации. Если мы хотим изменить состояние Person, нам не нужно изменять классAccount,наоборот.
|
|
|
|
2. Открытый и закрытый принцип
Это второе важное правило, которое мы должны учитывать при разработке приложений.Открытый и закрытый принцип:
«Программный компонент должен быть расширяемым, но закрытым для модификации»
Что это означает? ? Это означает, что наш класс должен быть спроектирован таким образом, чтобы всякий раз, когда разработчики хотят изменить поток управления при определенных условиях в приложении, они должны расширять наш класс и покрывать некоторые функции, вот и все.
Если другие разработчики не могут спроектировать требуемое поведение из-за ограничений нашего класса, мы должны пересмотреть вопрос об изменении нашего класса. Я не говорю, что кто-то может изменить всю логику нашего класса, но он / она должен иметь возможность переопределить параметры, предоставляемые программным обеспечением, безопасным способом, который позволяет программное обеспечение.
2.1. Откройте пример закрытого принципа
Если мы посмотрим на хороший фреймворк, такой как Struts или Spring, мы обнаружим, что мы не можем изменить их основную логику и обработку запросов, а просто изменим необходимый поток приложения, расширив некоторые классы и вставив их в файл конфигурации.
Например, весенний каркас имеет классыDispatcherServlet, Этот класс действует как строковое веб-приложениеФронт контроллер, Чтобы использовать этот класс, нам не нужно изменять этот класс. Все, что нам нужно, это передать параметры инициализации, и мы можем расширить его функциональные возможности так, как мы хотим.
Обратите внимание, что в дополнение к передаче параметров инициализации во время запуска приложения, мы также можем переопределить методы, расширив класс для изменения поведения целевого класса. Например, класс Action Struts был расширен для охвата логики обработки запросов.
|
|
3. Лисковский принцип замещения
Этот принцип является модификацией ранее обсужденного принципа открытого и закрытого. это говорит:
«Производный тип должен полностью заменить его базовый тип»
Это означает, что разработчики классов, созданные путем расширения нашего класса, должны иметь возможность адаптироваться к приложению без сбоев. Это требует, чтобы объекты подклассов вели себя так же, как объекты суперклассов. В основном это происходит, когда мы выполняем распознавание типов во время выполнения и затем преобразуем его в соответствующий ссылочный тип.
3.1. Пример принципа подстановки Лискова
Примером LSP может быть в рамках SpringРедактор пользовательских свойств, Spring предоставляет редактор свойств для представления свойств, отличных от самого объекта, например, для анализа удобочитаемого ввода данных из параметров запроса HTTP или отображения понятных человеку значений чистых объектов Java, например, на уровне представления.CurrencyилиURL。
Spring может зарегистрировать редактор атрибутов для типа данных и должен следоватьБазовый классОграниченияPropertyEditorSupport, Так что любое расширение классаPropertyEditorSupportКласс, тогда его можно заменить всеми необходимыми базовыми классами.
Например, номер ISBN каждой книги всегда является фиксированным форматом отображения. Вы можете представлять ISBN отдельно в базе данных и пользовательском интерфейсе. Для этого требования мы можем написать редактор атрибутов следующим образом:
|
|
4. Принцип изоляции интерфейса
Этот принцип мой любимый. Это относится к интерфейсам, потому что принцип единой ответственности применяется к классам. Интернет-провайдер говорит:
«Клиентов не следует заставлять внедрять ненужные методы, которые они не будут использовать»
например. Интерфейс создан разработчиком AlexReportableИ добавить два методаgenerateExcel()с участиемgeneratedPdf(), Теперь customer’A ‘хочет использовать этот интерфейс, но он намерен использовать только отчеты в формате PDF, но не Excel. Может ли он легко использовать эту функцию?
Нет. Он должен будет реализовать эти два метода, одним из которых является дополнительное бремя, налагаемое на него разработчиком программного обеспечения. Он либо реализует другой метод, либо оставит его пустым. Это не хороший дизайн.
Так в чем же решение? Решение состоит в том, чтобы создать два интерфейса, ломая существующие интерфейсы. Они должны быть какPdfReportableс участиемExcelReportable, Это даст пользователям возможность использовать только необходимые функции.
4.1. Пример принципа изоляции интерфейса
Лучшее место для поиска примеров IPS — обработчик событий Java AWT, который обрабатывает события GUI, запускаемые с клавиатуры и мыши. У него разные классы слушателей для каждого события. Нам нужно только написать обработчики для событий, мы хотим обработать их. незачем.
Некоторые слушатели
- FocusListener
- KeyListener
- MouseMotionListener
- MouseWheelListener
- TextListener
- WindowFocusListener
В любое время мы хотим обработать любое событие, просто найти соответствующего слушателя и реализовать его.
|
|
5. Положитесь на принцип инверсии
Большинству из нас уже знакомы слова, используемые в основных именах. Принцип DI гласит:
«Зависит от абстракции, а не от туберкулеза»
Перефразируй. Мы должны спроектировать наше программное обеспечение таким образом, чтобы уровень абстракции использовался для отделения различных модулей друг от друга, чтобы связать их вместе.
5.1. Пример принципа обращения зависимостей
bean configurationЭтот принцип используется классически в среде Spring.
В платформе Spring все модули предоставляются как отдельные компоненты, и они могут работать вместе, просто внедряя зависимости в другие модули. Эта зависимость управляется извне в файле XML.
Эти независимые компоненты очень замкнуты в своих границах, и мы можем легко использовать их в программных модулях, отличных от пружин. Это достигается за счет использования принципов инверсии, открытости и замыкания. Все модули предоставляют только абстракции, что полезно для расширения функций или плагинов в другом модуле.
ЭтиПять принципов дизайна, Также известный какТВЕРДЫЙ принцип, Что позволяет следовать рекомендациям при разработке наших классов приложений.
S – The Single Responsibility Principle
Название: Принцип единственной ответственности.
Определение: У класса/модуля должна быть лишь одна причина для изменения.
Смысл принципа: Борьба со сложностью, важность которой резко возрастает при развитии логики приложения.
Краткое описание: Любой сложный класс должен быть разбит на несколько простых составляющих, отвечающих за определенный аспект поведения, что упрощает как понимание, так и будущее развитие.
Типовые примеры нарушения: 1) смешивание логики и инфраструктуры: бизнес-логика смешана с представлением, слоем персистентности, находится внутри WCF или windows-сервисов и т.п. 2) класс/модуль решает задачи разных уровней абстракции: вычисляет CRC и отправляет уведомления по электронной почте; разбирает json-объект и анализирует его содержимое и т.п.
Anti-SRP – Принцип размытой ответственности. Чрезмерная любовь к SRP ведет к обилию мелких классов/методов и размазыванию логики между ними.
O – The Open-Closed Principle
Название: Принцип Открыт-Закрыт
Определение: Программные сущности (классы, модули, функции и т.п.) должны быть открытыми для расширения, но закрытыми для модификации.
Смысл: ограничить распространение изменений минимальным числом классов/модулей; позволить вести параллельную разработку путем фиксации интерфейсов классов и открытости реализаций.
Краткое описание: закрытость модулей означает стабильность интерфейса и возможность использования классов/модулей клиентами. Открытость модулей означает возможность внесения изменений в поведении, путем изменения реализации или же путем переопределения поведения в наследниках. Борьба с изменениями заключается в ограничении количества изменений минимальным числом классов/модулей и не подразумевает возможность изменения поведения без перекомпиляции. На практике требуемая «гибкость» обеспечивается за счет наследования и сопоставления с образцом (pattern matching), в зависимости от того, какую операцию мы хотим упростить – добавление нового подтипа в иерархию наследования или добавление новой операции в семейство типов.
Типичные примеры нарушения: размазывание информации об иерархии типов по всему приложению.
Anti-OCP – Принцип фабрики-фабрик: Чрезмерная любовь к OCP ведет к переусложненным решениям с чрезмерным числом уровней абстракции.
L – The Liskov Substitution Principle
Название: Принцип замещения Барбары Лисков
Определение: Должна быть возможность вместо базового типа подставить любой его подтип.
Смысл: Реализуйте наследование подтипов правильно.
Краткое описание: для корректной реализации отношения «ЯВЛЯЕТСЯ», наследник может ослаблять предусловие и усиливать постусловие (требовать меньше и гарантировать больше), при этом инварианты базового класса должны выполняться наследником. При нарушении этих правил подстановка экземпляров наследника в метод, принимающий базовый класс будет приводить к непредсказуемым последствиям.
Типичные примеры нарушения: несогласованное поведение наследников, что приводит к необходимости приводить экземпляры базового класса к конкретным типам наследников.
Anti-LSP – Принцип непонятного наследования. Данный анти-принцип проявляется либо в чрезмерном количестве наследования, либо в его полном отсутствии, в зависимости от опыта и взглядов местного главного архитектора
I – Interface Segregation Principle
Название: Принцип разделения интерфейсов
Определение: клиенты не должны вынужденно зависеть от методов, которыми не пользуются.
Смысл: класс должен предоставлять удобный интерфейс с точки зрения его разнообразных клиентов.
Краткое описание: интерфейс класса должен быть цельным и согласованным не зависимо от числа клиентов. Несколько разных клиентов вполне могут использовать лишь подмножество методов класса, до тех пор, пока интерфейс класса будет оставаться согласованным. Проблемы появляются тогда, когда интерфейс класса начинает распухать или появляются разные методы с похожей семантикой лишь для того, чтобы ими было удобно пользоваться определенным клиентам.
Типичные примеры нарушения: 1) класс или интерфейс содержит несколько методов со схожей семантикой, которые используются разными клиентами; 2) интерфейс класса слишком разнороден и содержит методы, отвечающие за слабосвязанные операции.
Anti-ISP – Принцип тысячи интерфейсов. Интерфейсы классов разбиваются на слишком большое число составляющих, что делает их неудобными для использования всеми клиентами.
D – The Dependency Inversion Principle
Название: Принцип инверсии зависимостей
Определение: Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те и другие должны зависеть от абстракций.
Смысл: сделать ключевые и/или изменчивые зависимости класса явными.
Краткое описание: слишком большое число зависимостей класса говорит о проблемах в дизайне. Возможно класс делает слишком многое, или же текущий класс не удачен, что приводит к необходимости дергания по одному методу у слишком большого числа зависимостей. Любой объектный дизайн представляет собой некоторый граф взаимодействующих объектов, при этом некоторые зависимости являются частью реализации и должны создаваться напрямую (композиция), а некоторые – передаваться ему извне (агрегация). Выделять зависимости особенно полезно, когда они являются изменчивыми (завязаны на окружения), или же представляют собой некоторую форму «стратегий».
Типичные примеры нарушения: использование синглтонов, сервис-локаторов или же создание ключевых зависимостей класса по ходу дела в закрытых методах.
Anti-DIP – Принцип инверсии сознания или DI-головного мозга. Интерфейсы выделяются для каждого класса и пачками передаются через конструкторы. Понять, где находится логика становится практически невозможно.
Цикл статей по SOLID принципам
SOLID
Введение
Важным шагом к пониманию того, как писать правильный, аккуратный и чистый код является понимание принципов SOLID.
Итак, SOLID — это акроним, каждой букве соответствует свой принцип:
- S — SRP — Single responsibility principle
- O — OCP — Open closed principle
- L — LSP — Liskov substitution principle
- I — ISP — Interface segregation principle
- D — DIP — Dependency inversion principle
В полезных ссылках вы найдете примеры кода по каждому принципу.
Single Responsibility Principle — SRP
A class should have one, and only one, reason to change.
Класс должен иметь только одну причину для изменения.
Каждый класс должен иметь одну обязанность(ответственность за что-то) и эта обязанность должна быть полностью инкапсулирована в классе.
Для примера рассмотрим сервис, который занимается составлением и рассылкой отчетов.
Объединив все эти задачи в один класс мы полностью решим поставленную задачу по написанию нашего сервиса.
Такой класс будет иметь сразу несколько областей ответственности, а значит мы будем вынуждены вносить правки по каждому из перечисленных случаев:
- Изменяется содержимое отчета
- Изменяется формат отчета
- Изменяется способ рассылки отчета
Из-за того, что класс ответственен сразу за несколько задач в дальнейшем могут возникнуть сложности в поддержке работоспособности кода, в гибкости использования сервиса и т.д.
SRP говорит как раз о том, что надо разделить такой класс на несколько классов, чтобы каждый класс был ответственен за свою область.
Один класс отвечает за подготовку отчета, другой — за рассылку.
В таком случае наш сервис сначала запрашивает отчет у класса, который занимается отчетами, а после просит класс-отправитель осуществить рассылку.
Следование SRP принципу даст нам слабо связанное приложение, которое будет легко изменять и дорабатывать в дальнейшем.
Делегируйте ответственность!
И не забывайте: KISS!
Open/Closed Principle
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification
Программные сущности(классы, модули, функции и т.д.) должны быть открыты для расширения, но закрыты для изменения.
Т.е класс должен быть закрыт к изменению извне, но при этом — должен иметь возможности к расширению реализации.
В качестве примера рассмотрим класс Greeter, отвечающий за приветствия:
public class Greeter { String formality; public String greet() { if (this.formality == "formal") { return "Good evening, sir."; } else if (this.formality == "casual") { return "Sup bro?"; } else if (this.formality == "intimate") { return "Hello Darling!"; } else { return "Hello."; } } public void setFormality(String formality) { this.formality = formality; } }
В зависимости от ‘формальности’ мы подбираем приветствие.
Чем плох данный код?
Тем, что при добавлении нового приветствия необходимо изменять код Greeter, класс не открыт для расширения.
В то же время, если постараться сделать его более расширяемым можно ввести понятие интерфейса Personality, отвечающего за то, какое приветствие будет:
public interface Personality { public String greet(); }
И класс Greeter будет выглядеть уже как:
public class Greeter { private Personality personality; public Greeter(Personality personality) { this.personality = personality; } public String greet() { return personality.greet(); } }
Реализации интерфейса Personality:
public class FormalPersonality implements Personality { public String greet() { return "Good evening, sir."; } }
или
public class IntimatePersonality implements Personality { public String greet() { return "Hello Darling!"; } }
Класс Greeter спроектирован так, что открыт для расширения, мы можем передать любой Personality, в зависимости от задачи. И получим необходимый результат.
При этом сам код Greeter не изменяется.
Т.е классы и модули должны проектироваться так, чтобы для изменения их поведения, нам не нужно было изменять их исходный код.
В этом и состоит суть OCP принципа.
Liskov Substitution Principle
Этот принцип имеет сложное математическое определение, которое можно заменить на:
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
Или, если совсем упростить:
Объекты могут быть заменены их наследниками без изменения свойств программы.
В качестве примера рассмотрим классы Прямоугольник и Квадрат.
Класс Прямоугольник имеет методы, устанавливающие ширину и длину.
Для переиспользования кода класс Квадрат наследуется от Прямоугольника и переопределяет методы, определяющие ширину и длину, так, что любое изменение длины изменяет также и ширину, и наоборот.
Т.е выставляем всегда одинаковые значения для ширины и длины у класса Квадрата.
class Rectangle { private int height; private int width; public void setHeight(int height) { this.height = height; } public void setWidth(int width) { this.width = width; } } class Square extends Rectangle { @Override public void setHeight(int height) { this.height = height; this.width = height; } @Override public void setWidth(int width) { this.width = width; this.height = width; } }
На первый взгляд все отлично, но это явное нарушение Liskov Substitution Principle.
Почему? Потому что в таком случае нарушается поведение базового класса!
При использовании такого наследования Квадрат можно использовать везде, где используется родительский класс Прямоуголник.
Так вот там, где мы используем Квадрат в качестве Прямоугольник-а никто не ожидает, что при изменении длины вдруг изменится и ширина, ведь поведение базового класса нарушено.
Есть два варианта решения возникшей проблемы:
- Сделать два независимых класса.
- Сделать абстрактный класс
ГеометрическаяФигура, от которого отнаследоваться иКвадратом, иПрямоугольником.
Применять
наследованиев примере выше вообще говоря довольно плохая идея, хотя бы потому, что наследование — этоis aотношение.
АКвадратне являетсяПрямоугольником, также как иПрямоугольникне являетсяКвадратом.
Можно привести еще более простой пример, для демонстрации важности LSP.
Рассмотрим класс java.util.ArrayList и отнаследуемся от него, при этом переопределив методы так, что индексы элементов будут считаться не с 0, как обычно, а с 1.
Нарушение поведения родительского класса влечет за собой масштабные проблемы при работе с таким кодом, так как теперь, в зависимости от реализации, индексы элементов считаются по разному.
Interface Segregation Principle
Many client-specific interfaces are better than one general-purpose interface.
Принцип разделения интерфейсов говорит о том, что при проектировании интерфейсов необходимо придерживаться минимализма.
А слишком «толстые» интерфейсы необходимо разделять(разбивать) на более мелкие и более специфичные.
Тот, кто использует интерфейс должен знать только о методах, которые необходимы им в работе и не более того.
При изменении какого-либо метода интерфейса не должны меняться клиенты, которые этот метод не используют.
Для примера рассмотрим интерфейс ReportGenerator:
interface ReportGenerator { String generate(); String generateXml(); String generateJson(); }
Классам, реализующим такой интерфейс, потребуется переопределить все перечисленные способы генерации отчетов.
В то время как некоторым из них может вообще не потребоваться возможность генерации отчета в xml или в json.
Но с нашим ‘толстым’ интерфейсом мы не предоставляем никакого выбора.
Поэтому такой интерфейс лучше разбить на несколько, XmlReportGenerator и JsonReportGenerator и PlainReportGenerator.
И предоставлять разработчику возможность выбора, где какая генерация необходима. И там, где это необходимо реализовывать эти специфичные интерфейсы.
Следование этому принципу позволит писать более гибкий и проще поддерживаемый код.
Внимательный читатель должен уже провести аналогию с SRP!
Dependency Inversion
Depend on abstractions, not on concretions.
-
Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
-
Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Старайтесь, чтобы различные модули были автономными, и соединялись друг с другом с помощью абстракций.
Модуль верхнего уровня — модуль, работающий с бизнес-логикой.
Чем ближе модуль к вводу/выводу, тем ниже уровень модуля.Например, работа с БД — модуль более низкого уровня, чем модуль, работающий с бизнес-логикой пользователя.
Идея состоит в том, что разделяя уровни бизнес-логики и модули нижних уровней(например, работа с БД), вы в дальнейшем сможете поменять реализацию модуля нижнего уровня без изменения кода бизнес-логики.
Это достигается, если следовать OCP и LSP.
Разберем следующий пример:
public class WeatherTracker { String currentConditions; Phone phone; Emailer emailer; public WeatherTracker() { phone = new Phone(); emailer = new Emailer(); } public void setCurrentConditions(String weatherDescription) { this.currentConditions = weatherDescription; if (weatherDescription == "rainy") { String alert = phone.generateWeatherAlert(weatherDescription); System.out.print(alert); } if (weatherDescription == "sunny") { String alert = emailer.generateWeatherAlert(weatherDescription); System.out.print(alert); } } }
В зависимости от описания погоды выбирается тип оповещения.
Так вот текущая релизация завязана на реализации phone и emailer, что не дает нам гибкости в использовании.
Чтобы исправить текущий недостаток необходимо выделить уровень абстракции, от которой будут зависеть реализации.
В таком случае код будет выглядеть в виде:
interface Notifier { public void alertWeatherConditions(String weatherConditions); } public class MobileDevice implements Notifier { public void alertWeatherConditions(String weatherConditions) { if (weatherConditions == "rainy") System.out.print("It is rainy"); } } public class EmailClient implements Notifier { public void alertWeatherConditions(String weatherConditions) { if (weatherConditions == "sunny"); System.out.print("It is sunny"); } } public class WeatherTracker { String currentConditions; public void setCurrentConditions(String weatherDescription) { this.currentConditions = weatherDescription; } public void notify(Notifier notifier) { notifier.alertWeatherConditions(currentConditions); } }
В текущей реализации как раз детали завияст от абстракции и модуль верхнего уровня не зависит от модулей нижнего уровня(реализации Notifier).
Дополнение
Есть еще полезные принципы, о которых также следует поговорить.
KISS
Keep it simple, stupid.
KISS — это принцип проектирования и программирования, при котором простота системы декларируется в качестве основной цели или ценности.
Основной посыл KISS в том, что не имеет смысла реализовывать дополнительные функции и модули, которые не нужны или их использование крайне маловероятно.
Также, не стоит перегружать интерфейс теми опциями, которые не нужны большинству пользователей.
В простоте — сила.
Стоит отметить еще то, что надо опасаться неограниченно и бесконтрольно увеличивать уровень абстракций, так как это может отразиться на увеличении сложности архитектуры приложения.
Не стоит также закладывать избыточные функции «про запас», так как к моменту, когда понадобится этот функционал либо изменятся требования, либо этот момент может и вовсе никогда не наступить.
Еще одним важным моментом является контроль за зависимостями проекта.
Не стоит тянуть в зависимостях ‘огромную’ библиотеку, если вам от неё нужна лишь пара функций.
Стремитесь декомпозировать сложную задачу на простые составляющие.
DRY
Don’t repeat yourself.
Стоит избегать дублирования кода.
Если ваш код не дублируется, то его изменение и модификация будет происходить всегда в одном месте, что сократит количество ошибок, упростит тестирование и улучшит понимаение.
В противном случае вы обрекаете ваш продукт на ужасные муки при тестировании и внесении нового функционала.
YAGNI
You aren’t gonna need it.
Старайтесь избегать излишних абстракций, обходите стороной желания эксперимента ‘из интереса’ и не делайте реализации функционала, который сейчас не нужен, но, по вашему мнению, может либо вскоре понадобиться, либо просто будет полезен.
Как уже было сказано в KISS, в реальности функционал ‘про запас’ часто оказывается либо не нужен, либо не готов к текущим реалиям и требует доработок, что сводит ваши усилия на нет.
AHA
Avoid Hasty Abstractions
Принцип AHA (Avoid Hasty Abstractions) практически всегда должен быть превыше DRY. Если что-то может быть написано более просто, без дополнительных абстракций, то лучше написать это так, даже, если понадобится задублировать какой-то код. Так как лучше задублировать какой-то код, чем нагородить нелепых абстракций во имя переиспользования.
Полезные ссылки
- SOLID Примеры
- Подробно о каждом из принципов
Принципы из дополнения:
- YAGNI
- KISS
- DRY
Принципы SOLID в картинках +49
Совершенный код, Блог компании Цифровые Экосистемы
Рекомендация: подборка платных и бесплатных курсов создания сайтов — https://katalog-kursov.ru/

Если вы знакомы с объектно-ориентированным программированием, то наверняка слышали и о принципах SOLID. Эти пять правил разработки ПО задают траекторию, по которой нужно следовать, когда пишешь программы, чтобы их проще было масштабировать и поддерживать. Они получили известность благодаря программисту Роберту Мартину.
В Сети множество отличных статей, где рассказывается о принципах SOLID, но иллюстрированных среди них мне практически не попадалось. Из-за этого таким людям со склонностью к визуальному восприятию информации – таким, как я – бывает сложно схватывать суть и не отвлекаться.
Основная цель этой статьи – лучше усвоить принципы SOLID через отрисовку иллюстраций, а также определить назначение каждого принципа. Дело в том, что некоторые из принципов кажутся похожими, но функции выполняют разные. Может получиться так, что одному принципу следуешь, а другой при этом нарушаешь, хотя с виду особой разницы между ними нет.
Чтобы проще читалось, я упоминаю здесь только классы, однако всё сказанное в статье применимо также к функциям, методам и модулям, так что имейте это в виду.
Ну, приступим.
Принципы SOLID
S – Single Responsibility (Принцип единственной ответственности)
Каждый класс должен отвечать только за одну операцию.

Если класс отвечает за несколько операций сразу, вероятность возникновения багов возрастает – внося изменения, касающиеся одной из операций вы, сами того не подозревая, можете затронуть и другие.
Назначение
Принцип служит для разделения типов поведения, благодаря которому ошибки, вызванные модификациями в одном поведении, не распространялись на прочие, не связанные с ним типы.
O — Open-Closed (Принцип открытости-закрытости)
Классы должны? быть?открыты?для?расширения,?но?закрыты?для?модификации.

Когда вы меняете текущее поведение класса, эти изменения сказываются на всех системах, работающих с данным классом. Если хотите, чтобы класс выполнял больше операций, то идеальный вариант – не заменять старые на новые, а добавлять новые к уже существующим.
Назначение
Принцип служит для того, чтобы делать поведение класса более разнообразным, не вмешиваясь в текущие операции, которые он выполняет. Благодаря этому вы избегаете ошибок в тех фрагментах кода, где задействован этот класс.
L — Liskov Substitution (Принцип подстановки Барбары Лисков)
Если П является подтипом Т, то любые объекты типа Т, присутствующие в программе, могут заменяться объектами типа П без негативных последствий для функциональности программы.

В случаях, когда класс-потомок не способен выполнять те же действия, что и класс-родитель, возникает риск появления ошибок.
Если у вас имеется класс и вы создаете на его базе другой класс, исходный класс становится родителем, а новый – его потомком. Класс-потомок должен производить такие же операции, как и класс-родитель. Это называется наследственностью.
Необходимо, чтобы класс-потомок был способен обрабатывать те же запросы, что и родитель, и выдавать тот же результат. Или же результат может отличаться, но при этом относиться к тому же типу. На картинке это показано так: класс-родитель подаёт кофе (в любых видах), значит, для класса-потомка приемлемо подавать капучино (разновидность кофе), но неприемлемо подавать воду.
Если класс-потомок не удовлетворяет этим требованиям, значит, он слишком сильно отличается от родителя и нарушает принцип.
Назначение
Принцип служит для того, чтобы обеспечить постоянство: класс-родитель и класс-потомок могут использоваться одинаковым образом без нарушения работы программы.
I — Interface Segregation (Принцип разделения интерфейсов)
Не следует ставить клиент в зависимость от методов, которые он не использует.

Когда классу приходится производить действия, не несущие никакой реальной пользы, это выливается в пустую трату ресурса, а в случае, если класс выполнять эти действия не способен, ведёт к возникновению багов.
Класс должен производить только те операции, которые необходимы для осуществления его функций. Все другие действия следует либо удалить совсем, либо переместить, если есть вероятность, что они понадобятся другому классу в будущем.
Назначение
Принцип служит для того, чтобы раздробить единый набор действий на ряд наборов поменьше – таким образом, каждый класс делает то, что от него действительно требуется, и ничего больше.
D — Dependency Inversion (Принцип инверсии зависимостей)
Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Для начала объясню термины, которые здесь применяются, простыми словами.
Модули (или классы)?верхнего?уровня = классы, которые выполняют операцию при помощи инструмента
Модули (или классы)?нижнего?уровня = инструменты, которые нужны для выполнения операций
Абстракции – представляют интерфейс, соединяющий два класса
Детали = специфические характеристики работы инструмента
Согласно данному принципу, класс не должен соединяться с инструментом, который применяет для выполнения операции. Вместо этого он должен быть соединён с интерфейсом, который поможет установить связь между инструментом и классом.
Кроме того, принцип гласит, что ни интерфейс, ни класс, не обязаны вникать в специфику работы инструмента. Напротив, это инструмент должен подходить под требования интерфейса.
Назначение
Этот принцип служит для того, чтобы устранить зависимость классов верхнего уровня от классов нижнего уровня за счёт введения интерфейсов.
Обобщая сказанное
Мы разобрали все пять принципов и сформулировали для каждого назначение. Всё это призвано помочь вам писать код, который можно модифицировать, расширять и тестировать с минимумом проблем. Спасибо, что прочитали; надеюсь, вы получили не меньше удовольствия, чем я в процессе работы над статьёй.