Все статьи | Разработка

О релевантности принципов объектно-ориентированного программирования SOLID

О релевантности принципов объектно-ориентированного программирования SOLID главное изображение

Примечание — Это адаптированный перевод статьи Роберта Мартина Solid Relevance. Повествование ведётся от лица автора оригинала.

Недавно я получил письмо, автор которого интересовался релевантностью принципов SOLID. Вот что он написал:

Многие годы вопросы о принципах SOLID были стандартными на собеседованиях в нашей компании. Мы ожидали от кандидатов хорошего понимания этих принципов. В какой-то момент один из наших менеджеров, который не полностью погружён в программирование, спросил, актуально ли говорить с кандидатами о SOLID. Он сказал, что принцип открытости-закрытости стал не таким важным, как раньше. Это связано с переходом от монолитов к микросервисной архитектуре. Принцип подстановки Лисков давно устарел, потому что мы уже не уделяем столько же внимания наследованию, как 20 лет назад. Думаю, нам стоит учитывать позицию Дэна Норта в отношении SOLID, которую он выразил в рекомендации: «Пишите простой код».

В ответном письме я написал следующее.

Сегодня принципы SOLID остаются такими же релевантными, как в 90-е годы и раньше. Это связано с тем, что программы практически не изменились за эти годы. Более того, программы сильно не изменились с 1945 года, когда Алан Тьюринг написал первые строки кода для электронного компьютера. Программы всё ещё состоят из операторов if, циклов while и операторов присваивания. Это всё ещё последовательность, перебор и итерация.

Каждому новому поколению нравится думать, что его мир сильно отличается от мира, в котором жили предыдущие поколения. Это ошибка, которую совершает каждое поколение. Оно осознаёт эту ошибку, когда появляется следующее поколение, которое приходит и рассказывает, как сильно всё вокруг изменилось.

Давайте рассмотрим каждый принцип и оценим его актуальность.

Принцип единственной ответственности

Держите вместе сущности, которые меняются по одной причине. Разделяйте сущности, которые меняются по разным причинам.

Сложно представить, что этот принцип стал нерелевантным. Мы не смешиваем код, который отвечает за бизнес-логику, с кодом, который отвечает за интерфейсы. Мы не смешиваем SQL-запросы с протоколами передачи данных. Мы разделяем код, который меняется по разным причинам, благодаря чему изменения в одной подсистеме не влияют на другие подсистемы. Мы следим, чтобы модули, которые меняются по разным причинам, не влияли друг на друга.

Микросервисы не решают эту проблему. Если вы смешиваете код, который меняется по разным причинам, то можете получить запутанные микросервисы или даже наборы запутанных микросервисов.

Слайды Дэна Норта, ссылка на которые приводится выше, не учитывают этого момента. Из-за этого мне кажется, что он вообще не понимает принцип единственной ответственности, или что он иронизировал. Насколько я знаю Дэна, последнее очень вероятно. В ответ на упоминание принципа единственной ответственности он предлагает писать простой код. Я согласен с этим. Принцип единственной ответственности — один из способов писать простой код.

Принцип открытости-закрытости

Модуль должен быть открыт для расширения, но закрыт для модификации.

Когда кто-то сомневается в релевантности именно этого принципа, я сильнее всего беспокоюсь за будущее программирования. Конечно, мы хотим создавать модули, которые можно расширять без модификации. Вы можете представить себе работу с системой, в которой не реализована независимость от типа девайса, например, в которой запись файла на диск фундаментально отличается от печати файла на принтере? Мы же не хотим видеть отдельные инструкции для каждого типа девайса в коде?

Или… Хотим ли мы отделять абстрактные концепции от конкретных понятий? Хотим ли мы отделять бизнес-логику от небольших деталей, связанных с интерфейсами, или от протоколов передачи данных, или произвольного поведения базы данных? Конечно да!

