Если вам приходилось задумываться о построении эффективной экосистемы проекта и определении ролей тимлида и разработчика — статья Артема Прозорова из ZeBrains для вас.
Предлагаю вам задуматься над одним вопросом. Но не спешите с ответом, потому что он не так очевиден, как может показаться:
Какая из команд может реализовать более технически стабильный продукт?
Команда №1: Проектный менеджер, аналитик, тестировщик и несколько разработчиков, у каждого из которых за плечами минимум три года опыта. Все работают в одном офисе, посвящая свое время одному проекту в режиме fulltime.
Команда №2: Один сильный разработчик. Ему помогают множество не знакомых между собой людей из разных часовых поясов. У каждого — свой набор компетенций и уровень опыта. Работой над проектом участники занимаются в свободном режиме, по несколько часов в неделю.
* * *
Ответ на этот вопрос мы получим к концу статьи, а сейчас — немного скучной, но важной теории.
Разработка программного обеспечения — процесс творческий. Программисты реализуют один и тот же функционал по-разному. Но есть определенные правила, соблюдение которых придает коду стабильность, читаемость и простоту поддержки. Давайте взглянем на историю развития программирования и выявим эти правила.
Парадигмы программирования
В 1968 году Эдсгер Вибе Дейкстра показал, что безудержное использование переходов (инструкций goto) вредно для структуры программы. Он предложил заменить переходы более понятными конструкциями if/then/else и do/while/until. Это дало основу парадигме структурного программирования.
Можно сказать, что структурное программирование накладывает ограничение на прямую передачу управления.
Второй парадигмой, получившей широкое распространение, стала парадигма объектно-ориентированного программирования. Она поднимает структурирование кода на более высокий уровень, вводит понятия наследование, инкапсуляция и полиморфизм.
ООП устанавливает ограничение на косвенную передачу управления.
Третьей парадигмой является парадигма функционального программирования. В ее основе которой лежит неизменяемость или, иными словами, — невозможность изменения значений символов. Идеологически это означает, что в функциональном языке не должно быть инструкции присваивания. На практике большинство функциональных языков обладает средствами, позволяющими изменять значение переменной, но в очень ограниченных случаях.
Функциональное программирование накладывает ограничение на присваиваемость значений.
Получается, каждая парадигма привносит свои ограничения, при этом ни одна не добавляет новые сущности.
Принципы проектирования и шаблоны
Эстафету парадигм подхватывают принципы проектирования, которые добавляют свои ограничения:
SOLID — на построение абстракции.
DRY — на повторяемость кода.
KISS — на сложность логики.
Не отстают и шаблоны проектирования. Например, MVC ограничивает разделение логики. А различные линтеры и стандарты определяют правила оформления кода, что тоже является ограничением.
Неформальное определение качества кода
Каждая парадигма, каждый архитектурный принцип, каждый шаблон проектирования и каждый линтер говорят нам больше не о том, что делать, а о том, чего делать нельзя. Чем точнее разработчик следует установленным ограничениям, тем более качественным становится его код.
Код настолько качественен, насколько разработчик соблюдает установленные ограничения.
Пример из мира свободного ПО
Давайте вернемся к вопросу, который был обозначен в самом начале статьи. Первый вариант — типичная команда, собранная коммерческой организацией для коммерческого проекта. Второй — часто встречается в проектах с открытым исходным кодом.
Не бросая камни в сторону коммерческой разработки, все же надо отметить, что весьма часто ПО с открытым исходным кодом забирает себе львиную долю рынка, оставляя свои коммерческие аналоги далеко позади. Достаточно взглянуть на ОС Linux, ОС Android, веб-сервера Apache и Nginx, СУБД PostgreSQL, MySQL. Все они являются стандартами де-факто в своей отрасли.
Более того, часто ПО, разработанное командой, напоминающей вторую из примера в начале статьи, написано намного более качественно, чем ПО, разработанное InHouse.
Почему же проекты добиваются успеха, хотя их команда на первый взгляд не внушает доверия? Давайте разбираться.
Успех свободного ПО
Команда успешного проекта с открытым исходным кодом зачастую состоит из ключевого разработчика или инициативной группы и сообщества контрибьюторов. Роль ключевого разработчика заключается в том, чтобы сформировать идею и заложить такую архитектуру, в которой огромное количество разработчиков с абсолютно разными компетенциями будет эффективно работать. Под архитектурой здесь имеется в виду набор интерфейсов (контрактов, протоколов), оперирующих описанными структурами данных. Как правило, архитектура сопровождается спецификацией — она описывает, как именно система должна функционировать. Реализация же этих интерфейсов лежит на плечах всего сообщества. При этом каждый контрибьютор может быть погружен в проект ровно настолько, насколько это позволяют его компетенции, желание или возможности.
Главное качество ключевого разработчика — это способность разбить функционал приложения на мало связанные друг с другом компоненты, реализация каждого из которых не требует глубокого знания о системе вне пределов этого компонента.
Разбивая логику приложения на мало связанные компоненты, покрытые интерфейсами, ключевой разработчик одновременно накладывает архитектурные ограничения на разработчиков и обеспечивает их эффективную совместную работу. В таких условиях контрибьютор, работающий над реализацией интерфейса, не должен обладать знаниями о системе ВНЕ этого интерфейса. Это обеспечивает максимально быстрое погружение новых разработчиков в проект. С другой стороны, риски сломать что-то внутри проекта сведены к минимуму, потому что свобода действий ограничена интерфейсом.
Эта тема перекликается с принципом предметно-ориентированного проектирования (Domain Driven Design, DDD). Среди разработчиков бытует мнение, что основное предназначение DDD — обеспечение легкого переключения между фреймворками. Это не так. Главная задача DDD — это отделение логики приложения от логики фреймворка. Это дает возможность работать с высокоуровневой логикой приложения, не залезая в дебри фреймворка, и наоборот. Но это тема для отдельной статьи.
Ограничения, наложенные на свободное ПО
Условия, в которых происходит работа над проектами с открытым исходным кодом, также задают определенные правила для разработчиков. Вряд ли вы можете встретить успешный проект с открытым исходным кодом, который не будет на каждый Pull Request требовать как минимум двух апрувов, выполнять линтеры, прогонять тесты и статические анализаторы кода.
Для проекта с открытым исходным кодом строгое соблюдение ограничений — это залог выживания проекта в целом.
На коммерческих проектах зачастую контроль за соблюдением ограничений проходит весьма слабо. Даже тесты пишутся далеко не на каждом — по моей субъективной оценке, разработчики стараются избегать написания тестов, ошибочно аргументируя это отсутствием времени.
Второе отличие разработки проектов с открытым исходным кодом от коммерческих заключается в ограничениях на коммуникацию. Так как команда проекта может постоянно меняться, структурирование и сохранение информации для таких проектов — это не только вынужденная мера, но и единственный способ выжить и развиваться. Поэтому вся коммуникация тесно связана с кодом и фиксируется в обсуждениях внутри Pull Request-ов, в todo-шках и комментариях прямо в коде, в issues, страницах с документацией и так далее.
В коммерческой InHouse команде ничего не мешает подойти к коллеге в течение дня и обсудить важные архитектурные вопросы в личной беседе, игнорируя письменную фиксацию принятых решений. В итоге последующие разработчики могут потратить огромное количество времени на выяснение подробностей таких решений.
Секрет качественного кода — в управлении ограничениями
Чтобы максимально точно донести тезис, используем предельно жесткую и даже провокационную формулировку:
Для эффективной работы команды на проекте старайтесь накладывать на свою команду как можно больше ограничений.
Ограничьте роль тимлида
Роль тимлида предполагает, что такой специалист обладает видением всей системы в целом, всех ее компонентов, понимает, как именно компоненты взаимодействуют между собой.
Ограничения тимлида заключаются в том, что он не должен отвечать за реализацию компонентов. Конечно, тимлид может брать на себя разработку определенных блоков функционала, но его основная задача — отвечать за общую картину, решать архитектурные вопросы.
Ограничьте роль разработчика
В отличие от тимлида, разработчик на проекте должен быть погружен только в те компоненты, разработкой которых он занимается, и детально понимать их реализацию. То есть ограничения разработчика заключаются в том, что он действует строго внутри установленных интерфейсов и не имеет права их менять без согласования с тимлидом.
Используйте линтеры и статические анализаторы кода
И чем больше, тем лучше.
Используйте код ревью и кросс ревью
Код ревью — мощный инструмент повышения качества кода. Но не забывайте и про кросс ревью — взаимное ревью разработчиков, работающих над разными частями системы или даже на разных проектах.
Пишите тесты
Причем — модульные (юнит), потому что написание именно таких тестов накладывает на разработчиков архитектурные ограничения: вы не сможете написать модульный тест без использования таких паттернов, как Dependency Injection, DI Container, и так далее.
Вместо резюме
Следите за миром свободного ПО, изучайте архитектуру проектов с открытым исходным кодом. Перенимайте их практики. Установите ограничения на рабочую коммуникацию внутри команды. Следуйте правилам работы с системой контроля версий, правилам ветвления и создания Pull Request-ов.
Ограничения способствуют повышению качества кода, что, в свою очередь, приводит к созданию более жизнеспособного продукта.
Постскриптум: Формализация коммуникаций и «портирование» InHouse правил разработки, присущих OpenSource проектам, ни в коем случае не отменяет необходимость живого общения, выращивания командной культуры и здоровой атмосферы в коллективе. В противном случае — любой, даже самый отлаженный процесс сведут на нет холивары и бодания в Pull Request-ах.
Комментарии (12)
aversey
26.09.2021 21:00+1Почему вы думаете, что парадигмы привносят только ограничения? Мне кажется довольно странным мир, где есть некоторое универсальное супер-программирование в котором всё можно, и где вся наша задача в том, что бы ограничить свои возможности, что бы не выстрелить себе же в ногу. На ассемблере можно написать всё, да, но то что на самом деле привнесли первые языки программирования -- новый способ мыслить о программах. Именно привнесли, а не ограничили. Например, в создании Тони Хоаром быстрой сортировки огромную роль сыграло его знакомство с Алголом, где крайне элегантно вводилась рекурсия -- он просто не думал так до этого (https://youtu.be/tAl6wzDTrJA -- около 14:40). Так же и другие парадигмы больше привносят новых способов мыслить о программе, чем ограничивают уже существующие. Подчеркну, что формулировка структурного программирования как "не используй goto" -- крайнее упрощение.
При этом я конечно понимаю основную мысль, что программировать сложно, возможностей невероятно много (вероятно даже избыточно много), и хорошо себя ограничивать и поддерживать дисциплину. Только вот например Дейкстра в своей Дисциплине Программирования имел в виду (насколько я понял) не следование некоторым ограничивающим правилам, которые приведут вас к безошибочному коду -- но напротив, он признавал принципиальную сложность программирования и предлагал дисциплину в смысле научной дисциплины, когда вы полностью осознаёте свои программы и можете доказать их корректность, а не просто показывать тестами и поддерживать различными методиками их около-работоспособность (вспомните его "Program testing can be used to show the presence of bugs, but never to show their absence!" или даже "software engineering has accepted as its charter "How to program if you cannot."").
Я не пытаюсь сказать что тестирование не важно, а ограничивать программиста не нужно, хочу просто подчеркнуть, что, по-моему, статья несколько утрирует проблему, а предлагаемые ей решения, если их воспринимать слишком серьёзно, могут привести к серьёзным же проблемам.
Andrey_Solomatin
26.09.2021 21:35На ассемблере можно написать всё, да, но то что на самом деле привнесли первые языки программирования -- новый способ мыслить о программах. Именно привнесли, а не ограничили.
Тут смешиваются два понятия: Возможность делать и способ мыслить. Да парадигмы это расширения способа мыслить, но при этом это ограничения возможности делать.
mr_writer
26.09.2021 21:06-1Разумные ограничения однозначно надо, собственно опыт в том и заключается, что опытный человек знает как делать не надо. Проблема в том, что многие разработчики считают иначе. Мне вот близок swift в последнее время, разработчики языка помешаны на сокращениях и скрытых возможностях, причем иногда это удобно, но чаще всего люди просто не понимают как это использовать корректно и только усложняют код. И с каждой новой версией языка все больше возможностей и все больше мест и способов как ухудшить код.
В тоже время с призывом писать тесты я не согласен, пробовал и так и так, далеко не всегда тесты нужны и полезны. Все же я отлично обхожусь без них.
Andrey_Solomatin
26.09.2021 21:57+2Все же я отлично обхожусь без них.
Я тоже прекрасно обходился, пока не научился писать простые юниттесты. После этого они стали нужны и полезны намного чаще.
Aquahawk
Если вы не можете, это не говорит о том что так нельзя, это говорит об ограниченности вашего взгляда и опыта.
hellamps
мне так кажется, что вся эта строгая типизация и ооп был как раз придуман для тех, кто пишет спагетти код на процедурном языке, или "все в мейн".
в этом плане UT это "альтернативный" консьюмер интерфейсов, заставляющий таких людей выделять таки абстракции и не смешивать логики разных абстракций, в стиле "чтобы отрисовать чекбокс мы берем глобальный theApp, ищем там формочку, в формочке грид, в гриде строчку и там рисуем чекбокс"
с другой стороны, если ваш язык программирования не может такой код обернуть моками... значит вы себя ограничиваете :) в этом плане я был поражен возможностям js и ноды
Andrey_Solomatin
Если код не писать с прицелом на тесты, то тесты будет сложно писать. В итоге они будут объёмными, сложными хрупкими и от них откажутся, как от неэфективных.
Aquahawk
С этой формулировкой почти согласен. Автор поста же категорично утвержает что без таких паттернов никак, с этим я не согласен. Я правда вообще делю код на многоразовый и одноразовый. Одноразовый это всякие гуи, хендлеры взаимодействия и прочая бизнес логика которая вызывается из одного места для одной цели. Как правило у такого кода много зависимостей (от данных, от вью, особенностей местного законодательства, от чёрта лысого), нет смысла в рефакторинге, нет внятной спецификации. Такой код я не вижу смысла покрывать тестами. А вот код ядра системы, библиотеки, утилиты я отношу к многоразовому, как правило у такого кода немного зависимостей, простой и понятный апи и их тестировать удобно можно и нужно, и часто без DI.
Andrey_Solomatin
У меня другая философия, для меня ядро системы это то, почему мы вообще затеяли проект: та самая бизнес логика. Всё остальное это адаптеры: обработчики сообщений, клиенты к другим сервисам, подключения к базам данных, библиотеки, утилиты. Все они имею смысл только в конексте бизнес логики.
В проекте должа быть изюминка. То, чем он отличается от другого или другими словами то почему покупка другого не решит проблему.
Именно эти бизнес требования и стоит покрывать тестами в первую очередь. Программисты не хотят разбираться с бизнесом и размазывают эту логику по хэндлерам, базам данных, и прочим библиотекам. Что делает код запутанным и плохо тестируемым.
Andrey_Solomatin
Для меня самый простой вид Dependency Injection это передача аргументов в функцию/конструктор. В таком контексте тесты без DI это редкость, и я бы их скорее компонентными или модульными назвал.
Norgorn
Мимопроходил, но хочется возразить (как и вашему собеседнику ниже). Покрывать код тестами можно с разными целями, но главной (хоть это и вопрос дискуссионный) я бы выделил "улучшение качества", то есть, чтобы приложение "нормально" работало (ожидаемо, без косяков). В таком случае, чем больше протестировано, тем лучше (причём, не формально покрытие, а в более широком смысле). Будет ли этот "одноразовый" код протестирован после разработчика - вопрос организации процесса, но обычно лучше таки протестировать самому - и тут следует учесть, что реально write only код в проекте бывает не так то и часто, кто его знает, когда и что придётся менять.
В общем, у вас здравая точка зрения, которая помогает не тратить силы впустую, но хочется каждый раз добавлять, что, все таки, лучше тесты писать, чем не писать. И кода, для которого тесты не нужны в долгоживущем проекте меньше, чем хотелось бы
artem_prozorov
Автор статьи на связи :)
В общем-то, смысл в отсутствии жестких связей в коде и речь про паттерны, которые разбивают жесткие связи. Модульные тесты накладывают ограничение на использование жестких связей, а код, который мало связан легче поддается изменениям и более стабилен. Смысл в этом.