TL;DR
easy-model - это библиотека для организации сложной бизнес-логики по принципам DDD. Она предлагает Model-View-ViewModel паттерн с dependency injection, loader-декораторами для async операций и встроенной поддержкой тестирования. Идеально подходит для e-commerce с его сложными доменными моделями.
Введение: почему DDD во фронтенде
Современные фронтенд-приложения перестали быть “тонкими клиентами”. Особенно в e-commerce, где мы имеем:
- Сложные бизнес-правила (валидации, акции, инвентаризация)
- Состояния, которые должны быть согласованы между компонентами
- Асинхронные операции (платежи, проверка наличия)
Традиционные стейт-менеджеры (Redux, MobX) не предоставляют инструментов для организации доменной логики. easy-model заполняет эту нишу.
Основная часть: паттерны easy-model
1. Модели как First-Class Citizen
В easy-model модели - это не просто интерфейсы данных, а полноценные классы с поведением:
class ProductModel {
product = {
id: "",
name: "",
price: 0,
stock: 0
};
constructor(initialProduct: typeof this.product) {
this.product = initialProduct;
}
updateStock(newStock: number) {
if (newStock < 0) throw new Error("Stock cannot be negative");
this.product.stock = newStock;
}
applyDiscount(discount: DiscountModel) {
this.product.price = discount.applyTo(this.product.price);
}
}
2. Dependency Injection через provide
Для разделяемых состояний (корзина, пользователь) используем provide:
const CartProvider = provide(CartModel);
function Header() {
const cart = useInstance(CartProvider("user123"));
return <div>Cart items: {cart.items.length}</div>;
}
function ProductPage() {
const cart = useInstance(CartProvider("user123"));
// Тот же инстанс, что и в Header
}
3. Лоадеры для асинхронных операций
Декоратор @loader.load автоматически управляет состояниями loading/error:
class OrderModel {
@loader.load(true)
async submit() {
await api.post('/orders', this.order);
}
}
function OrderButton() {
const order = useModel(OrderModel);
const { isLoading } = useLoader();
return (
<button onClick={order.submit} disabled={isLoading}>
{isLoading ? "Processing..." : "Place Order"}
</button>
);
}
Практическое применение: e-commerce case
Inventory Management
Реализуем сложные правила управления остатками:
class InventoryModel {
reservations = new Map<string, number>();
reserve(productId: string, quantity: number) {
const available = this.getAvailableStock(productId);
if (available < quantity) {
throw new InventoryError("Not enough stock");
}
this.reservations.set(productId,
(this.reservations.get(productId) || 0) + quantity);
}
release(productId: string) {
this.reservations.delete(productId);
}
}
Price Engine
Доменная логика для динамического ценообразования:
class PricingModel {
applyPricingRules(rules: PricingRule[]) {
return rules.reduce((price, rule) => {
switch (rule.type) {
case "percentage":
return price * (1 - rule.value / 100);
case "fixed":
return price - rule.value;
case "tiered":
return rule.tiers.find(t => price >= t.min)?.price || price;
default:
return price;
}
}, this.basePrice);
}
}
Тестирование доменных моделей
easy-model encourages test-first approach:
describe("PricingModel", () => {
it("applies percentage discount correctly", () => {
const model = new PricingModel(100);
const rules = [{ type: "percentage", value: 10 }];
expect(model.applyPricingRules(rules)).toBe(90);
});
it("throws on invalid discount", () => {
const model = new PricingModel(100);
const rules = [{ type: "percentage", value: 110 }];
expect(() => model.applyPricingRules(rules)).toThrow();
});
});
Заключение
easy-model предлагает senior разработчикам:
- Четкое разделение ответственности (SRP)
- Инкапсуляцию бизнес-правил
- Готовые решения для DI и async операций
- Тестируемую архитектуру
Для сложных SPA, особенно в e-commerce, это достойная альтернатива традиционным стейт-менеджерам. Главное - правильно выделить bounded contexts и не смешивать доменные модели с UI-логикой.
Источник: https://dev.to/zyf93/domain-driven-development-with-easy-model-9m9