Почему разработчики такие медленные: распространенные проблемы и их решения

в 9:07, , рубрики: перевод с английского, Развитие стартапа, управление проектами и командой, управление разработкой

Привет! Представляю вашему вниманию перевод статьи Why Development Teams are Slow: Common Software Jams and Solutions автора Эрика Эллиота.

Почему разработчики такие медленные: распространенные проблемы и их решения - 1

Если вы больше любите слушать, чем читать, то в аудио формате перевод доступен на Яндекс.Музыке и в Apple Podcasts

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

  • Нереалистичные ожидания
  • Слишком много открытых тикетов
  • Неконтролируемый объем задач
  • Накопление код ревью
  • Плохая подготовка
  • Выгорание разработчиков
  • Баги
  • Текучка кадров

Медлительность разработчиков — не корень проблемы. Это симптом других перечисленных проблем. В 100% случаев, если команда разработки работает слишком медленно — это вина руководителя. Но хорошая новость заключается в том, что вы в силах это исправить. Давайте рассмотрим каждый из пунктов подробнее, чтобы разобраться, что мы можем с каждым из них сделать.

Нереалистичные ожидания

Большинство проблем с продуктивностью разработчиков вообще никак не затрагивают собственно разработку. Это скорее проблема нашего восприятия процесса разработки как менеджеров и стейкхолдеров.

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

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

Мы часто забываем, что софт, который мы создаем — это что-то принципиально новое. Если уже есть софт, который делает то же самое, купите его, используйте, импортируйте модуль и т.д. Не нужно его заново писать с нуля. Новый же софт уникален. Он делает что-то новое или делает что-то иначе. Именно затем мы его и создаем. А раз мы этого ещё не делали, то откуда нам знать сколько времени это займет?

Строители строят сборные стены в одном и том же темпе, а потому могут дать более-менее точные оценки, основываясь на наблюдениях. У разработчиков ПО нет надежных данных, на которые можно было бы опереться. Эту проблему усугубляет разная скорость работы разных разработчиков, и она может на порядок отличаться.

Как пишет Стив МакКоннелл, автор книги “Совершенный код”: «Заключение, что между продуктивностью работы разных программистов есть существенная разница было подтверждено множеством исследований профессиональных разработчиков (Curtis 1981, Mills 1983, DeMarco and Lister 1985, Curtis et al. 1986, Card 1987, Boehm and Papaccio 1988, Valett and McGarry 1989, Boehm et al. 2000). Нам не хватает данных чтобы предсказать как много времени потребуется для завершения нашего проекта. Мы узнаем каковы масштабы и сложность уже начав работу и этот процесс часто таит множество сюрпризов. Разработка это не только планирование, но и исследование, вне зависимости от того насколько тщательно мы пытались всё предусмотреть.»

«На первые 90 процентов кода уходит 10 процентов времени, потраченного на разработку. На оставшиеся 10 процентов кода уходит оставшиеся 90 процентов»
— Том Каргилл, Bell Labs

Существует несколько причин завышенных ожиданий, которые менеджер может контролировать. Одна из фундаментальных причин — измерение не тех вещей.
Возможно, вы знакомы с известным высказыванием Питера Друкера: «Что изменяется, то управляется».

И конечно это отличный совет. Мы конечно же должны изменять! Но мы упускаем суть этой цитаты, более того, переворачиваем её смысл с ног на голову.

Мысль целиком звучит так: «Что измеряется, то и управляется, даже если измерять это и пытаться контролировать совершенно бесполезно, даже если это вредит целям организации».

Два примера вещей, которые не стоит измерять:

  1. Прогнозирующие диаграммы выполнения задач (burndown charts), показывающие график количества открытых тикетов, предсказывающие дату окончания проекта, основываясь на недавних измерениях скорости работы;
  2. Количество тикетов, закрытых разработчиком, демонстрирующее как много задач завершил отдельный разработчик.

Измерение этих двух вещей стоило бесчисленному количеству компаний огромных убытков за счет потери продуктивности, сотрудников и прочих издержек.

Диаграммы выполнения задач

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

