Углубление вашего понимания часто достигается за счет того, что вы излагаете сложности предмета другому человеку. По сути, я создал эту статью как инструмент, чтобы лучше понять различные концепции.

Считайте эту статью живым документом, который постоянно развивается и совершенствуется с течением времени для повышения ясности и качества.

Если вы читаете это, то наверняка уже слышали о Чистой архитектуре Роберта С. Мартина.

Итак, зачем использовать этот архитектурный подход? Эта архитектура способствует автономии между слоями, создавая элегантную структуру. Представьте себе: простая замена React на AngularJS, Vue на React без необходимости рефакторинга или повторного тестирования бизнес-логики и передачи данных. Такой сценарий стал возможен благодаря этому архитектурному решению.

Репозиторий

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

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

Репозиторий действует как высокоуровневый интерфейс для взаимодействия бизнес-логики с источником данных (в данном случае через библиотеку API).

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

Репозиторий берет ответ из библиотеки API, преобразует его в формат, более удобный для варианта использования, и выполняет некоторую проверку, связанную с этим преобразованием.

// order.repository.ts

class OrderRepository {
  constructor(apiClient) {
    this.apiClient = apiClient;
  }

  async create(orderData) {
    const apiResponse = await this.apiClient.createOrder(orderData);

    // Validate the API response to ensure it can be used to construct an Order entity
    if (!apiResponse || typeof apiResponse !== 'object') {
      throw new Error('Invalid data received from API client: response must be an object');
    }
    if (!apiResponse.id || !apiResponse.date || !apiResponse.items) {
      throw new Error('Invalid data received from API client: missing required fields');
    }

    // Construct and return an Order entity
    const order = new Order(apiResponse.id, apiResponse.date, apiResponse.items);
    return order;
  }
}

export default OrderRepository;

Что репозиторий проверяет с точки зрения проверки данных? Его проверка направлена ​​на:

  1. Сопоставление данных. Обеспечивает правильную структуру данных, полученных от клиента API, для создания или сопоставления объектов домена (например, Order в нашем примере).
  2. Целостность: подтверждает, что полученные данные содержат всю необходимую информацию для правильной работы приложения в соответствии с его бизнес-правилами.

Интерактор (вариант использования)

В чистой архитектуре Interactor, также известный как уровень варианта использования, является подходящим местом для применения правил, специфичных для бизнеса. Это связано с тем, что на уровне Interactor определяются и организуются бизнес-операции.

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

Бизнес-правила. В то время как объекты применяют бизнес-правила низкого уровня (инварианты, относящиеся к конкретному объекту), варианты использования применяют бизнес-правила высокого уровня (действия, которые приложение должно выполнять при определенных условиях). Например, CreateOrderUseCase будет координировать шаги по созданию заказа, такие как проверка запроса, вызов OrderRepository для сохранения заказа и, возможно, уведомление других частей системы о новом заказе.

Управление потоком данных. Варианты использования контролируют поток данных, поступающих в систему и исходящих из нее. Они принимают данные (обычно в виде простых структур данных или DTO), проверяют и обрабатывают их и выводят результаты (также в виде простых структур данных или DTO). Они действуют как мост между внешним миром (таким как пользовательский интерфейс или внешние системы) и внутренним миром (сущности и доменные службы).

class CreateOrderUseCase {
  constructor(orderRepository, inventoryService, notificationService) {
    this.orderRepository = orderRepository;
    this.inventoryService = inventoryService;
    this.notificationService = notificationService;
  }

  async execute(orderDTO) {
    // Validate the OrderDTO
    if (!orderDTO || typeof orderDTO !== 'object') {
      throw new Error('Invalid order data: data must be an object');
    }
    if (!orderDTO.items || !Array.isArray(orderDTO.items)) {
      throw new Error('Invalid order data: data must include an array of items');
    }

    // Convert OrderDTO to an Order entity
    const order = new Order(orderDTO.id, orderDTO.date, orderDTO.items.map(item => new OrderItem(item.id, item.quantity, item.price)));

    // Check that there is sufficient inventory for the order
    for (const item of order.items) {
      if (!await this.inventoryService.isInStock(item.id, item.quantity)) {
        throw new Error(`Insufficient inventory for item ${item.id}`);
      }
    }

    // Pass the order entity to the repository, which will communicate with the API client
    const createdOrder = await this.orderRepository.create(order);

    // Notify other parts of the system about the new order
    await this.notificationService.notifyOrderCreated(createdOrder);

    // The Order entity returned from the repository should already be valid at this point, as its constructor has enforced the necessary business rules

    // Convert the created Order entity back to an OrderDTO for the response
    const createdOrderDTO = new OrderDTO(createdOrder.id, createdOrder.date, createdOrder.items.map(item => ({id: item.id, quantity: item.quantity, price: item.price})));

    return createdOrderDTO;
  }
}

