Pull to refresh
14
0
Сергей Прохоров @sorgpro

Software Engineering Manager, Backend Tech Lead

Send message

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

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

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. Если бы это было реальное приложение, то следовало бы заменить тип.

Information

Rating
Does not participate
Location
Казань, Татарстан, Россия
Works in
Registered
Activity