Slide 1

Slide 1 text

Зачем заниматься стандартизацией кодовой базы и Как нам помогут в этом линтеры Дмитрий Цепелев

Slide 2

Slide 2 text

@DmitryTsepelev DUMP’25 GitHub/Twitter: @dmitrytsepelev TG: @dmitry_tsepelev Блог: https:/ /dmitrytsepelev.dev 2

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Когда нужно начинать заниматься стандартизацией кодовой базы?

Slide 6

Slide 6 text

@DmitryTsepelev DUMP’25 Когда напрашиваются стандарты? 6 • SRP примитивов* не соблюдается/не определен; * примитив в данном случае — паттерн, компонент или что–то еще генерализуемое

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

@DmitryTsepelev DUMP’25 Пример: разделение ответственности в Rails 15 Model Controller Service Query Form

Slide 16

Slide 16 text

@DmitryTsepelev DUMP’25 Пример: разделение ответственности в Rails 16 Model Controller Service Query Form А теперь нужна система прав

Slide 17

Slide 17 text

@DmitryTsepelev DUMP’25 Пример: разделение ответственности в Rails 17 Model Controller Service Query Form Policy

Slide 18

Slide 18 text

@DmitryTsepelev DUMP’25 Пример: разделение ответственности в Rails 18 Model Controller Service Query Form Policy

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

@DmitryTsepelev DUMP’25 Пример: разделение ответственности в Rails 20 Model Controller Запросы к БД Валидации Прием HTTP– запросов Подготовка HTTP– ответов Service Query Form Policy Проверка прав Сложные запросы Условные валидации Логика

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

@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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Что делать?

Slide 30

Slide 30 text

@DmitryTsepelev DUMP’25 🗺 Разобраться, что уже есть 30 • собрать все устные и письменные соглашения; • собрать данные по взаимодействию абстракций: • если есть статический анализ — можно попробовать использовать его; • для динамических языков можно попробовать собрать в рантайме или тестах.

Slide 31

Slide 31 text

@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); }

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

@DmitryTsepelev DUMP’25 Как контролировать стиль кода 33 • code review; 👍 часть правил будет применяться 👎 люди тратят время на обсуждения стиля

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Как я добавляю правила в линтер

Slide 37

Slide 37 text

@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

Slide 38

Slide 38 text

@DmitryTsepelev DUMP’25 Что должно быть в «хорошем» правиле линтера? 38 • понятное сообщение об ошибке; • тесты; • ссылка на документацию, включающую: • объяснение, почему так делать плохо; • инструкцию, как делать хорошо. • если все сделано правильно — получится ADR as code.

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

@DmitryTsepelev DUMP’25 В каком порядке реализовывать правила? 42 • зависит от ситуации и приоритетов, например: • влияющие на производительность; • влияющие на безопасность; • улучшающие читабельность кода; • мешающие унификации.

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

@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

Slide 45

Slide 45 text

@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 …

Slide 46

Slide 46 text

@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%)

Slide 47

Slide 47 text

И зачем?

Slide 48

Slide 48 text

@DmitryTsepelev DUMP’25 Сайд–эффекты от навязанных стандартов 48 • онбординг становится сложнее, но распространение знаний — быстрее и проще; • неправильное решение наносит больше ущерба, но проще исправляется; • на code review не обсуждается стиль; • задачи делаются быстрее.

Slide 49

Slide 49 text

@DmitryTsepelev DUMP’25 Унификация тестов 49 • мы знаем ответственность компонентов; • мы можем зафиксировать правила тестирования и проверить их.

Slide 50

Slide 50 text

@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); • только принимает запросы и отправляет ответ.

Slide 51

Slide 51 text

@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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

@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

Slide 55

Slide 55 text

@DmitryTsepelev DUMP’25 Спасибо! Вопросы? 55 Оцените доклад • устные договоренности о правилах написания кода опасны; • стандартизация кода начивается с определения ответственности компонента; • один из способов внедрять проверяемые стандарты — линтер; • стандартизированная кодовая база требует усилий, но может окупиться.