Жили-были в двух соседних деревушках Вилларибо и Виллабаджо две команды разработчиков. И те и другие делали ревью кода, писали тесты, приводили рефакторинг, но через год разработки в Вилларибо уже выпустили релиз и вышли в продакшн, а в Виллабаджо все еще проводят рефакторинг и чинят баги. В чем же дело?
Разработка ПО – область, подверженная рискам. В нашей сфере при наступлении одного или нескольких рисков, срок поставки рабочей версии может сдвинуться не на привычные и комфортные 10-20%, а на все 150-300%. И надо признаться, что это далеко не предел.
Мы можем либо скрестить пальцы и надеяться, что удача будет сопутствовать проекту во всем, либо признать, что по статистике большая часть проектов по разработке ПО «проваливается» и предпринять дополнительные усилия по ослаблению возможных рисков.
Моя практика показывает, что клиенты крайне неохотно работают по схеме T&M и чаще предпочитают Fixed Price. В условиях зафиксированной стоимости наступление рискового случая означает автоматическое снижение рентабельности проекта: сотрудники получают зарплату ежемесячно, а не за сданные проекты.
До Agile и XP вся ответственность за работу с рисками ложилась на менеджеров. В гибких методологиях разработчики гораздо больше вовлечены в процесс и делят ответственность с менеджерами. Однако, принципы XP и Agile – больше методологические, чем технологические. Я думаю, что с рисками эффективнее работать комплексно на всех уровнях, в том числе на самом низком уровне, т.е. во время проектирования и написания кода.
Почему об этом следует думать разработчику, если есть менеджер?
- Не секрет, что если факап случится, менеджмент примет единственное «супер-умное» решение: «давайте поработаем сверхурочно и в выходные»
- Премии сотрудники получают тоже обычно за в срок сданные, а не за проваленные проекты
- Чувство сделанного дела, в конце концов. Гораздо приятнее сдать проект во время и видеть улыбку клиента, чем с опозданием в полгода отвязаться от «трудного ребенка»
С моей точки зрения спокойная рабочая обстановка вместо авралов и бонусы – неплохая мотивация, чтобы начать заботиться об этом.
Лайфхак
Статья содержит большое количество терминов. Я специально подбирал ссылки на матераилы, удачно раскрывающие каждый из них. Ссылки не вынесены в отдельный раздел «почитать», а находятся прямо в тексте. Таким образом я постарался не рвать нить повествования.
Итак, рассмотрим основные риски в разработке ПО.
Ошибка календарного плана
При оценке трудозатраты были оценены не верно.
Методологическое ослабление риска
Команда оценивает трудоемкость задачи, используя Planning Poker.Критика
В программировании мы сталкиваемся с большим количеством уникальных задач. Не смотря на предыдущий опыт можно промазать в разы из-за неучтенных деталей.Дополнительные методы ослабления
Разработка Proof of Concept (прототипа)
У вас есть описание API клиента, но не понятно, как оно себя поведет в боевых условиях? Прототип на «коленке» поможет проверить ваши самые страшные гипотезы. Код прототипа скорее всего придется просто выкинуть. В качестве бонуса, спроектировать систему со второго раза наверняка получится лучше, чем с первого.Если есть опасения за нагрузку следует провести нагрузочное тестирование в самом начале на массиве сгенерированных данных, чтобы проверить жизнеспособность архитектуры под нагрузкой.
Исследовательское тестирование
Метод особенно хорош при работе с неизвестным API, унаследованным кодом и новыми фреймворками. До того, как вы начали, у вас есть только догадки относительно чужого кода. Возможно у вас есть документация, но она может нагло врать. Написание исследовательских тестов помогает подтвердить или опровергнуть ваши догадки.Четкие критерии приемки при оценке задач
Довольно часто клиент и программист по-разному понимают значение слова «готово». Для программиста «готово» — это код написан, а для клиента «готово» — это все сконфигурировано, выложено на сервер, данные загружены в систему. Если в команде есть формальные критерии готовности работ, то их будет сложнее проигнорировать при оценке.Появление новых требований или изменение существующих
В процессе работы появляется все больше требований или текущие требования меняются.
Методологическое ослабление риска
Короткие итерации с фиксацией требований (scrum) или continuous delivery (kanban).Критика
Работая маленькими итерациями мы постоянно вынуждены проводить рефакторинг и переделывать уже существующий код. Команда располагает требованиями только на две недели вперед и не видит полной картины. Цена промаха снижается, но и КПД становится меньше, потому что не обладая полнотой информации не известно на сколько «навороченная» архитектура потребуется проекту.Дополнительные методы ослабления
Изменение требований – это риск с почти 100% вероятностью наступления, поэтому стоит считать, что он уже наступил до начала проекта и заложить его в календарный план и оценку трудозатрат. Однако именно он наиболее болезненно воспринимается программистами. Психологически сложно выкидывать код, который хорошо работал, потому что, видите ли, бизнес-модель не сработала. К сожалению надо просто принять тот факт, что мы регулярно будем отправлять часть кода на помойку. Более того, чем больше старого кода и костылей мы выкинем, тем лучше.Lean и метод прогрессивного jpeg'а
В первую очередь сосредоточьтесь на самом важном функционале. Все «прибамбасы», без которых можно обойтись должны реализовываться в конце проекта. Нужно показывать и скрывать панель? Для начала пойдет простой hide и show. Сложную анимацию можно добавить потом.Слабая связанность и модульное проектирование, onion-архитектура
К счастью на сегодняшний день существует множество готовых решений, обеспечивающих слабую связанность. Пойте мантру S.O.L.I.D, ежедневно на работе. Мыслите интерфейсами, а не реализациями. Потенциально, любая реализация может отправиться на помойку. Заведите за правило использовать принцип IOC/DI. Если у вас много JavaScript-кода обязательно пользуйтесь RequireJS или одним из фреймворков, иначе утонете в в болоте jQuery-лапши из колбеков, обращений к DOM, селекторов и логики.Откажитесь от идеи спроектировать все приложение в едином стиле. Делите его на подсистемы. Вам придется заплатить за это некоторым дублированием кода, но возможность выкинуть и переписать с нуля целую подсистему приложения, не разломав при этом другие части гораздо важнее.
Эволюционный рефакторинг совместно с классическим проектирование сверху-вниз
Людей, которых действительно можно назвать System Architect очень-очень мало. Настоящий архитектор должен поучаствовать в трех-четырех действительно больших проектах, парочку из них завалить, написать и выбросить вагон и маленькую тележку кода. Если у вас есть такой человек — честь ему и хвала. Беда в том, что у него не будет времени писать код. Его работой на полную ставку станет проектирование на уровне крупных блоков и контекстов системы. Проектирование, порой, целых подсистем уйдет на откуп Team Lead’ам и старшим разработчикам. Кто-то из них справится с задачей, а кто-то – нет, поэтому важно, чтобы все модули были независимы.Устанавливайте взаимоотношения разработчик-клиент между смежными командами, например команда UI – клиент команды Backend.
Такой подход дает следующие преимущества:
- Вы избавляетесь от священного идола и готовы постоянно проводить рефакторинг и улучшать качество кода. Могут появиться «пахнущие» куски, но только в рамках определенного модуля. Общий уровень качества кода будет постоянно поддерживаться на высокой отметке
- Вам проще взаимодействовать с клиентом и менеджментом, ведь объяснить, что за эту неделю у нас +3 новых фичи гораздо проще, чем у нас +100500 новых классов
- Вы экономите время, в том числе на согласования и интеграцию. Я неоднократно наблюдал пьесу в трех актах:
Акт 1 Команде UI не нравится API и надо все сделать иначе и вообще поля у нас названы совершенно иначе.
Акт 2 Команда бекенда считает, что команда UI — балбесы и вообще не понимают, как работает ядро
Акт 3 Менеджеры обеих команд уговаривают сделать хоть как-нибудь, чтобы работало, иначе уволят всех на фиг. Занавес.
Если коммуникация между командами налажена, то каждая из них получает ценную обратную связь. Наверняка существует способ немного
Действительно переиспользуемый код следует вынести в ядро. Каждый модуль (подсистема) может зависеть от функций ядра, но сами модули не должны ничего знать друг о друге. Если требуется обеспечить взаимодействие двух модулей, используйте событийно-ориентированное программирование. Подписывайтесь на события только через ядро. Если вам приходится дублировать какой-то функционал в двух-трех модулях – это сигнал для включения его в ядро или выделение в отдельную внешнюю зависимость. Такая организация кода позволит выкинуть любой модуль или переписать с нуля, возможно даже, используя другой язык программирования. Подробно об этом подходе на примере JavaScript рассказано тут.
Graceful Degradation для кода с низкой ценностью, хитрое YAGNI
Ценность (business value) разрабатываемого приложения не распределена равномерно по всей кодовой базе. Это утверждение справедливо и для количества усилий на разработку. До недавнего времени поддержка IE6 была примером колоссального перерасхода ресурсов на код с потенциально низкой ценностью.
Мы не можем ликвидировать затраты на написание кода с низким показателем ценности, но можем снизить их: не писать юнит-тестов, не проводить рефакторинга, забить на качество кода в определенных частях системы. За это нам придется заплатить поддержкой в будущем. Если конечно именно этот кусок систему действительно необходимо будет поддерживать и развивать в будущем.
А если нет? А если нет, то мы молодцы и смогли сэкономить. Один из секретов команды Вилларибо в том, что при прочих равных (а мы условились, что квалификация программистов и стеки у них аналогичные), они смогли направить основные свои усилия на разработку действительно ценного кода, а в Виллабаджо использовался унифицированный подход ко всей кодовой базе.
Пока меня не закидали помидорами в комментариях, поспешу раскрыть идею с отсутствием тестов и «костыльными» частями системы.
Ни один человек на свете не может знать достоверно, какая часть системы останется неизменной, а что придется переписывать с нуля или вообще выкидывать. Однако, существует вероятность изменения того или иного требования. Например, обработка финансовых транзакций с высокой вероятностью не будет меняться в течение длительного срока. Вдобавок стоимость ошибки в этой части ПО экстремально велика. В этой части приложения есть место DDD, TDD, BDD и любым другим крутым *DD, которые вам помогают в решении задач.
А вот если компания решила устроить рекламную акцию и вас попросили сделать landing page для вашего онлайн-банка – это совсем другая история. Скорее всего, проще отдать этот функционал на аутсорс и сделать страничку на PHP. Отгораживайтесь от такого кода с душком с помощью паттерна Facade. Оставляйте ядро системы чистым.
Направление слишком большого количества усилий в часто-изменяемую часть кодовой базы нерентабельно
Подробно этот подход описан в статье The Good, the Bad and the Ugly code.
Механизм обновления приложения
Если вы разрабатываете десктопное приложение, позаботьтесь о своих пользователях и предусмотрите механизм авто-обновлений из интернета. Для win существует, например ClickOnce.Смена сотрудников (Bus Factor)
Ключевые сотрудники могут покинуть компанию, уйти в отпуск, забеременеть или даже умереть, унося с собой важные знания как из предметной области, так и о коде приложения.
Методологическое ослабление риска
Совместное владение кодом, парное программирование, код-ревью.Дополнительные методы ослабления
Используйте общеизвестные паттерны проектирования, именуйте классы очевидным образом (Repository, Specification, DAL, DTO, ValueObject, Entity). Не лишним будет оставить ссылки на ресурсы в сети прямо в коде, если какая-то концепция не слишком известна, а материал удачно ее раскрывает.Используйте принцип Conventions Over Configurations, элементы функционального программирования (особенно для работы с коллекциями) и Side-effect free programming, если ваша платформа позволяет.
Минимизируйте количество «велосипедов». Если велосипеды появляются, добавьте комментарий, объясняющий наличие не очевидного кода и почему была выбрана именно такая реализация. Если велосипед вам все-таки очень нужен, выложите его в open-source или оформите отдельным продуктом. Пусть кто-то будет поддерживать новую технологию.
Для сложных предметных областей очень желательно использовать концепции DDD, особенно Ubiquitous Language. Это поможет новым сотрудникам быстрее «врубиться» в проект. Гораздо проще разбираться в коде, если он похож на естественный язык из спецификации, которую ты только что изучил.
Используйте аннотации/атрибуты, помогающие intellisense и анализаторам кода.
Используйте стандартные методы разворачивания приложений на сколько это возможно. Современные фреймворки идут в комплекте с механизмом миграции БД. Если конфигурация очень сложная есть смысл написать скрипт развертывания. Для особо-тяжелых случаев может быть целесообразно использовать штуки вроде vagrant.
Упрощайте dev-конфигурацию на сколько это возможно. Если вы используете Memcached для ускорения работы приложения, предусмотрите реализацию MemcachedDummy, которая будет работать без установленного сервера и всегда возвращать промах кеша. Чем быстрее новый разработчик может развернуть работающую, пусть возможно и не целиком, версию, тем лучше.
Старайтесь использовать общепринятые методы специфицирования и описания багов: user stories, use cases, UML, expected/actual behavior, закрепите основные бизнес-правила системы в unit-тестах. Используйте стайл-гайды для UI и серверного кода.
Создайте поддерживайте тесты хотя-бы на основные бизнес-правила Используйте bdd-нотацию в названиях тестов, чтобы сделать их назначение очевидными. Подробные рекомендации об организации unit-тестирования на проекте можно почитать в статье Unit-тестирование для чайников.
Декомпозиция спецификации
Спецификация неполная и/или содержит конфликтные требования
Методологическое ослабление риска
Использование методологии SpecByExample, возможность исключить требования с низким business value из плана спринта, формальная проверка на непротиворечивость до начала работ.Критика
Обычно риск ходит в паре с «изменением требований». Если наступил первый, то второй наступает почти автоматически. Для больших систем формальная задача проверки непротиворечивости требований по меньшей мере нетривиальна.Дополнительные методы ослабления
Здравый смысл. Если в ТЗ написан очевидный бред или очередной change request безумен, то лучше всего поднять этот вопрос на самой ранней стадии и внести исправления. Пока не принято решение нужно просто отложить эту задачу и взять из беклога следующую наиболее важную.Если вам не хватает каких-то материалов для начала работы и вы точно знаете, что к сроку они не появятся, не нужно играть в Counter Strike. Попробуйте начать работать без необходимых материалов. Нет графики? Используйте квадратики и кружочки. Нет визуального дизайна, но есть прототипы? Возьмите Bootstrap. Даже медленное движение лучше, чем полная остановка.
Наличие тестов на бизнес-логику отлично помогает ослабить и этот риск: вы узнаете о конфликте требований из упавшего теста, а не из плавающего бага на продакшне.
Технологические риски
Удовлетворит ли технологический стек задаче. Не придется ли менять язык программирования, СУБД из-за нагрузки или недостаточной интероперабельности? Подойдет ли выбранная «архитектура»/фреймворк, не станут ли они слишком дорогими в поддержке? На сколько плох чужой legacy-код?
Методы ослабления
Закладывайте возможность горизонтального масштабирования на раннем этапе, Обеспечьте Persistence Ignorance.Используйте Data Mapper или определите место, где он может появиться в процессе рефакторинга – это поможет гибко изменить источник данных в случае необходимости. Современные мапперы снабжены Assert’ами, которые помогут не забыть замапить все поля правильно, независимо от источника данных.
Следуйте принципу Low Coupling — High Cohesion (слабая связанность, сильное сцепление) при проектировании. Четко определяйте обязанности каждого класса, не допускайте появления God Object'ов.
Предпочитайте QueryObject паттерну Repository. Проблемы Repository в long-run отлично расписаны в этой статье. У QueryObject нет недостатков по сравнению с Repository, но есть преимущество – возможность быстро переехать с монолитной архитектуры на распределенную (CQRS и шину данных) для обеспечения полноценного горизонтального масштабирования.
Используйте защитное программирование, простите за тавтологию, для защиты инвариантов (инвариантом называется непротиворечивое внутреннее состояние объекта). Защитные конструкции следует размещать в конструкторе, тогда пост-условия практически не придется проверять, так как результатом выполнения функции будет тоже объект, защищенный в своем конструкторе.
Пользуйтесь паттернами Composite и Specification для организации кода и разбиения сложных участков на более мелкие и простые. Пользуйтесь паттерном Producer-Consumer для организации многопоточных приложений, если это возможно.
Предпочитайте Rich Domain Model, а не Anemic. Даже при использовании ORM в большинстве случаев вы сможете передать Entity в другой слой не прибегая к созданию DTO. Ответственность за создание DTO нужно возложить на Data Mapper.
Предусмотрите хотя бы минимальные системы самовосстановления и самодиагностики приложения: инсталляция, заполнение необходимых справочников, создание файлов настроек, если их удалили, перезапуск службы/демона в случае ошибки.
Низкая продуктивность
Интенсивность работы прямо-пропорциональна близости дедлайна. Пока сроки поставки далеки, есть соблазн валять дурака и всячески придаваться прокарстинации.
Методологическое ослабление риска
Короткие итерации, stand-up миттинги, практика демонстраций: после каждой итерации команда презентует проделанный объем работ представителю клиента или внутреннему Product Owner'у.Дополнительные методы ослабления
Инвестиции времен в начале проекта в инфраструктуру и мета-программирование. Если в вашем проекте много форм, задумайтесь о формо-генераторе, вместо клепания однотипных View-файлов. Если вам нужен CRUD-функционал для большого количества сущностей, один хорошо-спроектированный контроллер со всеми виртуальными методами поможет избавиться от написания тонн рутинного кода. Виртуальные методы позволят вам переопределить поведение, если это потребуется.Проблемы с производительностью мета-программирования (в особенности reflection) можно решить за счет динамической компиляции или кодо-генераторов.
Чем меньше кода, особенно однотипного рутинного, вы напишете, тем проще будет поддержка. Кроме этого, рутинный код – самый противный. Клиенту в принципе не важно, интересно вам было в процессе решения его проблемы или скучно. Вы как разработчик можете поставить себе задачу оптимизировать свою рутинную работу и решить задачу более элегантно. Новые задачи всегда интереснее, чем унылая копипаста.
Важно понимать, что увлекаться абстрактными фабриками абстрактных фабрик тоже не стоит. Если игра не стоит свеч, то лучше остановиться и воспользоваться проверенным способом. Вообще всегда, если вы закопались на 3-4 дня в задаче на 4 часа стоит остановиться и спросить себя «что я делаю не так?». Желательно спросить не только себя, но и еще парочку опытных коллег и еще парочку не-программистов («не технари» могут дать очень полезный фидбек, они мыслят иначе). Почти наверняка задачу можно решить проще.
Новые не проверенные технологии и подходы могут очень пригодиться проекту. Но они же и привносят дополнительные риски. Поэтому эксперименты следует проводить строго в начале проекта, а никак не за неделю до дедлайна. Начало проекта — отличный момент для настройки Continuous Integration, автоматизации рутинных операций и анализа рынка на предмет новых средств, упрощающих жизнь разработчику. Если вы давно слышите например, что R# — крутой, но вы до сих пор его не пробовали, потому что времени как-то не было — попробуйте. Кто знает, как она может улучшить вашу продуктивность.
В продолжении темы рекомендую этот скринкаст и Microservices Фаулера.
слайды доклада по мотивам этого поста