Найти в Дзене
ZDG

SOLID-ный код для солидных господ. Часть 5

Наконец, мы добрались до последней буквы D, которая обозначает Dependency Inversion, или Инверсия Зависимости.

Предыдущая часть:

Мы уже понимаем, как нужно проектировать код. Это и единственная ответственность, и разделение интерфейсов, и подстановка Лисков :) Всё это на самом деле примерно об одном и том же, так что и последний принцип не откроет нам ничего особенно нового. Хотя из всех он кажется мне наиболее хитрым. Вот как он звучит:

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

Как всегда непонятно. Но на самом деле ничего особенно сложного нет.

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

Вот скажем есть у нас модуль Logger, предназначенный для записи в логи. Он работает с файловой системой: может создавать файлы, писать в них и т.д.

Теперь рассмотрим другой модуль, назовём его App, то есть приложение. Приложение использует логгер. По отношению к модулю Logger модуль App – более высокого уровня.

Теперь подумаем, как модуль App может зависеть от модуля Logger. Например, когда App хочет начать новый лог, он вызывает какую-то функцию в модуле Logger, которая работает с файловой системой, и передаёт туда имя файла. В частности, модулю App нужно знать, что есть конкретная аппаратно- и программно-зависимая реализация – диск и файловая система и имена файлов.

Получается, что если в реализации нижнего уровня Logger что-то поменяется, то соответственно поменяется и его использование в App.

То есть модуль верхнего уровня App зависит от модуля нижнего уровня Logger. Что нам как раз делать и не разрешено!

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

Зачастую подобные опасения не имеют под собой почвы. Мы можем сколько угодно стелить соломку, но в большинстве случаев никогда ничего не меняется, разве что весь проект целиком :)

Поэтому я выбрал более жизнеспособный пример. На заре существования данного канала я делал игру Robots, которая писалась сразу на двух языках: JavaScript и Python.

Рисование там делалось прямоугольниками, и чтобы нарисовать прямоугольник, требовался низкоуровневый модуль. И для JavaScript и для Python варианты этого модуля были разные. Начиная от передачи координат и заканчивая форматом цвета.

Соответственно, версия игры на JavaScript зависела от одного варианта реализации прямоугольника, а на Python – от другого. Но сам принцип проектирования заключался в том, чтобы мне не надо было писать два разных варианта кода.

Поэтому здесь как раз подходящий случай: если в одном проекте может и не быть изменений, то в двух версиях проекта разница есть сразу.

Как развернуть зависимость?

И вот тут мы приходим к не совсем тривиальному, по сравнению с предыдущими принципами, решению.

Рассмотрим опять модули Logger и App. Сейчас модуль App содержит зависимость – если изменится реализация Logger, то App должен поддержать её у себя.

Но App не хочет этого делать. Тогда он говорит – Logger, вот тебе абстракция AppLogger, я буду пользоваться тобой через неё. У неё будет вот такой интерфейс. А твоя ответственность теперь – поддерживать у себя работу с этой абстракцией.

Введением абстракции мы развязали два модуля, и кроме того, развернули зависимость в противоположную сторону. Теперь Logger ответственный за то, чтобы поддерживать AppLogger, которым пользуется App.

Аналогичным образом в игре Robots на JS и Python были сделаны функции draw_rectangle(), которые работают одинаково со стороны игры. То есть туда передаются одни и те же координаты и один и тот же цвет, заданные в одном и том же формате (на самом деле не совсем, но будем для данного материала считать так :) Два варианта игры теперь не зависят от конкретной реализации рисования прямоугольника. А функция, которая его реально рисует, теперь зависит от установленного порядка передачи данных и их форматов. Она сама преобразовывает эти данные в нужные форматы и структуры.

Функция draw_rectangle() и есть та самая абстрация. Приложение зависит от неё, библиотека рисования тоже зависит от неё. Но приложение не зависит от библиотеки рисования.

Да, в данном примере это не модуль, а всего лишь функция, но принцип остаётся тот же – зависимость снимается с вышестоящего компонента и передаётся нижестоящему через абстракцию.

Так как варианты игры Robots содержат функции, которые преобразуют данные в нужный вид, то можно сказать, что нам всё-таки пришлось писать два варианта кода. И где же тогда наша выгода?

Она в том, что эти варианты изолированы, а работа приложения на верхнем уровне не зависит от деталей реализации. Да, мы можем дописывать детали реализации, но приложение в целом нам переписывать не нужно.

Освободив его от зависимости, мы очищаем его логику и облегчаем доработки, в том числе перенос на другие языки.

Ну, вот наверное и весь SOLID, с чем всех нас и поздравляю.