Как стать автором
Обновить
14
0
Сергей Прохоров @sorgpro

Software Engineering Manager, Backend Tech Lead

Отправить сообщение

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

Итак, имеем псевдокод А:

if (1 == n) // вроде так советуют писать с ==, чтобы не присвоить ненароком :)
  r = 10;
else if (2 == n)
  r = 20;
else
  r = 30;

и псевдокод B:

bool t = n >= 1 && n <= 2;
int a[] = {30, 10, 20};
int r = a[n * t];
  • С точки зрения code review псевдокод A выглядит сносно, а вот для псевдокода B потребуется, как минимум, переименование переменной t, чтобы объяснить её дальнейшее участие в алгоритме.

  • Читабельность кода пострадала. В более сложных случаях, хотя даже и в этом, было бы разумно потребовать написать комментарий, объясняющий неочевидность происходящего при беглом просмотре кода, в отличии от псевдокода A. Более очевидным выбором было бы использование, например, словаря ключ-значение, вместо массива, но здесь не так, и поэтому следует указать, что эта конструкция - альтернатива if/switch.

  • Использование "хитрого" алгоритма привносит дополнительные трудности:

    • при возникновении еще одного условия, например, при n == 3 нужно возвращать 25, легко добавить правку в код, чтобы все отлично заработало, но также легко забыть добавить тест для этого значения. При этом инструмент проверки покрытия кода тестами для псевдокода B нам ничем не поможет, в то время как для псевдокода A он обязательно бы отметил этот момент, изменив процент покрытия, поскольку добавленная ветка else if (3 == n) не выполняется.

    • требования меняются, нужно добавить в существующую программу какой-то специфичный случай, и придется менять этот неочевидный алгоритм. Например, если для текущего псевдокода при n == 0 или n == 8 нужно возвращать какое-либо значение, то алгоритм псевдокода B a[n * t] перестанет работать и придется придумывать еще более изощренный и менее читабельный вариант.

  • Язык программирования не конкретизировался, поэтому я и назвал это пседкокодом, а значит у кого-то может возникнуть идея реализовать этот подход на других языках. Но в других языках придется вносить правки, поскольку компилироваться/работать этот код не будет. Это чревато последствиями, которые в случае псевдокода A просто бы не возникли:

    • для компиляции объявления массива, например в C#, интуитивно напрашивается ключевое слово new и вуаля - строка скомпилировалось! Только вот теперь объявление приведет к выделению объекта в куче, а это влечет за собой снижение производительности, а также добавление работы сборщику мусора, что дополнительно снизит производительность.

    • для того, чтобы как в оригинале на C++, код использовал только стек, нужно писать иначе, но не каждый начинающий разработчик догадается/умеет, а ведь именно такие разработчики могут последовать советам из этой статьи. Но! Даже если использовать инициализацию массива на стеке мы потеряем в производительности по сравнению с псевдокодом A.

    • чтобы производительность все же приросла, массив можно объявить статическим, как это сделал автор одного из комментариев выше, в котором он приводит замеры производительности. Но это значит, что мы выделили объект в куче на все время работы программы - мы повысили требование к памяти (размеру кучи), которое при массовом использовании этого приема может составить существенное значение, а в случае псевдокода A такое явление не возникает.

Так чего же добились, применяя псевдокод B?
Усложнили работу разработчикам, которые будут поддерживать и развивать программу?
Обманули инструмент, контролирующий покрытие кода тестами?
Себя? Тестировщиков? Работодателя? Качество продукта?

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

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

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

Хорошая практика в этом случае — тестирование. Накатываем миграции новой версии и тестируем старую версию так же, как и при её релизе, только вот уже на новой структуре БД (возможен сокращенный вариант тестирования, но тогда растут риски).

Сложный пример переименования выбран не случайно — он состоит из составляющих, которые демонстрируют в том числе подходы добавления (как уже было подмечено в комментарии выше), а так же удаления.

Весьма сложный кейс, но и такое бывает. Здесь технику blue-green deployment разумно будет сочетать с использованием feature toggles для возможности переключения строки соединения "на лету".

Практичные вопросы. Разберу их по порядку.


Сколько времени вы ждёте прежде чем переключаться между версиями?

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


Как вы понимаете, что все зависимые сервиса перешли на новую версию?

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


Что делаете с теми потребителями о которых вы не знаете или не имеете контактов в с ними, как они узнают, что скоро будет удалено устаревшее поле LastName?

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


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

Для отслеживания есть несколько механизмов:


  • пометки TODO: в коде;
  • атрибут [Obsolete];
  • создание задачи на удаление в бэклоге;
  • формирование диаграммы БД в CI — DocOps. Такой подход позволяет видеть структуру базы данных не только разработчикам, но и остальным членам команды, в том числе QA, аналитикам… Чем больше глаз, тем больше шансов, что ничто не будет забыто.
    Кстати о DocOps. Я планирую освятить эту тему в будущем.
Да. Именно в этом посыл. Обновление БД разрабатывается так, чтобы не было проблем не только с новой версией ПО, но и с предыдущей.
Не хотелось бы спойлерить, поэтому отвечу кратко: во второй части статьи я предложу подход, который позволит преодолеть такого рода кошмары в коде. Впрочем, указанный подход версионирование никоим образом не отменяет.
Очень правильно подмечено! Для случаев, когда для применения нового значения параметра готовы прибегать к перезапуску приложения, как в указанном примере, нужно использовать IOptions, во всех остальных случаях лучше применять IOptionsSnapshot. Если бы это было реальное приложение, то следовало бы заменить тип.

Информация

В рейтинге
Не участвует
Откуда
Казань, Татарстан, Россия
Работает в
Зарегистрирован
Активность