Задача этой статьи только одна - попробовать уложить принципы SOLID на понятных «бытовых» примерах, а уже потом посмотреть, как оно может работать на практике - в коде.
Итак, SOLID - это 5 принципов, которые используются при разработке приложений. На каждый принцип по букве:
1. S — Single Responsibility Principle (Принцип единственной ответственности)
Определение: Каждый класс должен выполнять только одну задачу.
Пример из жизни:
Например, мы купили шкаф для одежды.
Хранение одежды - это его основная и единственная задача.
Потом мы решили хранить там не только вещи, но и инструменты. Это усложнило назначение шкафа и поиск конкретной вещи стал медленнее (вещей-то больше)
Потом мы решили хранить в этом же шкафу еще и продукты. Теперь шкаф выполняет сразу несколько абсолютно разных задач
Результат: шкаф больше не справляется с одной конкретной задачей и начинает терять свою основную функцию, превращаясь в беспорядочную «помойку».
Пример из разработки:
Например, у нас есть класс, в котором происходят действия с пользователями: их сохранение, получение, удаление. Ну, что-нибудь вроде такого:
@Service
@RequiredArgsConstructor
public class PersonService {
private final PersonRepository personRepository;
@Transactional
public void createPerson(Person person) {
personRepository.save(person);
}
public Person getPerson(UUID personId) {
return personRepository.getById(personId);
}
@Transactional
public void deletePerson(UUID personId) {
personRepository.deleteById(personId);
}
Такой класс не нарушает принцип единственной ответственности, т.к. отвечает за операции только с одной сущностью - пользователем. А вот если я в этот же класс решу добавить управление заказами пользователя, то выйдет что-то вроде:
@Service
@RequiredArgsConstructor
public class PersonOrderService {
private final PersonRepository personRepository;
private final OrderRepository orderRepository;
@Transactional
public void createPerson(Person person) {
personRepository.save(person);
}
public Person getPerson(UUID personId) {
return personRepository.getById(personId);
}
@Transactional
public void deletePerson(UUID personId) {
personRepository.deleteById(personId);
}
@Transactional
public void createOrder(Order order) {
orderRepository.save(order);
}
@Transactional
public void deleteOrder(UUID orderId) {
orderRepository.deleteById(orderId);
}
public Order getOrder(UUID orderId) {
return orderRepository.getById(orderId);
}
}
Ну, там в целом уже из названия понятно, что выходит какая-то ерунда. А если еще в этот класс попробовать добавить управление затратами пользователя, например, и еще что-нибудь, то в скором времени у нас выйдет длинная простыня, в которой едва ли нам самим будет возможно разобраться, не говоря уже о новых ребятах на проекте.
Правильная реализация - добавить под управление заказами отдельный класс.
Кстати, у меня есть телеграмм-канал, где я всякие штуки про разработку пишу - задачки алгоритмеческие решаю, паттерны обсуждаю. Если интересно - клик по ссылке https://t.me/crushiteasy
А мы продолжаем наш SOLID!
O — Open-Closed Principle (Принцип открытости/закрытости)
Определение: Классы должны быть открыты для расширения, но закрыты для модификации.
Пример из жизни:
Допустим, мы купили шкаф! Хех. Опять шкаф, да. Когда у нас стало больше одежды (да-да, одежды, а не продукты мы тоже захотели в нем хранить), не нужно разбирать шкаф и делать новый. Мы просто покупаем дополнительные полки, ящик или секцию, расширяя таким образом функциональность шкафа и не ломая его структуру.
Пример из разработки:
Допустим, у нас есть класс TaskService, который отвечает за определенные действия над задачами - начинает их выполнение и завершает:
@Service
@RequiredArgsConstructor
public class TaskService {
public void process(String action) {
if (action.equals("start")) {
//начни выполнение задачи
//проставь дату начала выполнения
} else if (action.equals("complete")) {
//заверши выполнение задачи
//проставь дату окончания выполнения
}
}
}
Вроде бы все ничего, компактно и понятно. Но вдруг бизнес решил, что задачи нужно не только начинать и выполнять, но и иметь возможность переназначить, что ведет за собой еще ряд дополнительных действий. А потом бизнес захочет что-то еще, например. Если мы полезем со своими правками прямо в этот класс, то мы нарушим принцип открытости/закрытости, т.к. модифицируем его.
Ну, и ладно, скажете вы, нарушим и нарушим. Беда наступит тогда, когда у нас будет огромное полотно из if-ов, в каждом из которых будет своя логика. Результат: код, сложный для прочтения.
Правильная реализация:
Создаем общий интерфейс, назовем его TaskProcessor:
public interface TaskProcessor {
void process(String action);
}
Создаем два класса, каждый из которых реализует этот интерфейс и метод process:
public class StartActionProcessor implements TaskProcessor {
@Override
public void process(String action) {
//начни выполнение задачи
//проставь дату начала выполнения
}
}
public class CompleteActionProcessor implements TaskProcessor {
@Override
public void process(String action) {
//заверши выполнение задачи
//проставь дату окончания выполнения
}
}
Все, теперь не нужно волноваться, если потребуется добавить еще какое-нибудь действие вроде «переназначить» задачу. В этом случае мы просто создадим новый класс и сделаем это.
p.s. если интересно, как внедрять все эти процессоры и заставить сработать определенный, то в планах есть статья как раз об этом.
3. L — Liskov Substitution Principle (Принцип подстановки Барбары Лисков)
Определение: Объекты должны быть заменяемыми их подтипами без изменения правильности работы программы.
Пример из жизни:
Представь, что мы купили пылесос, он выглядит, как пылесос, включается, как пылесос, но вот только одно но - вместо того, чтобы всасывать в себя пыль, он разбрызгивает огромную струю масла при включении. Думаю, такая ситуация, не вызовет у нас радости, а заставит вернуть пылесос обратно в магазин, да еще и жалобу накатать его создателям.
Пример из разработки:
У нас есть общий класс по обработке заказов с двумя методами - обработка заказа и печать чека:
public class OrderService {
public void process() {
//обработка
}
public void printReceipt() {
//печать чека
}
}
У этого этого класса есть два наследника:
public class OfflineOrderService extends OrderService {
public void process() {
//обработка заказа
}
public void printReceipt() {
//печать чека
}
}
public class OnlineOrderService extends OrderService {
public void process() {
//обработка заказа
}
public void printReceipt() {
throw new UnsupportedOperationException("Операция не поддреживается");
}
}
Второй наследник не поддерживает операцию «печать чека», а поэтому в случае подстановки вместо базового класса OrderService, мы получим исключение, что нарушает принцип.
Правильная реализация была бы:
public class OrderService {
public void process() {
//обработка
}
}
public class OfflineOrderService extends OrderService {
public void process() {
//обработка заказа
}
public void printReceipt() {
//печать чека
}
}
public class OnlineOrderService extends OrderService {
public void process() {
//обработка заказа
}
//что-то еще
}
Теперь, если мы заменим OrderService на OfflineOrderService или OnlineOrderService в любом куске кода, то общая логика сохранится.
4. I — Interface Segregation Principle (Принцип разделения интерфейса)
Определение: Не нужно заставлять клиента зависеть от методов, которые они не используют.
Пример из жизни:
Представьте, что мы купили телевизор. К этому телевизору шел пульт, с помощью которого им можно управлять. Но оказалось, что этот пульт управляет не только телевизором, но и кондиционером, и обогревателем, т.е. на нем гораздо больше кнопок. А ты хотел простой и понятный, минималистичный пульт, который управляет только телевизором.
Пример из разработки:
Например, у нас есть также онлайн и оффлайн заказы. К онлайн заказам можно применить скидку, а к оффлайн заказам нельзя. Если мы напишем общий интерфейс для обработки заказов, поместив туда методы и для создания заказа и для применения скидки, то получим что-то такое:
public interface OrderService {
void createOrder();
void applyDiscount();
}
public class OnlineOrderService implements OrderService {
@Override
public void createOrder() {
System.out.println("Order created.");
}
@Override
public void applyDiscount() {
System.out.println("Discount applied.");
}
}
public class OfflineOrderService implements OrderService {
@Override
public void createOrder() {
System.out.println("Order created.");
}
@Override
public void applyDiscount() {
throw new UnsupportedOperationException("Discount cannot be applied");
}
}
Получается, тут мы заставили OfflineOrder реализовывать метод «применения скидки», который ему не нужен. Правильная реализация:
public interface OrderService {
void createOrder();
}
public interface DiscountService {
void applyDiscount();
}
public class OnlineOrderService implements OrderService, DiscountService {
@Override
public void createOrder() {
System.out.println("Order created.");
}
@Override
public void applyDiscount() {
System.out.println("Discount applied.");
}
}
public class OfflineOrderService implements OrderService {
@Override
public void createOrder() {
System.out.println("Order created.");
}
}
Теперь мы не нарушаем принцип. Кстати, если подумать чуть глубже, то таким образом мы не нарушаем и принцип единственной ответственности - так как все-таки управление заказом и применение скидки - разные вещи.
5. D — Dependency Inversion Principle (Принцип инверсии зависимостей)
Сейчас будет самая нудитяна!! Ладно, там нудное только определение, а дальше разберемся:)
Итак, определение: Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей, но детали должны зависеть от абстракций.
Погнали разбираться с всей этой «абстракцией»
Пример из жизни:
Представь, что у тебя есть розетка в доме. Ты не задумываешься о том, какое именно устройство ты будешь подключать в неё — фен, телефон, зарядку для ноутбука — всё будет работать, потому что розетка стандартизирована (абстракция) - не надо шутить тут про американские вилки))))). Если бы тебе нужно было менять розетку каждый раз под новое устройство, это было бы крайне неудобно.
Пример из разработки:
// Низкоуровневый класс для уведомлений
public class EmailNotificationService {
public void send(String message) {
System.out.println("Sending email notification: " + message);
}
}
// Высокоуровневый класс
public class OrderService {
private EmailNotificationService emailNotificationService;
public OrderService() {
this.emailNotification = new EmailNotification(); // Прямое создание зависимости
}
public void placeOrder(String orderDetails) {
System.out.println("Order placed: " + orderDetails);
emailNotificationService.send("Order confirmation for: " + orderDetails);
}
}
Здесь высокоуровневый класс OrderService зависит от низкоуровнего EmailNotification - это затруднит тестирование и замену реализации уведомлений, если появится новый способ - SmsNotificationService.
Правильная реализация:
// Абстракция для уведомлений
public interface NotificationService {
void send(String message);
}
// Конкретная реализация для уведомлений по электронной почте
public class EmailNotificationService implements NotificationService {
@Override
public void send(String message) {
System.out.println("Sending email notification: " + message);
}
}
// Конкретная реализация для уведомлений по SMS
public class SmsNotificationService implements NotificationService {
@Override
public void send(String message) {
System.out.println("Sending SMS notification: " + message);
}
}
// Высокоуровневый класс, который зависит от абстракции
public class OrderService {
private NotificationService notificationService;
// Зависимость передается через конструктор
public OrderService(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void placeOrder(String orderDetails) {
System.out.println("Order placed: " + orderDetails);
notificationService.send("Order confirmation for: " + orderDetails);
}
}
Теперь высоуровневый класс зависит от абстракции - интерфейса NotificationService, а значит в случае замены реализации проблем не будет. Если подключить спринг, то там уже можно будет интереснее показать, когда и как какая именно реализация будет использоваться (тут у нас и профили, и квалифаеры, да и можно через процессоры с хэш-мапой тоже реализовать круто)
Ну, на этом SOLID все. А зачем все эти принципы, спросите вы? В конечном счете, чтобы ваш код было удобнее читать и поддерживать вам самому, ну и вашим коллегам, конечно же:)
Комментарии (22)
maratxat
09.10.2024 11:02Примеры хорошие, за это плюс. Однако, есть упущение.
В DIP раскрыли только первую часть принципа, а вторую - нет.
Абстракции не должны зависеть от деталей, но детали должны зависеть от абстракций.
Здесь стоит добавить, что абстракции не должны быть большими и т.о. не должны требовать от класса-реализатора реализовать весь огромный функционал. Иначе, абстракция становится зависимой от дефолтной реализации - т.е. абстракция становится зависимой от деталей.
Если абстракция становится зависимой от дефолтной реализации, то и компоненты, зависимые от этой абстракции, транзитивно становятся зависимы от дефолтной реализации этой абстракции - т.е. нарушается первая часть DIP, поскольку компоненты становятся зависимы не от абстракции а от других компонентов - дефолтных реализаций абстракции.
Фактически, вторая часть DIP'а требует от абстракций соблюдать SRP и ISP.
nin-jin
09.10.2024 11:02А что вы думаете по такой взгляд на эти принципы?
nivorbud
09.10.2024 11:02Я например согласен с таким взглядом. Он сводится к тому, что к этим принципам надо подходить разумно, без фанатизма. Иначе всё может стать только хуже: огромное количество микрофункций/микромодуле/микроклассов, разбросанных по тысячам дирректорий/файлов... Разобраться в такой каше может быть намного сложнее, чем в простыне на десяток-другой тысяч строк. В общем, главное во всём этом - вовремя остановиться. А многие ведь буквально воспринимают принципы типа "не более пяти строк".
panzerfaust
09.10.2024 11:02Иначе всё может стать только хуже: огромное количество микрофункций/микромодуле/микроклассов, разбросанных по тысячам дирректорий/файлов... Разобраться в такой каше может быть намного сложнее, чем в простыне на десяток-другой тысяч строк
...
принципы типа "не более пяти строк".
Вы палитесь. Вы мешаете в кучу SOLID и подход "Чистый код". Так делают или тролли или те, кто не видит разницу и плохо понимает вопрос. Вот Мартин-то не зря описал эти 2 вещи в 2 разных трудах.
nivorbud
09.10.2024 11:02Во-первых, я рассматриваю аспекты фанатизма/неофитства, а они общие для всех подходов.
Во-вторых, связь между этими вещами всё же есть и она прямая. Например, требование к компактности функций и малому числу аргументов (не более трех вроде?) выводятся из принципа единственной ответственности. Т.е. эти "пять строк" не из пустого места взялись.
Как общие подходы эти принципы хороши, но рассматривать их сами по себе, в отрыве от конкретной задачи, да ещё с фанатизмом, не очень хорошая затея. Зачастую проект простой и там не нужны горы многоуровневых абстракций. Я раньше думал, что может быть в будущем, когда наступит время расширять и усложнять проект, эти дополнительные абстракции помогут, но... по факту оказывалось, что это не нужно, а сопровождение проекта из-за лишних абстракций усложнялось. А в случае если всё же наступала пора радикально расширять проект, то он обычно с нуля полностью переписывался с учетом новых требований. А первоначальное простое решение было по сути прототипом.
Мой вывод: все эти принципы держать в голове, но конкретную реализацию выбирать под конкретную задачу и главное разумно выбирать.
playermet
09.10.2024 11:02выводятся из принципа единственной ответственности
Не выводятся. Потому что SRP это вообще не про то, что у функции/класса должна быть одна ответственность. О чем этот принцип лучше прочитать от его создателя напрямую.
panzerfaust
09.10.2024 11:02Во-вторых, связь между этими вещами всё же есть и она прямая. Например, требование к компактности функций и малому числу аргументов (не более трех вроде?) выводятся из принципа единственной ответственности
Это вы сами придумали. Все-таки книги Мартина написаны на простом современном языке и в толковании не нуждаются. Открываем и видим, что и откуда у него выводится. Тезисы про арность и длину функций следуют просто из его опыта. Про SOLID ни слова. Он просто советует делать так, потому что ему так было проще. Хочешь - доверяешь его опыту, хочешь - не доверяешь.
Функции хорошие в плане SRP могут быть короткими, могут быть длинными и страшными. Например, весь класс HashMap в джаве страшный как ядерная война. Однако по SRP какие к нему вопросы? Такая ситуация скорее всего в стандартной либе любого языка. Чистым кодом и не пахнет, а вот найти нарушения SOLID надо прям постараться.
nivorbud
09.10.2024 11:02Это вы сами придумали. Все-таки книги Мартина написаны на простом современном языке и в толковании не нуждаются.
Извините, я не фанатик, чтобы такие тексты воспринимать буквально.
А если мысли авторов воспринимать по существу, то связь с некоторыми сабжевыми принципами прекрасно видна. Да и почти буквально в этих книгах говорится, что если функция длинная, если она принимает на вход больше трех аргументов или принимает на вход флаги, то это скорей всего означает, что ф-ция выполняет более одной задачи. И эту логику тоже не надо рассматривать буквально. Это корректней рассматривать как рекомендацию внимательней посмотреть на функцию в плоскости соответствия некоторым принципам, если обнаруживаются эти признаки. Да, это не прямые связи, а косвенные, но всё взаимосвязано.
lamerok
09.10.2024 11:02В принципе статья про это же. Тут пример про шкаф для одежды, для S, но можно же и дальше дальше пойти, отдельный шкаф для носков, трусов, футболок.
Где то остановиться же надо и определить для себя, что для вашего приложения S.
evgenyk
09.10.2024 11:02Случай из жизни. Довелось мне как-то рефакторить один API. Где, как и что - неважно.
Но была там одна функция на 700 строк, написанная в стиле "что вижу, то пою". Т.е. логика совершенно разнородная в одной функции, куча if-ов и так далее. Ну, я выделил отдельные логические блоки в функции, постарался их понятно обозвать и сделать так, чтобы функция верхнего уровня читалась как объяснение логики работы.А в это же самое время пришел к нам еще один товарищ, адепт ООП. Он рефакторил другой кусок, тоже большой. Он взял, создал класс, получился класс со множеством методов, но, так как логика была довольно сложная,он накидал еще условий в метод
_init_()
Так, что теперь логика созданного объекта зависела от того, с какими аргументами вызывался этот инит.
И снизошло на меня просветление. Я подумал, хорошо, что я рефакторил код, написанный в стиле "большая простыня кода". Если бы я что-то делал за адептом ООП, да если бы еще он применил бы все принципы, создал бы дерево классов, я бы еще и сейчас бы сидел там и разбирался, что куда наследует и с какими свойствами получается объект.
nivorbud
09.10.2024 11:02Мне тоже пришлось недавно изучать один проект. Сразу скажу, возникли двоякие ощущения... С одной стороны, код был идеально вылизан, всё было четко разбито на уровни/классы/функции, причем на очень компактные функции (менее 5 строк, как по феншую). И я, пытаясь понять логику кода,... задолбался перескакивать из одной микрофункции (в одном файле), в другую микрофункцию (в другом файле), потом далее, делее, далее, потом... обратно, обратно, обратно... А их тысячи. Например, вижу вызов функии, иду туда (в ее реализацию), там пара строк с вызовом другой функции, иду в нее, там одна строка с вызовом третьей функции из четырех строк... и так далее... всё глубже и глубже... я уже давно забыл, чего начал...
evgenyk
09.10.2024 11:02Для себя я выработал примерно такое эмпирическое правило. Может оно и не по фэншую, но для меня работает. И оно довольно простое и помещается в голове.
1) Всю логику верхнего уровня делаю в функции run() верхнего уровня. Ну или методе run().
2) Этоа функция по возможности не должна содержать "if" или только один уровень, так, чтобы вложенных ифов не было.
3) Функции второго уровня называю так, чтобы читая функцию верхнего уровня сверху вниз в голове создавалась блок схема того, что делает программа.
4) Вложенность второго уровня должна быть минимальной, насколько возможно.
В общем структура программы должна напоминать грабли с короткими зубьями, а не дерево со множеством ветвлений.
Наследование стараюсь применять по минимому, в основном там где нужно изменить поведение, просто переопределяыя методы. (пишу на питоне).
Но выбор архитектуры программирования ИМХО зависит от области применения. Где-то хорошо подходит ООП, где-то пайплайн. Но таких областей не так уж много.
В моем стиел один недостаток, на интервью начнешь описыать - не поймут о чем это.
maratxat
09.10.2024 11:02побочная сложность растет не только если у компонента большой функционал, но еще и когда слишком много уровней скрыто за абстракцией. пока гуляешь по этим вызовам забывается что было в предыдущих вызовах
думаю, стоит выделять в системе небольшое количество концептуальных слоев и делить реализацию между ними. даже если методы получаются не меньше 5 строк
lamerok
09.10.2024 11:02Для этого вначале рисуют дизайн на UML например, но это при водопад. И потом смотрят не в код, а на дизайн.
При еджайле, конечно такое не проканает.
gudvinr
09.10.2024 11:02Хранение одежды - это его основная и единственная задача.
Потом мы решили хранить там не только вещи, но и инструменты. Это усложнило назначение шкафа и поиск конкретной вещи стал медленнее (вещей-то больше)
Потом мы решили хранить в этом же шкафу еще и продукты. Теперь шкаф выполняет сразу несколько абсолютно разных задач
Это довольно плохой пример для иллюстрации SRP. Шкаф может состоять из модулей (про это вы говорите в OCP). И то, что вы будете хранить в одном из модулей не одежду, а инструменты - не будет нарушать SRP на уровне отдельных модулей.
Даже хранить инструменты в шкафу для одежды - это вообще не зазорно. Для этого там может быть ниша снизу, например, в которой может лежать вообще ничего (т.к. вы решили что в этом шкафу кроме одежды ничего не будет).
А поиск конкретной вещи может и не стать медленнее. Если у вас шкафов меньше - то и мест для поиска тоже меньше.
Ну хорошо, допустим мы считаем что шкаф для одежды - только для одежды. А верхняя одежда в этом шкафу - будет являться нарушением SRP? Это одежда, но мы обычно выделяем такую одежду. Или обувь: технически, это вещь, которую мы надеваем.
Допустим, мы купили шкаф! Хех. Опять шкаф, да. Когда у нас стало больше одежды (да-да, одежды, а не продукты мы тоже захотели в нем хранить), не нужно разбирать шкаф и делать новый. Мы просто покупаем дополнительные полки, ящик или секцию, расширяя таким образом функциональность шкафа и не ломая его структуру.
Здесь тоже шкаф - это плохой пример. "Просто покупаем" полки/ящики может привести к тому, что у вас некуда будет их вставлять, или уже висящая одежда по высоте не будет помещаться. И с этим в принципе ничего не сделать - это физическое ограничение. Секции тоже не получится ставить бесконечно по тем же причинам.
Представьте, что мы купили телевизор. К этому телевизору шел пульт, с помощью которого им можно управлять. Но оказалось, что этот пульт управляет не только телевизором, но и кондиционером, и обогревателем, т.е. на нем гораздо больше кнопок. А ты хотел простой и понятный, минималистичный пульт, который управляет только телевизором.
И это плохой пример. Точнее, ISP он конечно иллюстрирует, но в реальности многим людям наоборот не хочется иметь много пультов, потому что они теряются и необходимо разрабатывать стратегию менеджмента пультами.
Представь, что у тебя есть розетка в доме. Ты не задумываешься о том, какое именно устройство ты будешь подключать в неё — фен, телефон, зарядку для ноутбука — всё будет работать, потому что розетка стандартизирована (абстракция) - не надо шутить тут про американские вилки))))). Если бы тебе нужно было менять розетку каждый раз под новое устройство, это было бы крайне неудобно.
Да, но нет. Опять же, мир достаточно сложен и без американских розеток. Технически, розетка одна, но максимальный ток может быть разным. Особенно это на удлинителях заметно. Удлинителей на 16А очень мало, а на 10А и меньше - хоть ложкой ешь.
Примеры должны быть такими, чтобы было как можно меньше разночтений. Но придумывать хорошие примеры - сложно.
mrobespierre
09.10.2024 11:02Первый принцип понят неверно. Хотя это обычное дело, даже с учётом того, что в книге, чуть дальше, всё расписано. У меня, может, весь микросервис выполняет одну задачу. Что такое задача вообще?
Правильно так: у кода должна быть только одна причина для изменений. Более того, дядя Боб её буквально уточняет: эта причина - пользователь системы (бухгалтер например). Т.е. мы должны зависеть только от бизнес-требований, а не от каких-то API и уж тем более имплементаций чего-то. Это принцип влияет на все остальные принципы, особенно второй. В итоге любые детали реализации (API breakage например) не должны заставлять нас что-то переписывать, только написать новый адаптер.
gun_dose
09.10.2024 11:02В целом статья неплохая, но на Барбару Лисков очень странный пример "из жизни". Не знаю, у кого в жизни случалось такое, что пылесос брызгал маслом. И даже если у кого-то в жизни случалось такое, хз как это относится к теме.
Я бы предложил другой пример: допустим, вы купили шкаф (опять), да не простой, а серверный. В шкафу стоят стандартные рейки от системы крепления и вы можете купить что угодно с таким креплением, и оно подойдёт в этот шкаф, что бы это ни было: сервер, накопитель, UPS, маршрутизатор.
Кстати, если у брызжущего маслом пылесоса из чьей-то жизни окажется аналогичное крепление, то он как раз будет в тему.
vojaganto
09.10.2024 11:02Что касается примеров из жизни - вот вообще всё не так. По порядку.
Здесь рассказано про наборы коллекций, это вполне может быть уместно. Более корректный пример был бы если вы например зимой использовали бы шкаф для хранения одежды, а летом, ну не знаю, как доску для серфинга например.
Сам принцип связан с наследованием, как можно отнаследоваться от шкафа, не совсем понятно. В примере про замену секций рассказывается про композицию. Более корректный пример был бы если мы взяли бы чертежи шкафа и дорисовали бы к ним что-то дополнительное на основе существующего.
Тут вообще описан баг и несоответствие ТЗ, причем тут L?
В примере про I из SOLID на самом деле рассказывается про S.
В примере с розеткой на самом деле описывается интерфейс и его преимущества, и не более того.
SergioPredatore
09.10.2024 11:02Пример с телевизором не очень. Был у меня телевизор и видеомагнитофон, так пульт от магнитофона управлял ещё и телевизором, и это было чертовски удобно! А ещё сейчас в руках я держу смартфон, а он и звонки делает и сообщения пишет в самые разные мессенджеры (и вот прямо сейчас на Хабр) и видео проигрывает и музыку, а ещё фотографирует и даже видео снимает и ещё кучу всего делает... и это тоже удобно.
Другое дело, что и на пульте кнопки были визуально отделены друг от друга и в телефоне всё делается в собственных приложениях.
В общем не на всех уровнях хорошо и полезно разделять интерфейсы. Иногда их нужно наоборот объединять, для этого, например, существует паттерн агрегатор.
volmir
Спасибо за примеры из реальной жизни.
Сам применяю принципы SOLID в повседневности.
akozyrenko Автор
Рада, что понравилось!