Если вы устанавливаете дедлайн основываясь на дате из диаграммы, то считайте вы уже не уложились в сроки. Единственное, что вас может спасти — это выкинуть из скоупа в будущем так много задач, как возможно.

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

Не все чарты — зло. Те, что не пытаются предсказывать будущее, могут быть и полезны. Они могут предупредить нас о расползании масштабов проекта и комбинаторных взрывах, когда вы видите, что количество тикетов увеличивается вместо того, чтобы уменьшаться или двигаться вверх-вниз. Полезные диаграммы выполнения задач демонстрируют уже реально закрытые задачи, а не пытаются предсказывать будущее.

Хороший показатель — это кривая у которой есть взлеты и падения, но в целом она движется вниз, что демонстрирует уменьшение количества открытых задач к концу проекта.

График проекта, где количество тикетов сокращается

Проект страдающий от разрастания скоупа будет напротив представлен кривой, которая изгибается вверх.

График проекта, тонущего в новых задачах

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

Цель — прозрачность процессов, а не красивая нисходящая кривая.

Остерегайтесь принципа Гудхарта: «Если измерение становится целью, то оно перестает быть полезным».

Не любое прогнозирование вредно. Когда у вас есть жесткий дедлайн (например вы пытаетесь выпустить игру до черной пятницы), вы можете планомерно контролировать скоуп, основываясь на средней скорости работы, чтобы знать когда стоит начать урезать скоуп. Если прогноз говорит вам, что вы закончите не раньше декабря, верьте ему. Настало время приоритизации и сокращения.

Эмпирическое правило для предсказаний такого рода:
«Если предсказание говорит, что вы можете что-то сделать к заданной дате, не верьте ему, если говорит, что не сможете — верьте.

Тикеты закрытые одним программистом

Идея пересчитать все задачи, выполненные одним программистом, а потом сравнить эту цифру со средним значением очень заманчива. Но я призываю вас противостоять этому соблазну. Существует масса лучших способов собрать данные о продуктивности разработчика.

В подсчете закрытых задач есть два фундаментальных упущения. Во-первых, задачи не тождественны друг другу по сложности и важности и на самом деле ценность работы зависит от степенного закона. На небольшую горстку задач приходится на порядок больше значимости чем “в среднем”. Это как разница между фундаментом небоскреба и последним забитым гвоздем. Таким образом просто подсчитав количество закрытых тикетов точно узнать ценность сотрудника невозможно.

Почему разработчики такие медленные: распространенные проблемы и их решения - 4

Много лет назад я работал над корзиной покупок для мирового лидера ритейла. Однажды я прекратил писать код и закрывать тикеты в Джире и добавил ещё один тикет: “Изучение юзабилити”.

Я работал над редизайном корзины больше года и дата релиза стремительно приближалась. До того момента никакого тестирование юзабилити нового процесса оформления заказа со стороны юзера не проводилось, так что это заняло неделю. Мы дали ранний доступ тысяче самых лояльных сторонников и опросили их, чтобы собрать обратную связь.

Я проанализировал результаты и обнаружил тревожную тенденцию в опросах и логах: частота ухода пользователей с сайта на этапе корзины была слишком высока и выражалась двузначной цифрой. Надвигалось настоящее бедствие! Так что я приступил к работе и запланировал запись новых юзабилити тестов с личными присутствием. Я усадил новичков за нашу новую корзину, дал им несколько задач и оставил наедине с сайтом. Я ничего не говорил, просто смотрел как они пользуются новым интерфейсом.

Я заметил, что в процессе оформления заказа у людей возникают сложности с ошибками в формах. Имея эти данные я слегка поправил наш опен-сорс проект на гитхабе (не отметив ничего в Джире). Через некоторое время мы провели ещё один тест. Пользователи стали гораздо реже уходит со страницы корзины: разница в доходе для компании в 1 миллион долларов в месяц.

Тем временем мои коллеги каждый закрыли по 10-15 тикетов. Вы могли бы возразить, что я мог бы попросту завести больше тикетов по тестированию юзабилити, чтобы они отражали реальное положение вещей. Но тогда мне бы пришлось завести тысячу дополнительных тикетов, что только создавало бы шум и потребовало бы много времени.

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

Если вы не будете внимательны, то проглядите вклад ваших самых продуктивных разработчиков. Лучший способ узнать что программисты делают для проекта — просить их. Спросите команду о том, кто по их мнению их самый полезный коллега.

Обычно информация, которая отражена в этой обратной связи сильно отличается от данных, которые можно получить просто посчитав количество тикетов.

Собирайте данные об эффективности работы, но не подходите к каждому разработчику с одной меркой. Разработка — это командный спорт и каждый участник процесса играет в нем свою роль. Нет никакого единого волшебного метода, который бы подошел для оценки всех без исключения.

Слишком много открытых задач

Казалось бы всё просто — открой тикет в трекере и двигайся дальше. Но каждая задача в трекере требует целого цикла переработки.

Их надо отсортировать, приоритизировать и назначить исполнителя прежде чем разработчики смогут приступить к их выполнению. Эта работа повторяется каждый раз, когда разработчики закрывают одну задачу и выбирают следующую. Если у вас есть проектный менеджер или скрам мастер, они выполняют эту работу каждый раз, когда они реприоритезируют список задач (что случается обычно в начале спринта или просто раз в пару недель).

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

Создание и чтение тикетов — это много работы, но это как бы ненастоящая работа. Это мета-задача. Нужна она чтобы приступить к выполнению настоящих задач. Сами по себе они имеют нулевую ценность. И на это тратится время каждый раз, когда программисты выбирают следующую задачу. Чем меньше тикетов одномоментно находятся в трекере тем лучше. Чем меньше низкоприотитетных задач висят в бэклоге, тем выше шанс, что разработчик выберет высокоприоритетную задачу.

Если существует баг, который упомянул только один пользователь, насколько он нам действительно важен? Он затронул одного человека, а есть ли у нас баги, которые заметили больше людей? Есть ли новые фичи, которые будут полезнее, чем фикс этого бага?
Вероятно, да.

Уберите шум из бэклога. Удалите то, чем вы в ближайшее время не планируете заниматься.
Если это действительно важно, добавите потом, когда для этого будет время.

Неконтролируемый размер задач

Люблю просить девелоперов в своей команде разбивать работу на задачи, которые они могут завершить за один день. Это сложнее, чем кажется, потому что это требует умения делить сложные задачи на мелкие, которые к тому же могут быть протестированы отдельно от всего остального приложения.

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

Вместо этого начните с независимо тестируемого модуля состояния корзины на стороне клиента и сделайте пулл реквест для этого. Потом постройте серверный API и сделайте отдельный PR и для него. Затем напишите UI компонент который бы импортировал состояние из модуля и связывался бы с серверным API. Каждая из этих задач может быть разбита на отдельные задачи, хотя всё это по сути является одной большой фичей. В качестве бонуса, задачи можно раскидать между несколькими программистами и ускорить разработку, извлекая пользу из численности команды.

Фича-свитчер сделает этот процесс проще и безопаснее, позволяя выключить разрабатываемую функциональность в до тех пор пока они не будут готовы для включения в продакшене.

Обратите внимание: не пытайтесь это делать без хорошего покрытия смоук тестами. У вас должна быть возможность убедиться что вы ничего не сломали, задеплоив полуготовые фичи. Обязательно проверяйте как это работает и во включенном и выключенном виде.

Накопление задач по код ревью

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

Это фаза интеграции в “Continuous Integration” (CI). Проблема в том, что чем дольше PR остается открытым, тем больше времени на него тратится. Разработчики будут его открывать чтобы посмотреть, могут ли они помочь его смержить. Они оставят обратную связь, запросят изменения и реквест вернется к автору для внесения правок и дальнейшего одобрения этих изменений. А пока всё это происходит пулл реквест будет отдаляться всё дальше мастера.
Когда у нескольких девелоперов появляется привычка делать очень большие коммиты, то количество пулл реквестов начинает расти как снежный ком и интеграция становится всё более сложной.

