За свою карьеру я успел разработать и переработать с нуля несколько десятков крупных систем.
Бизнес области были сильно непохожи друг на друга: всероссийская электронная библиотека, федеральная система аккредитации химических лабораторий, интернет магазины, включая крупнейший в СНГ B2B магазин товар для взрослых, система по автоматизации выращивания растений, криптоплатежные шлюзы, несколько чатов, телеметрия и автоматизация городских парковок, и так далее.
Каждый раз, когда я сталкивался с новой системой вставал главной вопрос: «А с чего начать проектирование?»
И я нашел ответ, который работает в 100 из 100 случаев:
Начинать проектирование системы надо с Процессов
Как это работает
Если абстрактно:
- Абсолютно любой код решает задачу, поэтому для начала мы должны выяснить «к какому результату мы хотим прийти», то есть описываем Задачи
- Далее описываем Процессы – последовательность действий, которые мы должны произвести, чтобы решить Задачу.
- Далее мы проходимся по описанию всех Процессов и можем вычленить Структур Данных, разделив их таким образом, который будет хорошо отвечать Процессам.
- Следующим этапом, мы выделяем шаги, которые повторяются в Процессах и выносим их в Операции.
Рассмотрим на на примере разработки интернет магазина:
i. Задачи
- Клиент должен иметь возможность:
- Искать товары
- Добавлять их в корзину
- Оформлять заказ
- Смотреть историю заказов
- Изменять данные еще несобранного заказа
- Получать нотификации об изменении статуса заказа
- Регистрироваться и Аутентифицироваться
- Админ должен иметь возможность:
- Добавлять товары
- Видеть заказы клиентов
- Изменять данные заказа
- Изменять статус заказа
ii. Процессы
- Клиент
- Оформлять заказ
- Просматривать список товаров в моей корзине
- Входные данные (Input или просто I): идентификатор пользователя
- Выходные (Output или O): Корзина
- Видеть сумму, скидки, акции, условия доставки
- I: идентификатор Корзины, идентификатор Условия доставки
- O: оригинальная сумма, размер скидки, стоимость доставки, итоговая сумма
- Оплачивать заказ
- I: идентификатор Корзины
- O: ссылка на систему оплаты
- Видеть оформленный заказ
- I: id Заказа
- O: данные по Заказу
- Админ
- Добавлять товар
- Добавлять описание товара
- I: описание
- O: OK
- Стоимость
- I: число
- O: OK
- Особенности доставки
- I: набор идентификаторов условий доставки
- O: OK
- Выставлять категории и тэги
- I: набор идентификаторов категорий и тэгов
- O: OK
- Добавлять его в акции
- I: набор идентификаторов акций
- O: OK
- Описывать к какой политике скидок он относится
- I: набор идентификаторов политики скидок
- O: OK
iii. Структуры Данных
Теперь сформируем условные Структуры Данных, которые нужны для этих процессов:
- Пользователь – Пользователь или Админ
- id
- Продукт
- id
- name
- price
- Корзина
- id
- productIds
- userId
- Заказ
- id
- productIds
- userId
- status
- addressId
- deliveryTypeId
- Чек
- id
- status
- orderId
- Адрес пользователя
- id
- city
- street
- userId
- Условия доставки
- id
- name
- prices
- Тип оплаты
- id
- name
- Акция
- id
- name
- productIds
- priceCoeficient
- Политика скидок
- id
- name
- productIds
- userIds
- priceCoeficient
Хочу отметить, что эти структуры сейчас отвечают только тем двум процессам, который я расписал в предыдущем пункте. Чем больше было бы процессов, тем больше структур данных я бы расписал.
+ поскольку у нас не ООП, мне не надо делать их по какому-то принципу объединения поведения и данных, нет, я их описываю так, как мне удобно будет их хранить в БД. То есть, если Заказ было бы удобно хранить кусками в 3-х разных БД, то у меня было бы 3 разных структуры данных, отписывающих как это храниться в этих БД.
iv. Операции
Последний этап, это найти процедуры, которые повторяются между процессами и вычленить их в Операции:
- Изменение данных заказа – логика, которая доступна как Админу, так и Пользователю, единственное отличие в которой заключается в том, что админу доступно это изменение в любом статусе Заказа, а Пользователю только статусе «Оформлен».
Это очень утрированный пример, но самое главное, что я хочу показать – как круто и просто такое проектирование накладывается на код:
// Главное отличие от этапа проектирования, это то, что мы сначала // описываем Структуры Данных, потом Процессы, потом Операции // # Структуры Данных // ./data/main-db-data-structures.ts type User = { id: string; role: "client" | "admin" } type Product = { id: string; name: string; price: number; } type Cart = { id: string; productIds: string[]; userId: string; } type Order = { id: string; productIds: string[] userId: string; status: string; addressId: string; deliveryTypeId: string; } type Check = { id: string; status: string; orderId: string; } type UserAddress = { id: string; city: string; street: string; userId: string; } type DeliveryType = { id: string; name: string; prices: number[] } type PaymentType = { id: string; name: string; } type Stock = { id: string; name: string; productIds: string[] priceCoeficient: number; } type DiscountPolicy = { id: string; name: string; productIds: string[] userIds: string[] priceCoeficient: number; } // # Процессы, с описание Входных (Input) и Вызодных (Output) Структур Данных // ./process/get-my-cart.ts const getMyCart = (userId: string): Cart = { return db.cart.findOne({ userId: userID }) } // ./process/calculate-cart-sum.ts // а вот этот тип относится только к Процессу ниже, поэтому будет описан // рядом с ним type CalculateCartSumResult = { sumPrice: number deliveryPrice: number totalPrice: number } const calculateCartSum = (cartId: string, deliveryTypeId: string): CalculateCartSumResult => { const carts = db.cart.findOne({ userId: userID }) // Достаем Продукты const products = db.products.find({ id: carts.productIds }) // Достаем относящиеся к ним акции и скидки const stocks = db.stock.find({ productIds: products.map(p => p.id) }) const discountPolicies = db.discountPolicy.find({ productIds: products.map(p => p.id) }) // Достаем тип доставки const deliveryType = db.deliveryType.find({id: deliveryTypeId}) // Ну и здесь какие-то вычисления стоимостей return { sumPrice: ..., deliveryPrice: ..., totalPrice: ..., } } // ./process/confirm-order-ts const confirmOrder = ( cartId: string, deliveryTypeId: string, paymentTypeId: string ) => { // ... } // и так по всем Процессам // ... // # Теперь опишем Операции, которые могут переиспользоваться Процессами // ./opertaions/change-order-info.ts const changeOrderInfo = (order: Order, byWhom: user, newOrerData: Partial<Order>): void => { // Запрещаем изменять информацию о заказе если оне не в статусе "оформлен" и при это // изменять хочет Клиент if (order.status !== "formed" && user.role === "client") return; // Если же все ок, тогда делаем все нужные трансформации // ... }
Заметьте:
- Я не объединяю Процессы в какие «Сервисы» и подобное.
Максимум, во что я бы мог их объединить, это в какой-то модуль или микросервис по домену (например, в одном месте, все что связано с Корзиной, Продуктами и так далее, в другом, что связано с Акциями, в третьем с Оплатами), что влияло бы на то, в каких папках находились бы функции этих Процессов.
- Каждый Процесс и Операция могут использовать абсолютно любые Данные
Тут есть еще много правил проектирования и построения приложений, об этом мы будем говорить в других главах и даже книгах.
Главное, что я хочу донести: проектируйте начиная с Процессов, и пишите свой код начиная с Функций.
Именно таким образом вы лучше всего попадёте в требования системы + сделаете ее достаточно гибкой для дальнейшего развития.
Это то, что активно используется как в ФП, так и в ПП, так, собственно, и в ФОП.
Что дальше
Ок, начинать будем с процессов, а как правильно подходить к работе с Данными?
Именно этому посвящена следующая глава:
👈 Предыдущая глава
Следующая глава 👉