💾

3.3. Data Oriented Architecture

 
Данные важнее кода.
 
Так было, есть и будет.
 
Многие парадигмы (ООП, DDD, Clean Architecture и подобные) забывают об этом и поэтому рано или поздно программисты их использующие платятся месяцами рефакторинга.
 
Но как только вы примите эту аксиому и начнёте писать код в подобной манере ваши программы станут на порядок понятнее, быстрее и гибче.
 
Мы достаём данные из какого-то источника данных (оперативной памяти приложения, API, файловой системе, message queue, etc.), изменяем их и сохраняем этот же или другой источник данных.
 
Data Oriented Architecture – подход, который ставит работу с Данными на первое место
 
Если сформулировать это как набор правил кодинга, то получится следующее:
 
  1. Не создавайте абстракций поверх структур данных хранилища
  1. Операции над источником данных приоритетнее бизнес-логики

i. Не создавайте абстракций

 
Как только программист приступает к написанию программы, первое его желание – это написать какие-нибудь Доменные Модели, потом написать к ним бизнес-логику, а потом уже спроектировать хранилище, которое будет отвечать этим Моделям.
 
class UserModel { id: number // Это поле лежит в таблице `user` основной базы profile: ProfileModel // В БД Profile не будет частью User, а лишь связью через foreign key на Profile email: string // Это поле вообще хранится в отдельной системе } class ProfileModel { id: number // Это поле лежит в таблице `profile` основной базы age: number // Это поле лежит в таблице `profile` основной базы firstName: string // А это поле лежит в таблице `profile` легаси базы user: UserModel // User не является частью Profile, а связан с ним по полю `userId` }
 
Когда он будет получать или отправлять запросы, он будет в том числе использовать Модели в качестве параметров этих запросов, потому что для него – Модель центр вселенной его программы.
 
type ChangeUserProfileRequest = { data: Profile // не будем описывать нужные поля, а просто восопльзуемся уже описанной моделью Profile }
 
Но только чаще всего, он создаёт тем самым лишь больше количество проблем, чем преимуществ своей программе.
 
Идея DoA, в том, чтобы работать с нашими данными так, как они выглядят в оригинале, не создавая лишний абстракций поверх:
 
// # Таблицы из основной БД type UserTable = { id: number // В БД это будет Serial } type ProfileTable = { id: number legacyProfileId: string userId: number age: number } // # Таблицы из легаси БД type OldProfileTable = { id: string firstName: string } // # Данные из сторонней системы аутентификации (Auth0) type GetUserDataRequest = { id: string userId: number email: string }
 
И тоже самое, когда мы описываем запросы и ответы, мы должны описать их конкретно так, как они выглядят:
 
type ChangeUserProfileRequest = { data: { firstName: string age: number userId: number } }
 
И все наше приложение в тот момент времени когда ему нужно, должно работать именно с этими структурами.
 

А если нам неудобно работать с исходной структурой?

 
Бывают ситуации, когда нам слишком часто приходится использовать связку сразу нескольких данных, тогда к вам на помощь спешит Проекция (Projection).
 
Проекция (Projection) – паттерн, который позволяет расширять структуры данных, не нарушая их исходную структуру.
 
Правила построения Проекция следующие:
 
  1. Вы можете расширять типы, пока это расширение обратносовместимо.
  1. Вы можете добавлять расчитываемые поля (computed fields)
  1. Если вы используете другую сущность, то используйте ее полностью
 
Пример:
 
type PositiveNumber = BrandedType<number, "PositiveNumber"> // О брендированных типах поговорим позже const PositiveNumber = { ofNumber: (val: number) => { if (val < 0) { throw new Error() } return val } } type Profile = ProfileTable & { age: PositiveNumber // # Пример расширения уже существующего типа } type UserWithProfile = { user: UserTable // Пример полного использовальния уже существующей структуры в БД profiles: ProfileTable[] // Пример полного использовальния уже существующей структуры в БД profileCount: number // Пример расчитываемого поля }
 
То есть, все операции, которые были нам доступны при работе, например, с UserTable останутся доступны и для этой Проекции, потому что она является частью Проекции.

ii. Операции над источником данных приоритетнее бизнес-логики

 
Если упростить, то это означает, что вы должны иметь возможность воспользоваться функционалом своего источника данных в любом месте программы.
 
Если вы когда-то слышали про Clean / Onion / Hexagonal Architecture или DDD, то вы знаете о понятии “слоев”, где, например, с БД разрешено работать только в отдельном слое, а в бизнес-логику работа с БД должна быть абстрагирована интерфейсов.
 
Одними из главных недостатков подобного подхода становятся:
 
  1. Невозможность использования специфичных функций вашего источника данных (если уж мы сделали абстракцию, то использовать какую-нибудь особенную возможность PostgreSQL – это уже привязываться к ее коду)
  1. Невозможность низкоуровневых оптимизаций работы с источником данных. Любая оптимизация потребует использования более конкретных операций над источником данных, но нам “нельзя” его прокидывать туда, куда нужно.
  1. Трата кучи времени на создание интерфейсов и абстракций, которые по итогу очень часто или не будут достаточно абстрагировать источник данных (например, чтобы его заменить), дадут вышеописанные проблемы, так еще и будут вызывать по одному методу друг друга (как часто я видел, что Controller, вызывает метод Adapter, который в свою очередь просто дергает UseCase, который ничего не делает кроме как вызвать Repository… и так 90% кодовой базы…)
 
Так вот, идея DOA ровно противоположна: не создавайте дополнительных слоев, используйте работу с вашими источниками данных где угодно и как угодно.
 
И тем самым, вы не только лишитесь вышеописанных проблем, так еще и получите возможность низкоуровневых оптимизаций + использования ВСЕХ возможностей вашего источника данных + не будете тратить время на изобретение ненужных абстракций.

Что дальше?

А сейчас мы затронем еще одну суперважную тема, которая даст ответ о том, когда и как переиспользовать код: