Domain-Driven Development с easy-model: практика для senior фронтендеров

#ddd#typescript#react#state management

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 разработчикам:

  1. Четкое разделение ответственности (SRP)
  2. Инкапсуляцию бизнес-правил
  3. Готовые решения для DI и async операций
  4. Тестируемую архитектуру

Для сложных SPA, особенно в e-commerce, это достойная альтернатива традиционным стейт-менеджерам. Главное - правильно выделить bounded contexts и не смешивать доменные модели с UI-логикой.


Источник: https://dev.to/zyf93/domain-driven-development-with-easy-model-9m9