И снова в слайдах Дэна Норта есть ошибка. При изменении требований только часть существующего кода становится неактуальной. Но значительная часть кода остаётся актуальной. И мы хотим знать, что нам не придётся менять работающую часть кода только для того, чтобы неработающий код снова заработал. Дэн снова предлагает писать простой код. И я снова соглашаюсь. Соблюдение принципа открытости-закрытости помогает писать простой код.

Принцип подстановки Лисков

Если программа использует интерфейс, она не должна знать о реализации этого интерфейса.

Люди, и я в том числе, долгое время ошибались, думая, что принцип подстановки Лисков применим только к наследованию. Это не так. Этот принцип определяет использование подтипов.

Все реализации интерфейса являются подтипами этого интерфейса. Все утиные типы относятся к подтипам подразумеваемого интерфейса. А каждый пользователь базового интерфейса должен принимать значение этого интерфейса. Если реализация мешает пользователю базового типа, количество конструкций if/switch в коде растёт.

Принцип подстановки Лисков говорит о необходимости чётко определять абстракции. Невозможно поверить, что этот принцип устарел.

В этом случае Дэн и его слайды правы. Он просто упустил из виду детали: простой код — это код, в котором чётко выделены абстракции.

Принцип разделения интерфейса

Делайте интерфейсы маленькими, чтобы клиенты не зависели от вещей, которыми они не пользуются.

Мы всё ещё работаем с компилируемыми языками. Мы всё ещё зависим от дат модификации, с помощью которых определяем, какие модули нужно перекомпилировать и повторно задеплоить. Пока это так, придётся сталкиваться с проблемой зависимости модуля A от модуля B во время компиляции, а не во время выполнения, поскольку изменения в модуле B приведут к перекомпиляции и повторному деплою модуля A.

Эта проблема стоит особенно остро в языках со статической типизацией, например, Java, C#, C++, Go, Swift и так далее. Языки с динамической типизацией подвержены этому в гораздо меньшей степени, но всё-таки полностью не защищены от этой проблемы. Это доказывает существование таких инструментов, как Maven и Leiningen.

Читайте также: Системы типов в языке — какие бывают и чем отличаются

Слайд Дэна, посвящённый этому принципу, содержит явно неправильную информацию. Клиенты однозначно зависят от методов, которые не используют, если они нуждаются в перекомпиляции и повторном деплое, когда эти методы меняются. Финальное замечание Дэна здравое, насколько это возможно. Да, если вы можете разделить класс с двумя интерфейсами на два класса, сделайте это. Это соответствует принципу единой ответственности. Но такое разделение часто невозможно и даже нежелательно.

Принцип инверсии зависимостей

Модули верхнего уровня не должны зависеть от деталей реализации модулей нижнего уровня.

Трудно представить архитектуру, в которой не используется этот принцип. Мы не хотим, чтобы высокоуровневая бизнес-логика зависела от деталей реализации на нижних уровнях. Я надеюсь, что это очевидно. Мы не хотим, чтобы вычисления, которые приносят нам деньги, смешивались с SQL-запросами, низкоуровневой валидацией или форматированием.

Мы хотим изолировать высокоуровневые абстракции от низкоуровневых деталей. Это разделение возможно благодаря корректному управлению зависимостями внутри системы. Это управление позволяет всем зависимостям в исходном коде, особенно пересекающим архитектурные границы, указывать на высокоуровневые абстракции, а не на низкоуровневые детали.

Слайды Дэна каждый раз заканчиваются призывом писать простой код. Это хороший совет. Но если годы развития индустрии чему-то нас научили, так это тому, что простота требует дисциплины, которая возможна благодаря принципам. Это те самые принципы, которые определяют простоту. Это та дисциплина, которая заставляет программистов писать простой код.

Лучший способ всё усложнить и устроить беспорядок — просто посоветовать людям писать простой код и ничего больше им не объяснить.

Бесплатные курсы на Хекслете

Учитесь в удобном для вас ритме