[recovery mode] Немного про SOLID и суровое зомби-легаси
Осторожно-оптимистические размышления о месте современной производственной культуры в сопровождении унаследованного из древних времён программного обеспечения. И немного о взаимопроникновении принципов SOLID.
Я систематически работаю с Delphi 7 (работа такая, чо…). Поддерживаю и активно дорабатываю приложение, уходящее корнями в начало-середину нулевых и написанное в беспощадном императивно-процедурном стиле. Можно ли выжить ментально в подобном окружении? Ведь «единственный способ их победить — не стать одним из них», не так ли?
Здесь на самом деле можно увидеть что-то вроде такого:
procedure TReport13Form.OnButton1Click(Sender: TObject);
begin
with MainForm.WorkQuery.SQL do
begin
Add('Select ...');
if ... then
Add(', ...');
Add('from ...');
if ... then
Add('join...');
... // Ещё строк двадцать-тридцать подобного трэша.
Add(Format('where id = %d and operDate = %s',
[id, QuotedStr(DToS(DatePicker1.Date))]));
… и прочие подобные непотребства.
Но переписать всё с нуля — это что-то из области радикального религиозного фанатизма (как если бы кто-то взвизгнул и автоматически замахнулся тапком, только услышав про Delphi 7).
Из опыта скажу: можно. Да, в результате нарушается стилевое единообразие исходного текста программы, но это далеко не худшее, что может произойти. Худшее — «стать одним из них»(1).
1) Важное примечание: нисколько не ущемляю право старорежимных специалистов на работу в подобном стиле и соответствующую ментальность в условиях глубоко прикладной разработки. У себя на работе (компания НЕ производит ПО, разработка обслуживает интересы основного бизнеса) в этом смысле получил идеальное разграничение интересов: я веду несколько проектов, пишу как хочу, думаю о технологии и принципах проектирования, использую git (2). Мне хорошо. У остальных тоже всё работает. Они тоже молодцы. Дела идут, контора пишет.
2) Таки да, больше никто git не использует — так и не смог убедить. Я же без систем контроля версий процесса разработки не представляю ещё с тех пор, как пользовался CVS (git тогда ещё не родился) и читал новинку Кента Бека про «Экстремальное программирование».
Итак, смешение стиля неизбежно. Можно ли при этом хотя бы в активно дорабатываемой части «писать красиво»? И чтобы не получилось, как в мультике: «И так ходили они красиво весь день, а потом медвежонок сказал: Есть хочу!»? Думаю, что скорее да, даже несмотря на то, что часто приходится переключать мозги на ржавые рельсы ушедших времён, и это, несомненно, накладывает отпечаток. Но получится ли, к примеру, православный SOLID?
Что же, поищем ответ. За примерами далеко ходить не надо — недавно написал микро-библиотеку для сохранения настроек внешнего вида формы (положение разделителей, порядок и расположение столбцов в таблице, при желании — прочие фантазии). В библиотеке VCL, концептуально разработанной в середине девяностых, с гибкостью отображения вообще проблемы. На самом деле не великая задача, пара часов работы и несколько классов, но радикально сократилось время на поддержку сохранения настроек внешнего вида. Буквально до нескольких строк кода. Попробую оценить её на соответствие культурному коду современного ООП.
Библиотеку, с благословения руководства, выложил на Битбакет под собственным копирайтом: https://bitbucket.org/danik-ik/layoutkeeper/
Там же лежит пример использования.
Комментарии по схеме:
ILayoutKeeper — основной интерфейс, с которым взаимодействует пользователь. Реализация отвечает за сбор данных и их сохранение в конкретном хранилище.
ILayoutProcessor — отвечает за единицу хранения. Определяет строковый ключ, под которым сохраняются данные, преобразует отображает параметры компонента в строковую единицу хранения и обратно
Оба интерфейса имеют как абстрактные реализации (фреймворк) , так и конкретные функциональные решения, содержащие исключительно код, реализующий события фреймворка для предметной работы с хранилищем (Keeper) или внешним видом компонента (Processor).
Там ещё есть в публичной части фабрики, я просто не стал перегружать схему.
Итак, пройдём по пунктам.
S. Single responsibility principle
В форме (пользовательский код) осталось только то, что относится непосредственно к ней: регистрация компонентов, раскладка которых сохраняется, и загрузка/сохранение раскладки как реакция на события формы.
В самой библиотеке выделены интерфейсы и реализующие их абстрактные классы, обеспечивающие фреймворк. Конкретные реализации переопределяют минимальный набор событий фреймворка. Реализация отделена от фреймворка. Фреймворк отделён от реализации. Вроде, S.principle на месте.
O. Open-Closed principle
Сразу скажу, что значимость в части Closed может нивелироваться в зависимости от условий (единоличное владение проектом, невеликий объём кодовой базы, адекватные регрессные тесты), но где же вы видели хорошо покрытое тестами легаси? К тому же я имел наглость назвать это творение библиотекой, что налагает ответственность в части обеспечения интересов неопределённого круга пользователей. О, этот вечный вопрос совместимости версий! Поэтому данный принцип одним махом выходит в цари горы.
В данном случае ключевым фактором становится наличие интерфейсов. Мало того, что именно в Delphi на интерфейсах работает чёрная магия подсчёта ссылок, так ещё и при желании можно без малейшего зазрения совести выкинуть даже приведённый в библиотеке фреймворк, и написать свою реализацию с нуля, но переиспользовать, например, процессоры, потому, что интерфейсы поделены в соответствии с принципом I. Впрочем, абстрактные классы, как правило, могут быть использованы как есть для реализации (Open) и не требуют при этом вмешательства в собственный код (Closed). Например, в реализации кипера мы можем использовать для хранения БД, или можем сохранять отдельные настройки для каждого из пользователей. При этом с точки зрения использующего библиотеку кода ничего не изменится (разве что где-то в одном месте поменяется фабрика).
L. Liskov substitution principle
Самый трудный для понимания, потому, что чаще всего рассматривается на неправильном (личное мнение) примере: нам СНАЧАЛА предлагают спроектировать иерархию наследования (предлагают спроектировать сферические прямоугольник и квадрат в вакууме), и лишь ЗАТЕМ, при анализе решения, предлагают задаться вопросом, зачем это было надо, и соответствует ли решение высказанным «когда нибудь потом» пожеланиям. А тут всё (почти) просто: поведение наследника должно оправдывать ожидания от родителя. Если мы не ждём от прямоугольника, что у него будут независимо меняться ширина и высота (может, он вообще иммутабельный), то квадрат от него наследовать можно (в рамках исходной задачи).
Чтобы проверить на соответствие данному принципу, следует а) понять, что мы хотим от заявленных интерфейсов и б) рассмотреть, чего мы НЕ делали для решения задачи.
По (а) всё прозрачно, приведённые примеры, кажется, полностью оправдывают ожидания, а учитывая соблюдение S там вообще сложно что-то испортить — интересы наследников не пересекаются с родителями (абстрактный фреймворк и реализация — это совсем о разном), а реализацию интерфейса за наследование в этом смысле можно не считать, ибо интерфейс сам по себе поведения не имеет.
По (б) — ежели пускаться во все тяжкие, то можно было бы сделать класс «сохраняемой формы» назвать его тоже TForm, унаследовать его от Forms.TForm и всегда вписывать модуль с новым классом в uses после Forms, чтобы использовался именно допиленный класс. Вы ещё не заплакали? Между тем, есть в проекте ряд компонентов, которые издревле именно так и завёрнуты (хитроподвывернуты?). И это реально работает, хотя и прямо нарушает принципы S и L.
I. Interface segregation principle
Тут говорить особо не о чем: ни один из классов не стремится объять необъятное и даже не наследует более одного интерфейса. Собственно, это прямое следствие S. И собственно хранилище (ILayoutKeeper) полностью обособленно от «процессора» (ILayoutProcessor), который определяет ключ для хранилища и преобразует реальную раскладку в её строковое представление и обратно. Принцип не то, что соблюдается — тут после S нужно ещё много нелепых телодвижений совершить, чтобы его нарушить.
D. Dependency inversion principle
Хм. Считаю, что один только факт того, что библиотека была абсолютно безболезненно выделена как самодостаточный проект, говорит за соблюдение этого принципа. Если идёт сохранение в ini-файл, то здесь нет (и не должно быть, спаси, Господи) ссылки на MainForm, где где-то в публичной секции лежит эта инишка. Конкретный объект инжектится из пользовательского кода. Если будет, к примеру, сохранение в БД — инструмент доступа к БД тоже будет передан извне. Библиотека вызывает методы объекта пользовательского кода (инишка), но не имеет зависимости от него (зависимость идёт строго в обратном направлении). Изменение пользовательского кода никоим образом не будет поводом для изменения библиотеки. В этом смысле D перекликается с S в современном понимании от дяди Боба, только на уровне библиотеки, а не класса — повод для изменения классов библиотеки может найтись только в ней же, в её логике. Зависимость от стандартной библиотеки, конечно же, зависимостью от пользовательского кода не является. Реализация «процессора» с зависимостью от коммерческой библиотеки EhLib вынесена в отдельный модуль и может использоваться исключительно по потребности, при наличии соответствующих компонентов в проекте.
Единственная имевшая место «грязная» зависимость — модуль хелперов OeStrUtil, где многие функции работают в бизнес-контексте, и от которого для публичной версии я взял чистый по зависимостям огрызок. Можно было бы переписать разбор строк и на библиотеку из стандартной поставки (RxStrUtils.ExtractWord, к примеру), и я даже поначалу сделал это. Но что-то в полученном коде было настолько старорежимное, что я ничтоже сумняшеся стёр эту версию к свиньям собачьим. Пусть будет полезняшка в комплекте.
И как там было, когда я писал курсовые? Ах, да. Вывод.
В старорежимных проектах компромиссы практически неизбежны. Тем не менее, при желании остаётся возможность для стремления к лучшему. Самое суровое наследие ушедших времён часто позволяет создавать в себе «островки безопасности», которые могут постепенно разрастаться и со временем захватывают всё большую территорию в масштабах любимого болотца. А SOLID-принципы в определённой мере обладают синергетическим эффектом, обеспечивающим им взаимное влияние и поддержку. При этом ключевую роль играют взаимодополняющие S и I принципы, D и O необходимы для библиотек с неопределённым кругом пользователей (всё, что больше pet-проекта или действующего макета в натуральную величину), а L весь про то, что проектировать иерархию классов надо начинать только тогда, когда поймёшь, зачем эти классы вообще нужны (и что при этом может измениться, но это уже в зоне S и O).