Реальность повторного использования

в 2:46, , рубрики: DRY, reuse, WYSIATI, библиотека, Даниел Канеман, думай медленно, мышление, повторное использование кода, Программирование, решай быстро, управление разработкой, фреймворк

Говорят, что не нужно изобретать велосипед. На первый взгляд это кажется очевидным. Если вы потратили время на разработку, то зачем делать это снова, можно ведь повторно использовать старое решение? Казалось бы, со всех сторон хороший вариант. Но не всё так просто. Как старый «седой» программист, я видел, как организации снова и снова становятся жертвами этого заблуждения, вкладываясь в предварительный дизайн и разработку, но никогда не достигая обещанного массивного ROI благодаря повторному использованию. На самом деле я считаю, что наша чрезмерно оптимистичная оценка пользы и простоты повторного использования — одна из самых распространённых и опасных ловушек в разработке ПО.

Я считаю корнем проблемы то, что Даниел Канеман сформулировал как правило «Что видишь, то и есть». Оно в двух словах объясняет жёсткие ограничения человека на быстрое принятие решений, используя только имеющиеся данные и некоторые базовые эвристики. Замедление мышления требует времени и дисциплины — так что вместо этого мы пытаемся заменить сложные проблемы, которые не полностью понимаем, простыми.

В случае повторного использования интуиция просто и убедительно представляет труднодоступную в физическом мире аналогию программного обеспечения как «колесо», которое не следует изобретать заново. Это удобная ментальная модель, к которой мы часто возвращаемся при принятии решений о повторном использовании. Проблема в том, что такое представление о повторном использовании ошибочно или, по крайней мере, удручающе неполно. Давайте посмотрим, почему…

(Краткое предостережение: я говорю о крупномасштабном повторном использовании, а не об уровне методов и функций. Я совершенно не собираюсь применять принцип DRY на более низких уровнях детализации. Также я представляю повторное использование как применение неких сервисов, библиотек и прочего, созданного внутри компании, а не снаружи. Я не рекомендую создавать собственный фреймворк JS MVC! Ладно, теперь вернемся к первоначально запланированной статье...)

Интуиция

Представьте систему A, которая внутри содержит некоторую логику C. Вскоре должна быть построена новая система B, и ей тоже нужна такая же базовая логика C.

Реальность повторного использования - 1

Разумеется, если мы просто выделим C из системы A, то можно использовать её в системе B без необходимости повторной реализации. Таким образом, экономия составляет время на разработку C — её понадобилось реализовать только один раз, а не снова для B.

Реальность повторного использования - 2

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

Опять же, логика здесь простая и, казалось бы, железная — зачем разрабатывать несколько экземпляров C, если можно просто разработать один раз, а затем повторно использовать. Проблема в том, что картина более сложная — и то, что кажется лёгким трамплином для ROI, может превратиться в дорогую смирительную рубашку. Вот несколько вариантов, как наша базовая интуиция по поводу повторного использования может нас обмануть…

Реальность

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

Кроме того, почти всегда C нуждается в других вещах для работы (например, в других библиотеках, служебных функциях и т. д.). В некоторых случаях это общие зависимости (т. е. и A, и C нуждаются в них), а в некоторых случаях нет. В любом случае, простая картина A, B и C может выглядеть уже не такой простой. Для этого примера предположим, что A, B и C используют общую библиотеку L.

Реальность повторного использования - 3

Другая проблема — изменение: у разных пользователей C часто будут несколько разные требования к тому, что она должна делать. Например, в C может быть какая-то функция, которая должна вести себя немного иначе, если её вызывает А, чем если её вызывает B. Общим решением для таких случаев является параметризация: данная функция принимает некоторый параметр, который позволяет ей понять, как себя вести с учётом того, кто её вызвал. Это может работать, но увеличивает сложность C, и логика тоже захламляется, поскольку код наполняется блоками вроде «Если вызов из А, то запустить такой блок логики».

Реальность повторного использования - 4

