Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Зачем заниматься стандартизацией кодовой базы

Avatar for Dmitry Tsepelev Dmitry Tsepelev
June 07, 2025
13

Зачем заниматься стандартизацией кодовой базы

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

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

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

Avatar for Dmitry Tsepelev

Dmitry Tsepelev

June 07, 2025
Tweet

More Decks by Dmitry Tsepelev

Transcript

  1. @DmitryTsepelev DUMP’25 План • что такое стандартизация кода и когда

    стоит начинать; • стандартизация и линтеры; • влияние стандартизации на разработку. 3
  2. @DmitryTsepelev DUMP’25 Что такое стандартизация кода • набор соблюдаемых и

    проверяемых правил; • правила регулируют код в конкретном проекте; • например: • «публичные методы класса Repository должны возвращать relation (а не массив)»; • «не надо высылать письма из моделей»; • «не надо ходить в ENV напрямую». 4
  3. @DmitryTsepelev DUMP’25 Когда напрашиваются стандарты? 6 • SRP примитивов* не

    соблюдается/не определен; * примитив в данном случае — паттерн, компонент или что–то еще генерализуемое
  4. @DmitryTsepelev DUMP’25 Пример: разделение ответственности в Rails 7 Model Controller

    Запросы к БД Валидации Прием HTTP– запросов Подготовка HTTP– ответов А логику куда?
  5. @DmitryTsepelev DUMP’25 Пример: разделение ответственности в Rails 8 Model Controller

    Запросы к БД Валидации Прием HTTP– запросов Подготовка HTTP– ответов Бизнес–логика
  6. @DmitryTsepelev DUMP’25 Пример: разделение ответственности в Rails 9 Model Controller

    Запросы к БД Валидации Прием HTTP– запросов Подготовка HTTP– ответов Бизнес–логика
  7. @DmitryTsepelev DUMP’25 Пример: разделение ответственности в Rails 10 Model Controller

    Запросы к БД Валидации Прием HTTP– запросов Подготовка HTTP– ответов
  8. @DmitryTsepelev DUMP’25 Пример: разделение ответственности в Rails 11 Model Controller

    Запросы к БД Валидации Прием HTTP– запросов Подготовка HTTP– ответов Service Бизнес–логика
  9. @DmitryTsepelev DUMP’25 Пример: разделение ответственности в Rails 12 Model Controller

    Запросы к БД Валидации Прием HTTP– запросов Подготовка HTTP– ответов Service Бизнес–логика А у нас теперь сложные запросы
  10. @DmitryTsepelev DUMP’25 Пример: разделение ответственности в Rails 13 Model Controller

    Запросы к БД Валидации Прием HTTP– запросов Подготовка HTTP– ответов Service Бизнес–логика Query
  11. @DmitryTsepelev DUMP’25 Пример: разделение ответственности в Rails 14 Model Controller

    Запросы к БД Валидации Прием HTTP– запросов Подготовка HTTP– ответов Service Бизнес–логика Query А у нас валидации разные в разных контекстах!
  12. @DmitryTsepelev DUMP’25 Пример: разделение ответственности в Rails 19 Model Controller

    Service Query Form Policy • 🫠 все эти responsibility изначально были в наших двух примитивах; • 🤔 можем ли мы быть уверены, что все перенесли?
  13. @DmitryTsepelev DUMP’25 Пример: разделение ответственности в Rails 20 Model Controller

    Запросы к БД Валидации Прием HTTP– запросов Подготовка HTTP– ответов Service Query Form Policy Проверка прав Сложные запросы Условные валидации Логика
  14. @DmitryTsepelev DUMP’25 21 • SRP примитивов не соблюдается/не определен; •

    разные команды делают одно и то же по разному: • пример: проверка прав пользователя у одних в контроллерах, у других — в сервисах. Когда напрашиваются стандарты?
  15. @DmitryTsepelev DUMP’25 22 • SRP примитивов не соблюдается/не определен; •

    разные команды делают одно и то же по разному; • разные команды делают одно и то же очень одинаково: • например: есть несколько классов для поиска/создания корзины. Когда напрашиваются стандарты?
  16. @DmitryTsepelev DUMP’25 23 • SRP примитивов не соблюдается/не определен; •

    разные команды делают одно и то же по разному; • разные команды делают одно и то же очень одинаково; • многие вещи сделаны одинаково плохо: • например: часть тестов на контроллеры не проверяют сценарий запроса от неавторизованного пользователя. Когда напрашиваются стандарты?
  17. @DmitryTsepelev DUMP’25 Почему сделано одинаково плохо? 24 • проще и

    быстрее скопировать и поменять код, чем написать с нуля; • если в кодовой базе много неудачных решений — их будут копировать чаще.
  18. @DmitryTsepelev DUMP’25 25 • SRP примитивов не соблюдается/не определен; •

    разные команды делают одно и то же по разному; • разные команды делают одно и то же очень одинаково; • многие вещи сделаны одинаково плохо; • есть некие устные/письменные договоренности. Когда напрашиваются стандарты?
  19. @DmitryTsepelev DUMP’25 Устные договоренности опасны! 26 • очень сложно понять,

    насколько они соблюдаются; • часть команды тратит время на их соблюдение; • часть команды тратит время на их проверку; • если они нарушаются — можно принять неверное решение. 🗿 Лучше вообще не тратить время 🗿
  20. @DmitryTsepelev DUMP’25 Различие интерфейсов 27 class SomeService < BaseService def

    call if worked_f i ne? Success(some_object) else Failure(reason: some_reason) end end end class AnotherService < BaseService def call if worked_f i ne_too? some_object else { error: some_reason } end end end
  21. @DmitryTsepelev DUMP’25 Различие интерфейсов 28 • проблема языков с динамической

    типизацией; • LSP нарушен; • мы не можем написать универсальные средства для работы со схожими объектами, так как у нас нет гарантий; • нужно как–то проверять единообразие. class BaseService # () - > Result<T> def call Success() end end
  22. @DmitryTsepelev DUMP’25 🗺 Разобраться, что уже есть 30 • собрать

    все устные и письменные соглашения; • собрать данные по взаимодействию абстракций: • если есть статический анализ — можно попробовать использовать его; • для динамических языков можно попробовать собрать в рантайме или тестах.
  23. @DmitryTsepelev DUMP’25 Помните Archunit? 31 @Test public void Services_should_only_be_accessed_by_Controllers() {

    JavaClasses importedClasses = new ClassFileImporter().importPackages("com.mycompany.myapp"); ArchRule myRule = classes() .that().resideInAPackage(" . . service . . ") .should().onlyBeAccessed().byAnyPackage(" . . controller . . ", " . . service . . "); myRule.check(importedClasses); }
  24. @DmitryTsepelev DUMP’25 Как должно быть 32 • собрать список используемых

    паттернов/примитивов и описать их repsponsibility: • «все проверки прав пользователя — через Policy» • «контроллер только принимает HTTP–запрос и готовит ответ» • описать интерфейсы взаимодействия между ними: • «не ходим в БД из контроллеров, используем Repository»; • описать правила проектирования API: • «нельзя отдавать коллекцию без пейджинга».
  25. @DmitryTsepelev DUMP’25 Как контролировать стиль кода 33 • code review;

    👍 часть правил будет применяться 👎 люди тратят время на обсуждения стиля
  26. @DmitryTsepelev DUMP’25 34 • code review; • регулярный аудит: 👍

    Google: “readability review”; 👎 стандартизация будет фрагментарной; 👎 проблема копирования кода не решается. Как контролировать стиль кода
  27. @DmitryTsepelev DUMP’25 35 • code review; • регулярный аудит; •

    CI + Linter с собственными правилами: 👍 после введения правила новых нарушений не будет; 👍 линтер становится средством формирования бэклога; 👍 мгновенное code review по стилю на CI; 👎 надо отдельно заниматься разработкой и поддержкой правил. Как контролировать стиль кода
  28. @DmitryTsepelev DUMP’25 Rubocop — линтер в Ruby 37 # rule

    class ExtractInputType < Base MSG = "Consider moving arguments to a new input type" def on_class(node) schema_member = RuboCop : : GraphQL : : SchemaMember.new(node) if (body = schema_member.body) arguments = body.select { |node| argument?(node) } excess_arguments = arguments.count - cop_conf i g["MaxArguments"] return unless excess_arguments.positive? arguments.last(excess_arguments).each do |excess_argument| add_offense(excess_argument) end end end end # .rubocop.yml GraphQL/ExtractInputType: Include: "app/graphql/**/*" # .rubocop_todo.yml GraphQL/ExtractInputType: Exclude: - app/types/user_type.rb - app/types/order_type.rb
  29. @DmitryTsepelev DUMP’25 Что должно быть в «хорошем» правиле линтера? 38

    • понятное сообщение об ошибке; • тесты; • ссылка на документацию, включающую: • объяснение, почему так делать плохо; • инструкцию, как делать хорошо. • если все сделано правильно — получится ADR as code.
  30. @DmitryTsepelev DUMP’25 Типичный flow работы с линтером 39 • ставим

    линтер; • генерируем список нарушений; • делаем обязательной проверку на CI; • постепенно чиним; • ⚠ часто пропускается: конфиги линтера и список нарушений — в codeowners.
  31. @DmitryTsepelev DUMP’25 Предлагаемый flow работы с линтером 40 • ставим

    линтер; • генерируем список нарушений; • делаем обязательной проверку на CI; • ⚠ часто пропускается: конфиги линтера и список нарушений — в codeowners; • повторять бесконечно: • добавляем новое правило; • генерируем список нарушений; • чиним.
  32. @DmitryTsepelev DUMP’25 Чего ожидать 41 • всем немножко не понравится;

    • большая часть договоренностей выполняется не всегда; • от части договоренностей придется отказаться; • исключений больше нет (они исчезают либо становятся правилами); • возможно часть правил относится только к частям проекта или отдельным командам; • технический бэклог начнет стремительно пухнуть.
  33. @DmitryTsepelev DUMP’25 В каком порядке реализовывать правила? 42 • зависит

    от ситуации и приоритетов, например: • влияющие на производительность; • влияющие на безопасность; • улучшающие читабельность кода; • мешающие унификации.
  34. @DmitryTsepelev DUMP’25 Как я формирую бэклог для стандартизации 43 •

    код, который часто меняется, видят и копируют чаще; • чем больше хорошего кода — тем больше вероятности, что используют его; • некоторые проблемы более опасны, чем другие. Гипотезы
  35. @DmitryTsepelev DUMP’25 Как я формирую бэклог для стандартизации 44 •

    определить более и менее опасные проблемы; 📖 https:/ /dmitrytsepelev.dev/directing-refactoring # .rubocop_director.yml update_weight: 1 default_cop_weight: 1 weights: Graphql/AvoidFieldLoaders: 1 Graphql/NullableArrayField: 1 Isolation/ForbiddenDbCall: 2 Isolation/ForbiddenOperationCall: 2 Isolation/ForbiddenQueryCall: 1.5
  36. @DmitryTsepelev DUMP’25 Как я формирую бэклог для стандартизации 45 •

    определить более и менее опасные проблемы; • найти часто изменяемые файлы: 📖 https:/ /dmitrytsepelev.dev/directing-refactoring git log - - since=\"2024-01-01\" \ - - pretty=format: \ - - name - only | sort | uniq - c | sort - rg 54 conf i g/locales/en.yml 43 db/schema.rb 41 app/services/feature.rb …
  37. @DmitryTsepelev DUMP’25 Как я формирую бэклог для стандартизации 46 •

    определить более и менее опасные проблемы; • найти часто изменяемые файлы; • ранжировать и начать разбирать техдолг. 📖 https:/ /dmitrytsepelev.dev/directing-refactoring 💡 Checking git history since 1995-01-01 to fi nd hot fi les... 💡🎥 Running rubocop to get the list of o ff ences to fi x... 💡🎥🎬 Calculating a list of fi les to refactor... Path: app/controllers/user_controller.rb Updated 99 times since 2023-01-01 O ff enses: 🚓 Rails/SomeCop - 2 Refactoring value: 1.5431217598108933 (54.79575%)
  38. @DmitryTsepelev DUMP’25 Сайд–эффекты от навязанных стандартов 48 • онбординг становится

    сложнее, но распространение знаний — быстрее и проще; • неправильное решение наносит больше ущерба, но проще исправляется; • на code review не обсуждается стиль; • задачи делаются быстрее.
  39. @DmitryTsepelev DUMP’25 Унификация тестов 49 • мы знаем ответственность компонентов;

    • мы можем зафиксировать правила тестирования и проверить их.
  40. @DmitryTsepelev DUMP’25 Унификация тестов: пример 50 describe ItemsController before do

    allow(ItemPolicy).to receive(:update) .and_call_original end specify do put :update, update_params expect(ItemPolicy).to have_received(:update) expect(response.status).to eq(200) # . . . остальная логика end end • правила контроллера: • обязан вызывать Policy; • не содержит логику (логика в Operation); • только принимает запросы и отправляет ответ.
  41. @DmitryTsepelev DUMP’25 Унификация тестов: пример 51 describe ItemsController specify do

    expect { put :update, update_params } .to check_permissions(ItemPolicy, :update) .and perform_operation(ItemUpdateOperation) .and have_http_status(:ok) end end describe ItemsController before do allow(ItemPolicy).to receive(:update) .and_call_original end specify do put :update, update_params expect(ItemPolicy).to have_received(:update) expect(response.status).to eq(200) # . . . остальная логика end end
  42. @DmitryTsepelev DUMP’25 Унификация тестов 52 • мы знаем ответственность компонентов;

    • мы можем зафиксировать правила тестирования и проверить их; • результат — покрытие 100% из коробки.
  43. @DmitryTsepelev DUMP’25 Кодогенерация 53 • тесты могут быть настолько одинаковые,

    что их можно генерировать; • если правила достаточно зрелые, то можно попробовать генерировать и код.
  44. @DmitryTsepelev DUMP’25 Кодогенерация: пример 54 • правила контроллера: • обязан

    вызывать Policy; • не содержит логику (логика в Operation); • только принимает запросы и отправляет ответ. # rake "generate_controller[Orders, update]" class OrdersController < ApplicationController def update authorize! order, to: :update, with: OrderPolicy result = Operations : : Order : : Update.call(params) respond_with result end end class OrderPolicy < BasePolicy def update = raise NotImplementedError end class Operations : : Order : : Update < BaseOperation def call = raise NotImplementedError end
  45. @DmitryTsepelev DUMP’25 Спасибо! Вопросы? 55 Оцените доклад • устные договоренности

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