export default CreateOrderUseCase;

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

Наконец, он действует как мост между внешним миром и внутренними сущностями: он принимает данные как OrderDTO, преобразует их в Order для внутреннего использования, а затем преобразует их обратно в OrderDTO для ответа. Это показывает, как вариант использования управляет потоком данных в системе и из нее.

Сущности

Таким образом, в то время как Entities инкапсулируют поведение, поддерживают состояние и обеспечивают соблюдение бизнес-правил низкого уровня, связанных с конкретным бизнес-объектом, Interactor или вариант использования координируют высокоуровневый процесс использования этих объектов для выполнения конкретной операции или транзакции в ваша система.

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

class Order {
  constructor(id, date, items) {
    if (!items || items.length === 0) {
      throw new Error('Order must have at least one item');
    }
    this.id = id;
    this.date = date;
    this.items = items;
    this.totalPrice = this.calculateTotalPrice();
  }

  calculateTotalPrice() {
    return this.items.reduce((total, item) => total + (item.quantity * item.price), 0);
  }

  addItem(item) {
    this.items.push(item);
    this.totalPrice = this.calculateTotalPrice();
  }

  removeItem(itemId) {
    const itemIndex = this.items.findIndex(item => item.id === itemId);
    if (itemIndex === -1) {
      throw new Error(`Item with id ${itemId} not found`);
    }
    this.items.splice(itemIndex, 1);
    this.totalPrice = this.calculateTotalPrice();
  }
}

Здесь сущность Order — это не только контейнер данных, но и поведение:

  • Конструктор проверяет наличие хотя бы одного элемента в заказе и вычисляет общую стоимость.
  • Метод calculateTotalPrice суммирует цены товаров.
  • Метод addItem позволяет добавить товар в заказ и обновить общую стоимость.
  • Метод removeItem позволяет удалить товар из заказа и обновить общую стоимость.

Такое поведение обеспечивает соблюдение бизнес-правил и гарантирует, что объект Order всегда находится в допустимом состоянии.

Объекты передачи данных (DTO)

DTO, с другой стороны, используются для передачи данных между процессами или между уровнями приложения. Обычно они не содержат никакого поведения, а только данные. DTO чаще всего используются, когда данные перемещаются за пределы приложения, например, из уровня бизнес-логики на уровень представления или по сети между клиентом и сервером.

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

class OrderDTO {
  constructor(order) {
    this.id = order.id;
    this.date = order.date;
    this.items = order.items.map(item => ({
      id: item.id,
      quantity: item.quantity,
      price: item.price
    }));
    this.totalPrice = order.totalPrice;
  }
}

Реагировать компонент

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

import React, { useState } from 'react';
import { OrderDTO } from './OrderDTO';

function OrderForm({ createOrderUseCase }) {
  const [items, setItems] = useState([]);
  const [error, setError] = useState(null);
  // ... other state variables for form inputs

  const handleSubmit = async (event) => {
    event.preventDefault();
    
    // Create an OrderDTO from form data
    const orderDTO = new OrderDTO(/* fill in the order details from form state */);
    
    try {
      // Execute the use case
      const createdOrderDTO = await createOrderUseCase.execute(orderDTO);

      // On success, show a success message, clear the form, or navigate to a different page
      // ...
    } catch (error) {
      setError(error.message);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* form inputs for order details */}
      {error && <p>{error}</p>}
      <button type="submit">Create Order</button>
    </form>
  );
}

export default OrderForm;

Этот пример следует принципу сохранения бизнес-логики за пределами пользовательского интерфейса и внутри варианта использования, которому она принадлежит. Компоненту React нужно только знать, что он должен вызвать метод execute варианта использования с данными заказа, и ему не нужно беспокоиться о каких-либо деталях бизнес-правил или проверке данных.

Почему компонент OrderForm использует DTO заказа вместо сущности заказа?

Компонент OrderForm на уровне пользовательского интерфейса взаимодействует с DTO, а не с сущностями. На это есть несколько причин:

  1. Разделение ответственности. Сущности содержат бизнес-логику, которая должна быть отделена от пользовательского интерфейса. DTO содержат только данные и могут быть структурированы в соответствии с потребностями пользовательского интерфейса. Это поддерживает четкую границу между бизнес-логикой и пользовательским интерфейсом и предотвращает тесную связь пользовательского интерфейса с бизнес-логикой.
  2. Гибкость: благодаря взаимодействию с DTO вместо сущностей пользовательский интерфейс не зависит напрямую от структуры сущностей. Это означает, что объект можно реорганизовать или заменить, не влияя напрямую на пользовательский интерфейс.

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