Пример: Боб делает изменения в файле, который Джейн тоже правила, но ещё не смерджила. Пулл реквест Боба мержат первым и PR Джейн становится на один коммит дальше от мастера. Теперь она не может смержить свою ветку, пока не исправит все конфликты с кодом Боба. Умножьте эту ситуацию на количество программистов, которые работают с одним и тем же кодом в вашем проекте. Подобного рода “пробки” порождают избыток действий.

Подсчитаем количество таких действий в стандартном сценарии:

  • Боб и Джейн начинают работу в одной и той же ветке (0 действий)
  • Боб вносит изменения и коммитит в свою ветку. Джейн делает то же самое (2 действия)
  • Код Боба попадает в мастер. Джейн скачивает себе изменения Боба и обнаруживает конфликт. Они его исправляет и коммитит результат в свою ветку. (3 действия)
  • Джейн открывает пулл реквест. Боб отмечает, что её изменения в его коде сломают что-то, что она не учла. Джейн вносит изменения с учетом комментариев Боба и снова коммитит код (4 действия)
  • PR Джейн наконец мержат. Всего 4 действия.

Теперь рассмотрим ситуацию, в которой коммиты были бы меньше, и пулл реквесты бы мержились быстрее:

  • Боб делает маленькое изменения и его код попадает в мастер (1 действие)
  • Джейн скачивает новую версию мастера и пишут свой код уже с учетом изменений Боба. (2 действия)
  • Поскольку коммит Джейн тоже маленький его быстро мержат в мастер. Итого всего два действия.

Когда мы создаем мелкие PRы, мы существенно уменьшаем необходимость переделывать код, которая вызвана конфликтами и запутанностью кода.

Плохая подготовка

IT индустрия ужасна в том, что касается подготовки и поддержки специалистов. Университеты углубленно учат алгоритмам, которые уже встроены в стандартные библиотеки и лишь немногие программисты пишут их с нуля.

В то время как основы разработки такие как принципы абстракции, связанность и зацепление, модульность против монолитного дизайна, работа с модулями, композиция функций, композиция объектов, дизайн фреймворков и архитектура приложений упускают из виду. Из-за взрывного роста индустрии примерно половина разработчиков имеют меньше пяти лет опыта и 88% специалистов считает, что им не помешало бы обучение.

Команды медленно работают, потому что они плохо разбираются в том, что они делают и никто не желает их учить.

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

Что можно сделать:

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

Профессиональное выгорание

Заставить свою команду выгореть — это куда большая неудача для руководителя, чем провалить сроки.

Выгорание это серьезная проблема, которая может привести к потере разработчиков, текучке кадров, огромным расходам, росту бас фактора (Bus factor — мера сосредоточения информации среди отдельных членов проекта; фактор означает количество участников проекта, после потери которых проект не сможет быть завершён оставшимися участниками).

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

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

К сожалению, “умри, но сделай” — принцип организации работы этих команд, хотя руководители редко осознают опасность такого подхода.

Вместо того, чтобы заставлять разработчиков больше работать, менеджерам стоит признать, что 100% ответственности за соблюдение сроков несет руководство, а не разработчики.

Уложиться в дедлайн проще, если использовать следующие приемы:

  • Лучше приоритизировать задачи и сокращать скоуп
  • Оптимизировать процессы
  • Распознавать и рефакторить запутанность в коде

Текучка кадров

Данные, собранные Linkedin в 2018 году, показали, что текучка кадров в IT превосходит таковую в любом другом бизнесе. И это плохо, потому что это вызывает риск бас фактора, риск, что вы потеряете ключевых специалистов вашего проекта.

Многие компании не придают значение удержанию специалистов. Давайте ближе посмотрим на то во сколько обходится текучка кадров.

Размещение вакансий обходится в 15—30 тысяч долларов. Время инженера стоит а среднем 90 долларов в час. Умножьте это на примерно 50 собеседований и много-много часов на то, чтобы отвечать на вопросы новичка и помогать ему прижиться в команде. Таким образом мы уже потратили 50 тысяч долларов, но это ещё не всё.

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

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

Это всё очень времязатратно и большие команды постоянно страдают от непрекращающейся текучки, поскольку согласно опросу 2019 года, проведенному Stack Overflow, 60% разработчиков сменили работу за последние два года.

