Мы часто говорим кому-то фразы наподобие «Мне нужно всего лишь несколько часов, чтобы это сделать/реализовать/внедрить». Но спустя какое-то время после окончания работ вдруг осознаём, что нам приходится регулярно исправлять баги и ошибки, объяснять устройство и алгоритм работы другим инженерам и разработчикам, или помогать отвечать на вопросы клиентов. И получается, что объём времени, затраченного на поддержку вашего детища, многократно превышает те несколько часов, что ушли на его создание.
В разработке ПО одним из самых трудно усваиваемых правил является то, что с увеличением сложности продукта растут и скрытые издержки. Иногда сложность является лишь следствием каких-то иных проблем. Очень непросто найти баланс между «продающими» и «нагрузочными» свойствами продукта, особенно в свете определения его стоимости и регулирования спроса и предложения. Столь же трудно заниматься поддержкой качества продукта и расширять круг потребителей. Или, скажем, разработать текстовый редактор с богатыми возможностями, одинаково быстро и устойчиво работающий на всех устройствах и поддерживающий совместную работу над документами в реальном времени. Иными словами, зачастую приходится усложнять продукт, чтобы он стал успешным на рынке.
С другой стороны, нам зачастую приходится бороться со сложностью продукта, которую мы сами же и допустили. Скажем, написали приложение на языке, которым владеет очень мало сотрудников, и в будущем сталкиваемся с трудностями поддержки. Или внедрили новую, свежую технологию, которую очень захотелось попробовать, начитавшись обзоров, но выяснилось, что у неё есть ряд недостатков, о которых никто не подозревал. Или разработали новую функцию, которая нужна очень ограниченному числу пользователей, но на исправление багов в ней тратится непропорционально много времени и сил.
Усложнение продукта ведёт к росту скрытых издержек. Решения, принимаемые нами в ходе разработки ПО, влияют не только на скорость выпуска продукта. Они отражаются и на том, сколько времени и сил уйдёт на продвижение и поддержку. Причём избыточная сложность иметь место не только в коде или функционале продукта, этим страдает и организация работ в команде разработчиков, и структура самой команды.
Сложность кода
Сложность кода не проявляет линейной зависимости от количества строк. Она растёт комбинаторно: каждая строка кода может взаимодействовать со многими другими строками. И степень усложнения в этом случае осознать очень тяжело, поэтому многие разработчики склонны преуменьшать время, необходимое для создания большого и сложного проекта. В частности, это одна из причин того, что переписывании проекта зачастую срываются все сроки.
Когда код сильно усложняется, то становится труднее расширять его, обдумывать его в целом, обнаруживать баги. Тяжело разбираться во взаимосвязях и потоках данных, когда нужно найти причину той или иной ошибки. При этом разработчики периодически стараются вообще не касаться каких-то наиболее сложных частей кода, даже если внесение туда изменений было бы самым логичным шагом. Или избегают сообща работать над самыми трудными местами, хотя это может быть куда эффективнее.
Системная сложность
Инженерам и программистам-разработчикам нравится возиться с новыми технологиями, приложениями, приборами, устройствами и компонентами. Это проявление врождённого любопытства, обычно свойственного людям этих профессий. Большинство из них считают, что очередная новая технология может волшебным образом решить какие-то текущие проблемы. Например, когда в 2011 году в Pinterest взялись за расширение своего ресурса, они использовали шесть различных технологий хранения и баз данных: MySQL, Cassandra, Membase, Memcache, Redis и MongoDB. И всё это разнообразие обслуживала команда бэкэнд-инженеров из трёх человек. Каждое из решений, с которым они экспериментировали, на бумаге обещало решить какие-то ограничения их системы. Но на практике оказалось, что все эти продукты не справлялись, причём каждый по-своему. При этом на управление и поддержку уходило много времени и сил. В конце концов, в Pinterest пришли к решению, что будет проще масштабировать свой сервис путём добавления новых серверов, а не внедряя новые программные технологии. Команда отказалась от части продуктов и сконцентрировалась на развитии оставшихся.
К росту скрытых издержек приводит и фрагментирование инфраструктуры на слишком большое количество систем. Все они будут требовать немало внимания, станет труднее объединять ресурсы для создания многократно используемых библиотек для каждой системы, труднее повседневно обслуживать, разбираться в причинах конкретных сбоев и быстро восстанавливать работоспособность. Да и отдача от каждой системы вряд ли будет максимальной, поскольку им будет уделяться меньше времени.
Сложность продукта
Продукт может оказаться чрезмерно усложнённым из-за чьего-то ошибочного видения или неумеренных амбиций, что приводит к размытию задач и целевой аудитории продукта. Иногда сам факт того, что разработчики хотят стать лучшими везде, вместо того, чтобы сконцентрироваться на чём-то ключевом, уже может говорить о неспособности объяснить пользователям назначение продукта. Его усложнение приводит и к повышению системной сложности и сложности кода. Команда начинает добавлять новые функции, внедрять новые модули для их поддержки, раздувать кодовую базу. Если продукт уже обладает широким функционалом, то добавление новой возможности или изменение существующей приводит к тому, что нужно тратить ещё больше усилий для понимания и освоения всего этого обилия.
Сложные продукты обычно страдают большим количеством багов и дыр в безопасности. Инженерам и специалистам по обработке и анализу данных приходится учитывать куда больше переменных и писать больше отчётов, вместо того, чтобы сконцентрироваться на том, как пользователи используют продукт. Члены команды постоянно переключают своё внимание между различными проектам. А время, потраченное на поддержание функционала продукта, могло бы быть потрачено на оптимизацию кода, оплату «технического долга» и прочие полезные действия.
Организационная сложность
Все три вышеперечисленных вида сложности приводят к усложнению организационной структуры команды разработчиков. Приходится нанимать новых сотрудников, ведь из-за разрастания продукта возникает нехватка рук. А большие команды требуют больше коммуникаций, больше координации действий, что не всегда улучшает эффективность. Да и сам процесс поиска сотрудников, со всеми этими собеседованиями, может отнимать немало времени у членов команды. А ведь новичков приходится ещё и обучать, вводить в курс дела.
Альтернативой набору персонала может стать разделение команды на более мелкие группы. Возможно, в некоторых из них даже будет по одному человеку. Таким командам можно назначить разные зоны ответственности. Это позволяет предотвратить избыток коммуникативных связей, но у «команд одного человека» тоже есть ряд недостатков: снижение мотивации, ограниченная продолжительность рабочего процесса, ухудшение способности к обучению и т.д. Такой специалист-сам-себе-команда не может посоветоваться или получить помощь от коллег, потому что он один, и потому решение каких-то задач может занимать у него немало времени. В командах можно быстро получить мнение от коллег о своей работе, которые укажут на ошибки, подбодрят. А одиночки и этого не будут получать. В результате это часто приводит к снижению производительности, падению качества работы и… непреднамеренному усложнению кодовой базы или инфраструктуры.
Как бороться со сложностью
В 1980 году Энтони Хоар сказал: «Есть два пути создания программной архитектуры. Первый — сделать её настолько простой, чтобы в ней очевидно не было недостатков. Второй — сделать её настолько сложной, чтобы в ней не было очевидных недостатков». Как же нам защититься от издержек, связанных с неочевидными недостатками, порождёнными сложностью?
Можно использовать разные стратегии.
Оптимизировать ради упрощения. По мере возможности, не допускайте каких либо усложнений. Оценивайте стоимость поддержки. Спросите себя, стоит ли решение 20% проблемы того, чтобы проект был усложнён, или достаточно закрыть проблему на 80%?
Чётко определите для команды миссию или видение продукта. Авторы книги Team Geek рассказали, как они наставляли команду разработчиков Google Web Toolkit, вдохновив их на точное описание миссии. Последовавшие за тем обсуждения содержимого и формы подачи помогли выяснить, что ведущие разработчики на самом деле не согласны с общим видением того, как должен выглядеть конечный продукт! Команде пришлось сесть и обсуждать все высказанные точки зрения, и в результате была выработана такая цель: «Миссия GWT заключается в том, чтобы радикально улучшить опыт использования веба, позволив разработчикам применять существующие Java-инструменты для построения бескомпромиссного AJAX во всех современных браузерах». Сколько сил и времени они потратили бы зря, если бы не обсудили все свои разногласия на ранней стадии?
Создавайте большие системы из более простых компонентов. Google является хорошим примером организации, сосредоточившейся на создании мощных, базовых структур, которые потом широко используются во всевозможных приложениях. У них есть такие базовые компоненты, как Protocol Buffers, Google File System, серверы Stubby. Поверх этих компонентов они надстраивают другие структуры, наподобие MapReduce и BigTable. А на основе этого построено множество приложений, включая масштабный поисковик, сервис Google Analytics, кластер Google News, обработчик данных Google Earth, сервис анализа данных Google Zeitgeist и многое другое.
Чётко определяйте интерфейсы взаимодействия между модулями и сервисами. Уменьшение связей между модулями сервисами позволяет уменьшить комбинаторную сложность даже небольшого по объёму кода. В 2002 году Джефф Безос постановил, что Amazon будет двигаться в направлении сервис-ориентированной архитектуры, и что все команды разработчиков будут коммуницировать между собой только через service-level интерфейсы. Хотя эта трансформация потребовала огромных расходов на разработку, разделение кода и логики услуг облегчило создание очень успешной на сегодняшний день Amazon Web Services.
Периодически «оплачивайте технический долг». Мы всегда создаём приложения в условиях недостатка необходимых данных. В ответ на меняющиеся условия кодовая база разрастается, что ведёт к росту энтропии. Усложнение проекта становится своеобразным налогом на будущие разработки. И снизить этот «налог» можно выделением времени на соответствующие работы. Многие команды и разработчики занимаются этим в перерывах между проектами. Но даже однократных мероприятий, своеобразных субботников, иногда бывает достаточно. Например, в Quora как-то был проведён День Очищения Кода, во время которого разработчики занимались удалением неиспользуемого кода из кодовой базы.
Используйте статистику для избавления от невостребованных функций. В компании Yammer, когда кто-либо обнаруживает, что расширение функционала или его сохранение на прежнем уровне требует слишком больших усилий команды, то начинают анализировать статистику использования продукта, выясняя, востребован ли этот функционал пользователями. Если не востребован, или востребован слабо, то от этого функционала отказываются или ограничивают его. Подобная стратегия позволяет снизить издержки подобно тому, как упрощение кода снижает «технический долг».
Группируйте действующие проекты по тематикам. Это позволяет членам команды делиться подходящими решениями, облегчает обсуждение дизайна, разбор кода или создание многократно используемых библиотек.
Когда студенты учатся программированию, они могут позволить себе как угодно экспериментировать со сложными решениями, ведь по окончании обучения их проекты канут в небытие. Но ошибочные решения профессионала-разработчика могут существенно отразиться на его карьере. Не усложняйте вещи без нужды.