Даже если C действительно идеально подходит для A и B, всё равно изменения почти наверняка придётся делать с появлением новых систем, скажем, D и E. Они могли бы использовать C как есть, но тогда им самим понадобится доработка, небольшая или побольше. Опять же, каждое новое приспособление C представляет дополнительную сложность — и то, что раньше было легко в ней понять, теперь становится намного сложнее, поскольку C превращается во нечто большее, удовлетворяя потребности D, Е, F и так далее. Что приводит к следующей проблеме…

По мере роста сложности, разработчику всё сложнее понять, что делает C и как её использовать. Например, разработчик A может не понимать какой-либо параметр для функции C, поскольку он относится только к системам E и F. В большинстве случаев необходим некоторый уровень документации API (возможно, Swagger, Javadoc или более), для пояснения входа и выхода, исключительных условий и других SLA/ожиданий. И хотя документация сама по себе является хорошей вещью, она не лишена своих проблем (например, её нужно поддерживать в актуальном состоянии и т. д.).

Реальность повторного использования - 5

Другим следствием повышенной сложности является то, что становится труднее поддерживать качество. Теперь C служит многим хозяевам, поэтому появляется много пограничных случаев для тестов. Кроме того, поскольку теперь C используется многими другими системами, влияние любого конкретного бага усиливается, так как он может всплыть в любой или всех системах. Часто при внесении любого изменения в C недостаточно протестировать только общий компонент/сервис, а требуется некий уровень регрессионного тестирования также А, B, D и всех остальных зависимых систем (неважно, используется сделанное изменение С в этой системе или нет!).

Опять же, поскольку мы говорим о повторном использовании в нетривиальном масштабе, то вероятно C придётся разрабатывать отдельной группе разработчиков, что может привести к потере автономии. У отдельных групп обычно свои собственные графики выпуска, а иногда и собственные процессы разработки. Очевидный вывод в том, что если команде A нужно какое-то улучшение в C, то ей вероятно придётся работать через процесс C, т. е. чемпион А должен предоставить требования, отстаивать свой приоритет и помогать в тестировании. Другими словами, команда A больше не контролирует свою собственную судьбу в отношении функциональности, которую реализует C — она зависит от команды, которая поставляет C.

Реальность повторного использования - 6

Наконец, при обновлении С, по определению, появляются разные версии. В зависимости от характера повторного использования могут возникнуть различные проблемы. В случае повторного использования на этапе сборки (например, в библиотеке) разные системы (A, B, и т. д.) могут остаться со своими рабочими версиями и выбирать подходящий момент для обновления. Недостаток в том, что существуют разные версии C и есть вероятность, что какую-то одну ошибку придётся исправлять во всех версиях. В случае повторного использования во время выполнения (например микросервис) C должна либо поддерживать несколько версий своего API в одном экземпляре, либо просто обновиться без оглядки на обратную совместимость и, таким образом, заставить A и B обновиться тоже. В любом случае, значительно возрастают требования к надёжности и строгости процессов и организаций для поддержки такого повторного использования.

Реальность повторного использования - 7

Вывод

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

Даже после тщательного анализа, если крупномасштабное повторное использование является правильным, нужно принять решение о том, каким образом это сделать. Опыт подсказывает быть осторожнее со стрелками зависимости. Повторное использование, когда «повторный пользователь» находится под контролем, почти всегда проще реализовать и им проще управлять, чем повторным использованием, когда повторно используемый ресурс обращается к системе. В примере выше, если бы C была библиотекой или микросервисом, то A и B получили бы контроль. На мой взгляд, это ускоряет реализацию и уменьшает уровень управления/координации в долгосрочной перспективе.

Превращение C во фреймворк или платформу переключает стрелки зависимостей и усложняет контроль. Теперь A и B обязаны C. Такой тип повторного использования не только сложнее реализовать (и сделать это правильно), но он в дальнейшем приводит к более сильной блокировке (т. е. A и B полностью зависят от C). Народная мудрость гласит, что «библиотека — это инструмент, фреймворк — это образ жизни».

Реальность повторного использования - 8

Наконец, хотелось бы узнать ваше мнение или опыт крупномасштабного повторного использования. Когда оно работает, а когда терпит неудачу?

Автор: m1rko

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js