К тому моменту, когда разработчик наконец начнет работать с максимальной эффективностью, вы его потеряете.

Как избежать текучки? Вот несколько советов из разных источников:

  • Платите честно
  • Регулярно повышайте зарплату
  • Позволяйте людям уходить в длительные отпуска
  • Предлагайте удаленную работу
  • Сохраняйте реалистичные ожидания
  • Предоставляйте задачи, которые будут интересны разработчику
  • Не позволяйте технологическому стеку слишком сильно устаревать
  • Предоставляйте возможности обучения и профессионального роста
  • Обеспечивайте бенефиты для заботы о здоровье
  • Не заставляйте разработчиков работать более 40 часов в неделю
  • Обеспечивайте сотрудников современным оборудованием

Баги

Если вы думаете, что у вас нет времени на внедрение высококачественного процесса разработки, значит на самом деле вам без него никак не обойтись.

Согласно книге “Evaluating Software Engineering Technologies” (David N. Card, Frank E. Mc Garry, Gerald T. Page, 1978) хорошо оптимизированные процессы позволяют уменьшить количество ошибок, без увеличения затрат. Главная причина в том, что согласно другой книге “Software Assessments, Benchmarks, and Best Practices” (Каспер Джонс 2000) обнаружение и исправление дефектов это одна из самых время затратных и дорогих задач в разработке.

Баги печально известны тем, что вызывают необходимость переработки кода и чем позже вы их обнаружите, тем дороже будет их исправить. Когда разработчику поручают исправить баг, обнаруженный уже в продакшене, это часто отрывает его от того, чем он занимался. В книге “A Diary Study of Task Switching and Interruptions” (Mary Czerwinski, Eric J. Horvitz, Susan Wilhite) говорится о том, что задача, от которой нас отвлекли может занять вдвое больше времени и содержать вдвое больше ошибок, что говорит о том, что высокоприоритетные баги в каком-то смысле заразны: исправляя один, мы скорее всего наплодим новых.

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

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

Но это ещё не всё. У этого бага будет свой коммит, свой пулл-реквест, код ревью, интеграция и возможно даже свой деплой. И на любом этапе может упасть какой-нибудь тест и весь цикл CI/CD придется начинать заново.

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

Следующие советы помогут улучшить качество процессов.

  • Замедлитесь, чтобы ускориться. Медленный значит бесперебойный, бесперебойный значит быстрый.
  • Проводите дизайн ревью. Совокупность проверки кода и требований позволяет отловить до 70% багов.
  • Проводите код ревью. Хорошо проверенный код на 90% проще поддерживать. Один час рецензирования сохранить вам 33 часа поддержки. Программисты, которые проводят код ревью на 20% более продуктивны.
  • Используйте TDD подход. Он сокращает количество багов на 30-40 процентов
  • Используйте CI/CD. Это автоматизированный процесс объединяющий код ревью, автоматическое тестирование, автоматический деплой на тестовое окружение и наконец автоматический деплой в продакшн. Автоматизация всего процесса позволяет избежать ошибок и сэкономить многие рабочие часы, которые программисты тратили бы на ручной запуск всех процессов. Если вы ещё не используете CI/CD начните сегодня.
  • Повышайте покрытие тестами. Ваш процесс CI/CD должен запускать тесты и останавливаться в случае если хоть один из них упадет. Это поможет избежать деплоя багов в продакшн и сохранить вам много денег и времени. Ваша цель хотя бы на 70% покрытия, но постарайтесь держаться ближе к 80%. По мере приближения к 100% вы заметите, что покрытие функциональными тестами важных пользовательских процессов даст вам больше, чем дальнейшее увеличение объема юнит-тестов.

Заключение

Существует множество способов повлиять на эффективность работы команды, среди которых:

  • Установить реалистичные ожидания
  • Следить и контролировать количество открытых задач
  • Контролировать размеры задач
  • Не позволять накапливаться задачам по код ревью
  • Обучать разработчиков
  • Обеспечивать хороший баланс между работой и отдыхом
  • Внедрять эффективные процессы разработки
  • Уделять внимание удержанию сотрудников

Автор: Елена Когодеева

Источник


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


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