Какие бы вы предложили принципы для современной разработки программного обеспечения?
На прошлом виртуальном митапе Extreme Tuesday Club мы обсуждали, не устарели ли SOLID принципы. Не так давно я толкнул шутливую речь на эту тему, так что один из организаторов митапа спросил у меня - раз я несогласен с SOLID, чем бы я его заменил. Так получилось, что я уже думал об этом какое-то время, так что я решил предложить пять своих принципов, из которых получился акроним CUPID.
Статья не про эти принципы, об этом будет мой следующий пост. Эта статья о том, почему я думаю, что они нужны. Я бы хотел рассказать всю историю и объяснить, почему я никогда не покупался на SOLID. Поговорим об этом.
Почему неправилен каждый элемент SOLID.
PubConf был изобретён как разновидность афтепати к конференциям NDC. В соответствии с именем, место происходит в пабе. Несколько спикеров выступают в стиле Ignite - 20 слайдов, 15 секунд на слайд, автопродолжение - и аудитория ревет, хлопает, мечет молнии и благодарит в соответствии с местом, где это происходит. Победитель что-нибудь получает, все здорово проводят время.
Несколько лет назад я был приглашён спикером на PubConf эвент в Лондоне. Мне нравится челлендж выступлений с ограничениями. Я долго думал про SOLID принципы Роберта Мартина, и мне показалось забавным опровергнуть каждый из этих принципов, пытаясь сохранить при этом серьёзное лицо. Также я хотел для каждого пункта предложить альтернативу.
Некоторые речи пишут сами себя: я понял, что могу использовать один слайд на каждый принципе, один, чтобы опровергнуть его, один, чтобы предложить альтернативу - и так 5 раз. Итого 15 слайдов, по 45 секунд на принцип. Добавь начало и конец - и вот мои 20 слайдов!
По мере того, как я писал, я заметил две вещи. Во-первых, опровергнуть принципы было гораздо легче, чем я думал (за исключением принципа подстановки Барбары Лисков, так что там пришлось зайти с другой стороны). Во-вторых, альтернативой раз за разом оказывалась одна мысль: Пишите более простой код. Опровергнуть её довольно легко вопросом "что вообще означает "простой"?", но у меня было хорошее определение для этого, так что я не слишком волновался.
После конференции я выложил слайды на SpeakerDeck и целая куча людей ,которых я раньше не встречал, встретили в штыки сначала мою речь, потом детали моих слайдов, а потом и меня лично.
Посколько я никогда это не записывал, вот примерная схема того, как пошёл разгвор. Держите в голове, что на каждый принцип у меня было 15 секунд на то, чтобы его представить, 15 секунд на то, чтобы его опровергнуть, и 15 секунд на альтернативу. Готовы? Поехали!
Принцип единой ответственности (SRP)
SRP принцип утверждает, что код должен делать только одну вещь. Другое определение гласит, что код должен "иметь только одну причину для изменений". Я назвал это "Бессмысленный Слишком Общий Принцип" (в оригинале “Pointlessly Vague Principle” - прим. переводчика). Что вообще означает "одна вещь"? Является ли DataProcessor ETL (extract-transform-load - извлечение данных - их трансформация - их загрузка) одной вещью или тремя? Любой нетривиальный код может иметь любое количество причин, чтобы измениться, которые вы могли или не могли учитывать, так что, опять же, для меня это несёт не очень много смысла.
Вместо этого я предложил писать более простой код, используя для метода измерения эвристику "Помещается в Голове". Это означает, что вы можете утверждать что-то только о том, что помещается в вашей голове. Напротив, если что-то не помещается в вашей голове, то вы не сможете рационально утверждать что-то об этом предмете. Код должен помещаться в голову на любом уровне детальности, идёт ли речь о методе/функции, классе/модуле, компоненте, сделанном из классов, или о целых распределённых приложениях.
Вы можете спросить: "О чей голове речь?". Для целей эвристики я предположил, что владелец этой головы может читать и писать код в языках, которые используются в проекте, а также знакомство с доменной областью. Если для целей разработки требуются более эзотерические знания, например, знания недокументированных внутренних систем для интеграции, то это должно быть специально обозначено в коде так, чтобы знание об этом поместилось в голову.
На каждой шкале должно быть достаточно концептуальной целостности, чтобы ы могли охватить "целое" на этом уровне. Если не получается, то это достаточная эвристика, чтобы понять, что вам не хватает реструктуризации кода. Иногда можно связать несколько вещей вместе, и они всё равно поместятся в вашей голове. Объединение даже может упростить рассуждение об этих вещах, нежели если они были бы искуственно разделены из-за того, что кто-то настаивал на принципе единой ответственности (SRP). В других случаях имеет смысл разделить что-то с единой ответственностью на несколько модулей, просто чтобы об этом можно было проще рассуждать.
Принцип открытости-закрытости (OCP)
В идее этого принципа лежит, что код должен быть открыт для расширения, т.е. легко расширяться без изменений и закрыт для изменений, т.е. вы доверяете тому, что делает этот код и вам не нужно больше никогда с ним возиться.
Это был мудрый совет во времена, когда код:
-
Было дорого изменять: Попробуйте поменять маленький кусок кода, а затем сделать компиляцию и линковку миллионов строк в C++ в 1990 году. Я подожду.
-
Рискованно изменять: у нас не было ни правил рефакторинга, не было IDE с нормальным рефакторингом (кроме Smalltalk), не было практик разработки.
-
Самое важное, что можно добавить: вы писали какой-то код, помещали его в систему контроля версий (если использовали её, то, скорее всего, использовали RCS или SCCS (подробнее о них тут - прим. переводчика)), а затем переходили к следующему файлу. Вы транслировали спецификацию в код, по одному кусочку за раз. Переименовывать вещи было редкой практикой, не говоря уже о переименовании файлов. CVS, которая стала вездесущей системой контроля версий, буквально забывала всю историю файла, если вы его переименовывали, поэтому переименованию было такой редкостью. Этот пункт легко проглядеть в век автоматического рефакторинга и CVS, основанных на целом наборе изменений (changeset-based version control.)
Сегодня, подходящий совет, чтобы менять код, звучит так: Меняйте код, если он должен делать что-то ещё! Звучит банально, но сейчас мы воспринимаем код, как, скорее, податливую глину, нежели чем в старые добрые деньки, когда код напоминал строительные блоки. Между спецификацией и кодом не было цикла обратной связи, в то время как сейчас у нас есть автоматизированные тесты.
В этом случае я вывел "Принцип Аккреции Круфта". (Cruft Accretion Principle) (Аккре́ция(лат. accrētiō «приращение, увеличение» от accrēscere «прирастать») — повышение массы одного космического объекта за счет гравитационного притяжения. - прим. переводчика)
Код - это не "актив", который надо аккуратно упаковать и сохранить, это скорее стоимость, долг. Весь код для нас - это затраты. Так что если я могу взять большую кучу затрат и заменить их меньшей кучей, тогда я выиграл! Пишите простой код, который можно легко изменить, и у вас будет код, который легко может быть как открытым, так и закрытым, в зависимости от необходимости.
Принцип подстановки Лисков (LSP)
Это тот же Принцип наименьшего удивления, применяемый к подстановке кода, и в этом смысле он довольно разумный. Если я скажу, что что-то является валидным подтипом того, что у вас уже есть, тогда вы естественно полагаете, что оно и поведёт себя также, как вы от него ждёте.
Однако, язык, который использует LSP ("подтипы"), в сочетании с тем, что большинство разработчиков объединяет вместе "подтипы" и "подклассы", и ожидание "желаемых свойств" означает, что он пытается аппелировать к моделированию сущностей из 1980 года, со всем его "является (is-a)" и "имеет (has-a)" видами наследования.
В контексте моделирования, когда мы нож используем в качестве отвертки, а многие объекты наследуются видами: "ведёт-себя-как (act-like-a)", "иногда-используется-как (sometimes-be-used-as)" или "подойдет-если-не-сильно-приглядываться (pass-off-as-a-if-you-squint)"; в этом контексте что мы действительно хотим, так это маленькие, простые типы, которые мы можем композировать в любые насколько угодно сложные структуры, и примириться со всеми нюансами, которые это вызовет. Мой совет, внезапно, "писать более простой код", о котором легко рассуждать.
Принцип разделения интерфейсов (ISP)
Из всех принципов этот разобрать - как два пальца об асфальт. По каким-то причинам, этот пункт вызвал больше всего споров, но как по мне, его развенчать проще всего. Во время исследований при подготовке речи, я обратил внимание, что этот паттерн появился, когда Роберт Мартин столкнулся с God object, во время работы над софтом в Xerox. Всё происходило в классе под названием Job
. Его подход к упрощению был в том, чтобы найти все места, где он использовался, выяснить, какие методы "сочетаются друг с другом" и объединить их в отдельный интерфейс. Это принесло сразу несколько преимуществ:
-
Сбор связанных методов в разных интерфейсах показал все различные обязанности, которые выполнял класс
Job
. -
Присвоение каждому интерфейсу имени, раскрывающего намерение, упростило понимание кода, чем просто работа с объектом
Job
, встречающимся то тут, то там. -
Создана возможность разбить класс
Job
на более мелкие классы, реализующие свои интерфейсы. (Возможно, интерфейс им больше не нужен.)
Всё это имеет смысл, просто дело в том, что это не принцип. Это паттерн.
Принцип хорош в любом контексте: "Сначала пытайтесь понять, а потом быть понятым", "Будьте добры друг к другу".
Паттерн это стратегия, которая хорошо работает в определённом контекесте (God-класс). У неё есть преимущества (меньшие компоненты) и компромиссы (больше классов, которыми нужно управлять). Принципом было бы, скорее, "вообще не устраивайте в коде бардак, который к такому привёл!".
Так что моя позиция в споре была такая: если это и был принцип, то это был "Принцип Двери из Конюшни" (Stable Door Principle). (дверь, разделенная на две части - верхнюю и нижнюю. Представьте, как сверху в открытую часть выглядывает лошадка - прим. переводчика) Если у вас изначально были небольшие, основанные на ролях классы, то вы бы не оказались в ситуации, где пытаетесь декомпозировать этот огромный запутанный беспорядок.
Конечно, иногда мы оказываемся в таком положении. Тогда разделение интерфейсов - идеальная стратегия, чтобы хоть немного упорядочить бардак, наряду также с созданием тестами характеристик и другими советами Майка Фезарса из прекрасной книги Working Effectively With Legacy Code.
Принцип инверсии зависимостей (DIP)
С DIP нет ничего фундаментально неправильного. Но думаю, не будет преувеличением сказать, что наша одержимость с инверсией зависимостей, в одиночку вызвала миллиарды долларов невозвратных затрат и потерь за последние пару десятилетий.
Реальным принципом должна была стать опциональная инверсия. Зависимость интересна нам только тогда, когда есть несколько способов её предоставления, и вам нужно сделать инверсию отношений только если вы считаете, что в будущем это станет необходимо. Это довольно высокая планка, а чаще всего вам просто нужен только метод main
.
Если вместо этого вы присоединитесь к идее, что все зависимости всегда должны инвертироваться, то вы закончите с J2EE, OSGi, Spring или любым другим фреймворком "декларативной сборки", где сама структуризация компонентов - закрученный лабиринт конфигураций. J2EE заслуживает отдельного упоминания, что каждый тип инверсии - EJB, servlets, web domains, remote service locations, даже конфигурация конфигураций - должен принадлежать разным ролям.
Существуют даже целые кодовые базы, где каждый класс реализует точно один интерфейс, который существует, чтобы удовлетворить DI фреймворк, или инжектнуть мок или стаб для автоматизированного тестирования. Обещание, что "с DI вы сможете легко поменять базу данных" испаряется как только вы, кхм, пытаетесь поменять базу данных.
Большинство зависимостей не нуждаются в инверсии, поскольку большинство зависимостей не являются одним из вариантов внедрения, это просто то, как мы делаем вещи на этот раз. Моё - думаю, уже неудивительное предложение - писать более простой код, нежели сосредотачиваться на переиспользовании.
«Если они тебе не понравились, у меня есть ещё»
Когда я смотрю на SOLID, я вижу сочетание вещей, которые когда-то были хорошими советами, шаблонов, применимых в контексте, и советов, которые легко применить неправильно. Я бы не стал предлагать это как бесконтекстный совет начинающим программистам. Так что бы я сделал вместо этого? Я подумал, что для каждого из принципов и паттернов SOLID может быть однозначное соответствие, поскольку в любом из них нет ничего плохого или неправильного, но, как говорится, «Если бы я поехал в Дублин, я бы не стал не начинать отсюда ».
Итак, учитывая то, что я узнал о разработке программного обеспечения за последние 30 лет, могу ли я предложить какие-либо принципы вместо этого? И могли ли они образовать емкую аббревиатуру? Ответ положительный, и я изложу их в следующей статье.
Комментариев нет:
Отправить комментарий