Найти тему
Mad Devs

Эффективная переделка интерфейса базы данных

Оглавление

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

Дисклеймер: Если бы мы использовали в этом проекте различные ORM, такие как Gorm, скорее всего, мы бы не столкнулись с этой проблемой, но мы решили написать нашу реализацию именно так, и это создало проблему, а следовательно и этот пост.

Вот так выглядит огромный интерфейс БД с которым не возможно работать:

Проблема этого интерфейса заключалась в его размере — более 130 методов в одном интерфейсе! Это много методов, и это не то, как должен выглядеть SOLIDный интерфейс. Более того, так как мы пишем на Go, мы должны знать (и применять) одну из поговорок Go, а именно:

Чем больше интерфейс, тем слабее абстракция. © Роб Пайк

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

Шаг 0: До рефакторинга следует покрыть код основного API тестами

Это очень важно, поскольку работа с интерфейсами, абстракциями и рефакторингом без тестов, которые охватывают логику основного API, не приносит пользы. Я бы сказал, что это может сделать с точностью до наоборот — принести много проблем вашему проекту, так как каждое изменение, которое вы вносите в интерфейс, приводит к изменению более 20 файлов. И если у вас нет надежных тестов, есть большая вероятность, что вы что-то сломаете или создадите ошибки. Пожалуйста, будьте осторожны!

Шаг 1: Представьте какой должен быть итоговый результат

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

Это позволило бы написать следующий код вместо ссылки на один из 130+ методов из интерфейса:

user, err := api.Storage.GetUserByID(ctx, userID)
err := api.Storage.DenyAgreement(ctx, agreement)
err := api.Storage.UpdateUserDeviceToken(ctx, model.UserDeviceToken{...})

После изменений методы выглядили бы следующим образом:

user, err := api.Storage.User().Get(ctx, userID)
err := api.Storage.Agreement().Deny(ctx, agreement)
err := api.Storage.User().UpdateDeviceToken(ctx, model.UserDeviceToken{...}

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

Шаг 2: Изменяйте код пошагово (не всё сразу)

Наличие интерфейса с более чем 130 методами значительно усложняет процесс рефакторинга в стиле «раз и навсегда». Получается так много изменений, что каждый Merge Request превращается в изменения более 50 файлов. Поэтому следующим шагом должно быть постепенное разбиение интерфейса, один тип за другим и частая фиксация (коммиты) этих изменений для создания небольшого, понятного MRа и обеспечение того, чтобы все по-прежнему работало должным образом (помните шаг 0!). Для этого я сначала разбил интерфейс на несколько более мелких интерфейсов:

Итак, я создал несколько под-интерфейсов и заявил, что мой интерфейс IStorage реализует их все. Это не сильно изменило код, но заложило важный подготовительный блок к тому, что я хотел сделать дальше, который по сути заменяет мои подчиненные интерфейсы на отдельные интерфейсы с их отдельными методами в стиле CRUD, добавляя недостающие методы и объединяя те, которые имеют один и тот же смысл. Я добавил новые структуры в качестве реализации этих интерфейсов и заменил старые методы на новые через функцию “найти-заменить” VSCode и постепенно заменил все функции.

В результате я изменил все под-интерфейсы IStorage на его интерфейсы и сделал все функции более интуитивными и понятными. Да, это заняло у меня некоторое время, но результат того стоит.

Шаг 3: Чистка

Массовое редактирование с помощью команды find -> replace all помогает сэкономить время, но также создает некоторые побочные эффекты, в которых вы можете переименовать логи в коде, по которым в последствии будет очень сложно ориентироваться. Это то, что случилось со мной после рефакторинга.

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

Сторонние эффекты рефакторинга БД интерфейса

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

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

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

Заключение

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

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

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

Ранее статья была опубликована тут.