SOLID принципы программирования: полное погружение

SOLID — это аббревиатура, которая описывает пять ключевых принципов проектирования и организации объектно-ориентированного кода. Эти принципы были сформулированы Робертом Мартином (также известным как Uncle Bob) и включают в себя следующие концепции: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation и Dependency Inversion. Цель использования принципов SOLID — упростить разработку, сделать ее более гибкой и устойчивой к ошибкам.
SOLID принципы
Как уже упоминалось, SOLID включает в себя пять принципов. Принципы SOLID являются фундаментальными и важными в объектно-ориентированном программировании. При правильном использовании они упрощают процесс разработки, облегчают поддержку и изменение кода.
Принципы SOLID — примеры
Рассмотрим каждый из принципов более подробно и на примерах.
Принцип единственной ответственности (Single Responsibility)
Принцип единственной ответственности означает, что каждый класс или модуль в программе должен иметь только одну причину для изменения. Это означает, что класс должен выполнять только одну работу. Если класс начинает выполнять более одной функции, это может привести к зависимостям кода и сделать его менее устойчивым к изменениям.
В качестве примера допустим, у нас есть класс User, который управляет данными пользователя и также обрабатывает логику регистрации и входа в систему. Это нарушение принципа единственной ответственности, поскольку класс User имеет более одной причины для изменения. Вместо этого, мы могли бы разделить этот класс на два: User и Authentication.
Принцип открытости/закрытости (Open-Closed)
Принцип открытости/закрытости гласит, что «программные сущности (классы, модули, функции и т.п.) должны быть открыты для расширения, но закрыты для изменения». Это означает, что вы должны быть в состоянии добавлять новые функции или поведение к существующим классам или модулям без изменения их исходного кода.
Например, предположим, у вас есть класс Shape с методом draw. Если вам нужно добавить новую форму, вам не нужно изменять класс Shape. Вместо этого, вы можете создать новый класс (например, Circle), который наследует от Shape и реализует метод draw.
Принцип подстановки Барбары Лисков (Liskov Substitution)
Принцип подстановки Барбары Лисков утверждает, что «объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения корректности программы».
Это означает, что если класс B является подклассом класса A, то мы должны иметь возможность заменить объект A на объект B без каких-либо проблем.
В качестве примера представьте, что у вас есть класс Bird с методом fly. Затем у вас есть класс Penguin, который наследует от класса Bird. Однако пингвины не умеют летать, поэтому использование метода fly для объекта Penguin будет ошибочным. Это нарушение принципа подстановки Лисков. В этом случае следовало бы переосмыслить иерархию классов и возможно использовать другой метод моделирования поведения.
Рекомендуем к прочтению: 7 лучших вопросов, которые можно задать в конце интервью работодателю
Принцип разделения интерфейса (Interface Segregation)
Принцип разделения интерфейса говорит о том, что «клиенты не должны зависеть от интерфейсов, которые они не используют». Это означает, что большой интерфейс должен быть разбит на несколько меньших и более конкретных, чтобы клиенты могли использовать только те интерфейсы, которые им нужны.
Допустим, у нас есть интерфейс Machine с методами print, fax, scan. Но что, если у нас есть простой принтер, который может только печатать? В этом случае, наш принтер вынужден имплементировать методы, которые ему не нужны, что является нарушением принципа разделения интерфейса.
Принцип инверсии зависимостей (Dependency Inversion)
Принцип инверсии зависимостей утверждает, что «зависимости внутри системы должны строиться на основе абстракций, а не деталей». Это означает, что высокоуровневые модули не должны зависеть от низкоуровневых модулей, и они все должны зависеть от абстракций.
Например, предположим, у нас есть класс LightBulb, который имеет методы turnOn и turnOff, и у нас есть класс ElectricPowerSwitch, который контролирует LightBulb. Вместо того, чтобы напрямую зависеть от LightBulb, класс ElectricPowerSwitch должен зависеть от абстрактного интерфейса (например, Switchable), который может быть реализован любым устройством, которое можно включить и выключить.
Итого: принципы SOLID кратко
Итак, SOLID — это набор пяти принципов, которые помогают программистам создавать эффективный, устойчивый и поддерживаемый код. Эти принципы направлены на уменьшение зависимостей в коде, облегчение поддержки и упрощение процесса внесения изменений. Важно помнить, что, хотя каждый принцип является мощным инструментом, они наиболее эффективны, когда используются вместе.
Мы — ИТ кадровое агентство, которое поможет вам найти ит специалиста за менее чем 2 недели. Свяжитесь с нами уже сегодня, чтобы узнать, как мы можем помочь масштабировать ваш следующий проект. Мы гарантируем поиск самого сильного кандидата, а не самого дорогого. За 10 лет мы закрыли более 5500 вакансий и собрали более 25 команд с нуля. Вы можете ознакомиться с отзывами наших клиентов о нашем рекрутинговом агентстве. Если вам нужны дополнительные референсы, напишите нам в Telegram.
Принципы SOLID на примерах
Всем привет! Данная статья — эта попытка объяснить принципы SOLID на примерах пcевдокода на Java. Статья будет полезна начинающим разработчикам понять данные принципы проектирования.
Вначале рассмотрим общее понятие, что такое SOLID и как расшифровывается каждая буква данной аббревиатуры.
SOLID — это принципы разработки программного обеспечения, следуя которым Вы получите хороший код, который в дальнейшем будет хорошо масштабироваться и поддерживаться в рабочем состоянии.
S — Single Responsibility Principle — принцип единственной ответственности. Каждый класс должен иметь только одну зону ответственности.
O — Open closed Principle — принцип открытости-закрытости. Классы должны быть открыты для расширения, но закрыты для изменения.
L — Liskov substitution Principle — принцип подстановки Барбары Лисков. Должна быть возможность вместо базового (родительского) типа (класса) подставить любой его подтип (класс-наследник), при этом работа программы не должна измениться.
I — Interface Segregation Principle — принцип разделения интерфейсов. Данный принцип обозначает, что не нужно заставлять клиента (класс) реализовывать интерфейс, который не имеет к нему отношения.
D — Dependency Inversion Principle — принцип инверсии зависимостей. Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракции. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Рассмотрим первый принцип — принцип единственной ответственности на примере.
Допустим у нас есть класс RentCarService и в нем есть несколько методов: найти машину по номеру, забронировать машину, распечатать заказ, получить информацию о машине, отправить сообщение.
public class RentCarService < public Car findCar(String carNo) < //find car by number return car; >public Order orderCar(String carNo, Client client) < //client order car return order; >public void printOrder(Order order) < //print order >public void getCarInterestInfo(String carType) < if (carType.equals("sedan")) < //do some job >if (carType.equals("pickup")) < //do some job >if (carType.equals("van")) < //do some job >> public void sendMessage(String typeMessage, String message) < if (typeMessage.equals("email")) < //write email //use JavaMailSenderAPI >> >
У данного класса есть несколько зон ответственности, что является нарушением первого принципа. Возьмем метод получения информации об машине. Теперь у нас есть только три типа машин sedan, pickup и van, но если Заказчик захочет добавить еще несколько типов, тогда придется изменять и дописывать данный метод.
Или возьмем метод отправки сообщения. Если кроме отправки сообщения по электронной почте необходимо будет добавить отправку смс, то также необходимо будет изменять данный метод.
Одним словом, данный класс нарушает принцип единой ответственности, так как отвечает за разные действия.
Необходимо разделить данный класс RentCarService на несколько, и тем самым, следуя принципу единой ответственности, предоставить каждому классу отвечать только за одну зону или действие, так в дальнейшем его будет проще дополнять и модифицировать.
Необходимо создать класс PrinterService и вынести там функционал по печати.
public class PrinterService < public void printOrder(Order order) < //print order >>
Аналогично работа связанная с поиском информации о машине перенести в класс CarInfoService.
public class CarInfoService < public void getCarInterestInfo(String carType) < if (carType.equals("sedan")) < //do some job >if (carType.equals("pickup")) < //do some job >if (carType.equals("van")) < //do some job >> >
Метод по отправке сообщений перенести в класс NotificationService.
public class NotificationService < public void sendMessage(String typeMessage, String message) < if (typeMessage.equals("email")) < //write email //use JavaMailSenderAPI >> >
А метод поиска машины в CarService.
public class CarService < public Car findCar(String carNo) < //find car by number return car; >>
И в классе RentCarService останется только один метод.
public class RentCarService < public Order orderCar(String carNo, Client client) < //client order car return order; >>
Теперь каждый класс несет ответственность только за одну зону и есть только одна причина для его изменения.
Принцип открытости-закрытости рассмотрим на примере только что созданного класса по отправке сообщений.
public class NotificationService < public void sendMessage(String typeMessage, String message) < if (typeMessage.equals("email")) < //write email //use JavaMailSenderAPI >> >
Допустим нам необходимо кроме отправки сообщения по электронной почте отправлять еще смс сообщения. И мы можем дописать метод sendMessage таким образом:
public class NotificationService < public void sendMessage(String typeMessage, String message) < if (typeMessage.equals("email")) < //write email //use JavaMailSenderAPI >if (typeMessage.equals("sms")) < //write sms //send sms >> >
Но в данном случае мы нарушим второй принцип, потому что класс должен быть закрыт для модификации, но открыт для расширения, а мы модифицируем (изменяем) метод.
Для того чтобы придерживаться принципа открытости-закрытости нам необходимо спроектировать наш код таким образом, чтобы каждый мог повторно использовать нашу функцию, просто расширив ее. Поэтому создадим интерфейс NotificationService и в нем поместим метод sendMessage.
public interface NotificationService
Далее создадим класс EmailNotification, который имплементит интерфейс NotificationService и реализует метод отправки сообщений по электронной почте.
public class EmailNotification implements NotificationService < @Override public void sendMessage(String message) < //write email //use JavaMailSenderAPI >>
Создадим аналогично класс MobileNotification, который будет отвечать за отправку смс сообщений.
public class MobileNotification implements NotificationService < @Override public void sendMessage(String message) < //write sms //send sms >>
Проектируя таким образом код мы не будем нарушать принцип открытости-закрытости, так как мы расширяем нашу функциональность, а не изменяем (модифицируем) наш класс.
Давайте сейчас рассмотрим третий принцип: принцип подстановки Барбары Лисков.
Данный принцип непосредственно связан с наследованием классов. Допустим у нас есть базовый класс Счет (Account), в котором есть три метода: просмотр остатка на счете, пополнение счета и оплата.
public class Account < public BigDecimal balance(String numberAccount)< //logic return bigDecimal; >; public void refill(String numberAccount, BigDecimal sum) < //logic >public void payment(String numberAccount, BigDecimal sum) < //logic >>
Нам необходимо написать еще два класса: зарплатный счет и депозитный счет, при этом зарплатный счет должен поддерживать все операции, представленные в базовом классе, а депозитный счет — не должен поддерживать проведение оплаты.
public class SalaryAccount extends Account< @Override public BigDecimal balance(String numberAccount)< //logic return bigDecimal; >; @Override public void refill(String numberAccount, BigDecimal sum) < //logic >@Override public void payment(String numberAccount, BigDecimal sum) < //logic >>
public class DepositAccount extends Account< @Override public BigDecimal balance(String numberAccount)< //logic return bigDecimal; >; @Override public void refill(String numberAccount, BigDecimal sum) < //logic >@Override public void payment(String numberAccount, BigDecimal sum) < throw new UnsupportedOperationException("Operation not supported"); >>
Если сейчас в коде программы везде, где мы использовали класс Account заменить на его класс-наследник (подтип) SalaryAccount, то программа продолжит нормально работать, так как в классе SalaryAccount доступны все операции, которые есть и в классе Account.
Если же мы такое попробуем сделать с классом DepositAccount, то есть заменим базовый класс Account на его класс-наследник DepositAccount, то программа начнет неправильно работать, так как при вызове метода payment() будет выбрасываться исключение new UnsupportedOperationException. Таким образом произошло нарушение принципа подстановки Барбары Лисков.
Для того чтобы следовать принципу подстановки Барбары Лисков необходимо в базовый (родительский) класс выносить только общую логику, характерную для классов наследников, которые будут ее реализовывать и, соответственно, можно будет базовый класс без проблем заменить на его класс-наследник.
В нашем случае класс Account будет выглядеть следующим образом.
public class Account < public BigDecimal balance(String numberAccount)< //logic return bigDecimal; >; public void refill(String numberAccount, BigDecimal sum) < //logic >>
Мы сможем от него наследовать класс DepositAccount.
public class DepositAccount extends Account< @Override public BigDecimal balance(String numberAccount)< //logic return bigDecimal; >; @Override public void refill(String numberAccount, BigDecimal sum) < //logic >>
Создадим дополнительный класс PaymentAccount, который унаследуем от Account и его расширим методом проведения оплаты.
public class PaymentAccount extends Account < public void payment(String numberAccount, BigDecimal sum)< //logic >>
И наш класс SalaryAccount уже унаследуем от класса PaymentAccount.
public class SalaryAccount extends PaymentAccount< @Override public BigDecimal balance(String numberAccount)< //logic return bigDecimal; >; @Override public void refill(String numberAccount, BigDecimal sum) < //logic >@Override public void payment(String numberAccount, BigDecimal sum) < //logic >>
Сейчас замена класса PaymentAccount на его класс-наследник SalaryAccount не «поломает» нашу программу, так как класс SalaryAccount имеет доступ ко всем методам, что и PaymentAccount. Также все будет хорошо при замене класса Account на его класс-наследник PaymentAccount.
Принцип подстановки Барбары Лисков заключается в правильном использовании отношения наследования. Мы должны создавать наследников какого-либо базового класса тогда и только тогда, когда они собираются правильно реализовать его логику, не вызывая проблем при замене родителей на наследников.
Рассмотрим теперь принцип разделения интерфейсов.
Допустим у нас имеется интерфейс Payments и в нем есть три метода: оплата WebMoney, оплата банковской карточкой и оплата по номеру телефона.
public interface Payments
Далее нам надо реализовать два класса-сервиса, которые будут у себя реализовывать различные виды проведения оплат (класс InternetPaymentService и TerminalPaymentService). При этом TerminalPaymentService не будет поддерживать проведение оплат по номеру телефона. Но если мы оба класса имплементим от интерфейса Payments, то мы будем «заставлять» TerminalPaymentService реализовывать метод, который ему не нужен.
public class InternetPaymentService implements Payments < @Override public void payWebMoney() < //logic >@Override public void payCreditCard() < //logic >@Override public void payPhoneNumber() < //logic >>
public class TerminalPaymentService implements Payments < @Override public void payWebMoney() < //logic >@Override public void payCreditCard() < //logic >@Override public void payPhoneNumber() < //. >>
Таким образом произойдет нарушение принципа разделения интерфейсов.
Для того чтобы этого не происходило необходимо разделить наш исходный интерфейс Payments на несколько и, создавая классы, имплементить в них только те интерфейсы с методами, которые им нужны.
public interface WebMoneyPayment
public interface CreditCardPayment
public interface PhoneNumberPayment
public class InternetPaymentService implements WebMoneyPayment, CreditCardPayment, PhoneNumberPayment < @Override public void payWebMoney() < //logic >@Override public void payCreditCard() < //logic >@Override public void payPhoneNumber() < //logic >>
public class TerminalPaymentService implements WebMoneyPayment, CreditCardPayment < @Override public void payWebMoney() < //logic >@Override public void payCreditCard() < //logic >>
Давайте сейчас рассмотрим последний принцип: принцип инверсии зависимостей.
Допустим мы пишем приложение для магазина и решаем вопросы с проведением оплат. Вначале это просто небольшой магазин, где оплата происходит только за наличные. Создаем класс Cash и класс Shop.
public class Cash < public void doTransaction(BigDecimal amount)< //logic >>
public class Shop < private Cash cash; public Shop(Cash cash) < this.cash = cash; >public void doPayment(Object order, BigDecimal amount) < cash.doTransaction(amount); >>
Вроде все хорошо, но мы уже нарушили принцип инверсии зависимостей, так как мы тесно связали оплату наличными к нашему магазину. И если в дальнейшем нам необходимо будет добавить оплату еще банковской картой и телефоном («100% понадобится»), то нам придется переписывать и изменять много кода. Мы в нашем коде модуль верхнего уровня тесно связали с модулем нижнего уровня, а нужно чтобы оба уровня зависели от абстракции.
Поэтому создадим интерфейс Payments.
public interface Payments
Теперь все наши классы по оплате будут имплементить данный интерфейс.
public class Cash implements Payments < @Override public void doTransaction(BigDecimal amount) < //logic >>
public class BankCard implements Payments < @Override public void doTransaction(BigDecimal amount) < //logic >>
public class PayByPhone implements Payments < @Override public void doTransaction(BigDecimal amount) < //logic >>
Теперь надо перепроектировать реализацию нашего магазина.
public class Shop < private Payments payments; public Shop(Payments payments) < this.payments = payments; >public void doPayment(Object order, BigDecimal amount) < payments.doTransaction(amount); >>
Сейчас наш магазин слабо связан с системой оплаты, то есть он зависит от абстракции и уже не важно каким способом оплаты будут пользоваться (наличными, картой или телефоном) все будет работать.
Мы рассмотрели на примерах псевдокода принципы SOLID, надеюсь кому-то будет это полезно.
Спасибо Всем, кто дочитал до конца. Всем пока.
- принципы проектирования
- принципы разработки
- solid
SOLID — принципы объектно‑ориентированного программирования
SOLID — это аббревиатура пяти основных принципов проектирования в объектно‑ориентированном программировании — Single responsibility, Open-closed, Liskov substitution, Interface segregation и Dependency inversion.
В переводе на русский: принципы единственной ответственности, открытости / закрытости, подстановки Барбары Лисков, разделения интерфейса и инверсии зависимостей)
Аббревиатура SOLID была предложена Робертом Мартином, автором нескольких книг, широко известным в сообществе разработчиков. Следование принципам позволяет строить на базе ООП масштабируемые и сопровождаемые программные продукты с понятной бизнес‑логикой. Код, который написан с соблюдением принципов SOLID, проще понимать, поддерживать, расширять или изменять его функциональность.
Расшифровка:
- Single responsibility — принцип единственной ответственности
- Open-closed — принцип открытости / закрытости
- Liskov substitution — принцип подстановки Барбары Лисков
- Interface segregation — принцип разделения интерфейса
- Dependency inversion — принцип инверсии зависимостей
Принцип единственной обязанности / ответственности (single responsibility principle / SRP) обозначает, что каждый объект должен иметь одну обязанность и эта обязанность должна быть полностью инкапсулирована в класс. Все его сервисы должны быть направлены исключительно на обеспечение этой обязанности. Подробнее про SRP →
Принцип открытости / закрытости (open-closed principle / OCP) декларирует, что программные сущности (классы, модули, функции и т. п.) должны быть открыты для расширения, но закрыты для изменения. Это означает, что эти сущности могут менять свое поведение без изменения их исходного кода. Подробнее про OCP →
Принцип подстановки Барбары Лисков (Liskov substitution principle / LSP) в формулировке Роберта Мартина: «функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа не зная об этом». Подробнее про LSP →
Принцип разделения интерфейса (interface segregation principle / ISP) в формулировке Роберта Мартина: «клиенты не должны зависеть от методов, которые они не используют». Принцип разделения интерфейсов говорит о том, что слишком «толстые» интерфейсы необходимо разделять на более маленькие и специфические, чтобы клиенты маленьких интерфейсов знали только о методах, которые необходимы им в работе. В итоге, при изменении метода интерфейса не должны меняться клиенты, которые этот метод не используют. Подробнее про ISP →
Принцип инверсии зависимостей (dependency inversion principle / DIP) — модули верхних уровней не должны зависеть от модулей нижних уровней, а оба типа модулей должны зависеть от абстракций; сами абстракции не должны зависеть от деталей, а вот детали должны зависеть от абстракций. Подробнее про DIP →
Статья опубликована в 2019 и была обновлена в 2023 году
Тематические статьи
Принцип программирования YAGNI — «Вам это не понадобится»
Принцип заключается в том, что возможности, которые не описаны в требованиях к системе, просто не должны реализовываться.
В результате разработка ненужных функций не сжигает бюджет проекта, а разработчики не тратят оплачиваемое время на реализацию и дальнейшее сопровождение в реальности ненужного функционала. Избыточный функционал сжигает больше всего ресурсов именно на сопровождении: больше написанного кода — труднее сопровождать и выше вероятность появления «багов». И тут очень уместна поговорка: «лучший код — это ненаписанный код».
методологии разработки
веб-разработка
Статья опубликована в 2019 и обновлена в 2023 году
Принцип программирования KISS — делайте вещи проще
KISS — это принцип проектирования и программирования, при котором простота системы декларируется в качестве основной цели или ценности.
Большая часть программных систем необосновано перегружена практически ненужными функциями, что ухудшает удобство их использование конечными пользователями, а также усложняет их поддержку и развитие разработчиками. Следование принципу KISS позволяет разрабатывать решения, которые не обладают этими недостатками: они просты в использовании и в сопровождении.
методологии разработки
веб-разработка
Статья опубликована в 2019 и обновлена в 2023 году
Принцип программирования DRY — don’t repeat yourself / не повторяйте себя
Следование принципу DRY позволяет добиться высокой сопровождаемости программного продукта: внесение изменений и тестирование значительно упрощаются.
Если код не дублируется, то для изменения логики достаточно внесения исправлений всего в одном месте. Также значительно проще тестировать одну (пусть и более сложную) функцию, а не набор из десятков однотипных. При следовании DRY упрощается и повторное использование функций, вынесенных из сложных алгоритмов, что позволяет сократить время разработки и тестирования новой функциональности.
веб-разработка
методологии разработки
Статья опубликована в 2018 и обновлена в 2023 году
Стандарты кодирования — залог хорошей сопровождаемости проекта
Любая командная разработка может быть эффективной только в том случае, если участники команды имеют общее видение.
Если над проектом работает команда, а не один‑два разработчика, то обязательно должен быть стандарт оформления кода — набор правил и соглашений, которые описывают базовые принципы оформления программного кода, используемого совместно группой разработчиков.
методологии разработки
веб-разработка
Статья опубликована в 2014 и обновлена в 2023 году
Флаги функций (Feature Flags)
Флаги функций позволяют отделить развертывание функций от развертывания кода, обеспечивают возможности для A/B-тестирования и предоставляют механизм быстрого отключения проблемных функций
Принципы SOLID в картинках

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

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

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

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

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

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