habrahabr

DTO в JS

  • воскресенье, 11 июля 2021 г. в 00:32:39
https://habr.com/ru/post/567040/
  • JavaScript


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

Тем не менее, DTO являются важным классом объектов в приложениях и в этой статье я покажу JS-код, который на данный момент считаю оптимальным для DTO (в рамках стандартов ECMAScript 2015+).

Структура данных

Во-первых, в коде должна быть отражена сама структура данных. Лучше всего это делать с использованием классов (аннотации JSDoc помогают ориентироваться в типах данных):

class ConfigEmailAuth {
    /** @type {string} */
    pass;
    /** @type {string} */
    user;
}

Это пример простой структуры данных, где каждый атрибут является примитивом. Если некоторые атрибуты сами являются структурами, то класс выглядит примерно так:

class ConfigEmail {
    /** @type {ConfigEmailAuth} */
    auth;
    /** @type {string} */
    from;
    /** @type {string} */
    host;
    /** @type {number} */
    port;
    /** @type {boolean} */
    secure;
}

Создание объектов

Как правило, создание экземпляра DTO в половине случаев связано разбором имеющейся структуры данных, полученной "по проводам" с "другой стороны". Поэтому конструктор DTO получает на вход некоторый JS-объект, из которого пытается извлечь знакомые ему данные:

/**
 * @param {ConfigEmailAuth|null} data
 */
constructor(data = null) {
    this.pass = data?.pass;
    this.user = data?.user;
}

В конструкторе структуры со сложными атрибутами используются конструкторы для соответствующих атрибутов:

/**
 * @param {ConfigEmail} data
 */
constructor(data = null) {
    this.auth = (data?.auth instanceof ConfigEmailAuth)
        ? data.auth : new ConfigEmailAuth(data?.auth);
    this.from = data?.from || 'default@from.com';
    this.host = data?.host || 'localhost';
    this.port = data?.port || 465;
    this.secure = data?.secure || true;
}

Если какой-то атрибут представляет из себя массив, то в конструкторе его разбор выглядит примерно так:

class ConfigItems {
    /** @type {Item[]} */
    items;

    /**
     * @param {ConfigItems} data
     */
    constructor(data = null) {
        this.items = Array.isArray(data?.items)
            ? data.items.map((one) => (one instanceof Item) ? one : new Item(one))
            : [];
    }
}

Если какие-то данные должны быть сохранены в атрибуте без разбора, то это тоже возможно (хотя к DTO имеет такое себе отношение):

class SomeDto {
    /** @type {Object} */
    unknownStruct;

    /**
     * @param {SomeDto} data
     */
    constructor(data = null) {
        this.unknownStruct = data?.unknownStruct;
    }

}

Метаданные

Метаданные - это информация о коде. Метаданные позволяют отследить, где используются соответствующие атрибуты объекта:

class SaleOrder {
    /** @type {number} */
    amount;
    /** @type {number} */
    id;
}

SaleOrder.AMOUNT = 'amount';
SaleOrder.ID = 'id';

Например, при выборке данных из БД:

const query = trx.from('sale');
query.select([
    {[SaleOrder.ID]: 'saleId'},
    {[SaleOrder.AMOUNT]: 'totalAmount'},
    // ...
]);

Результирующую выборку можно напрямую передавать в конструктор SaleOrder, а затем получившийся DTO выкидывать на web в качестве ответа.

Резюме

Если сводить воедино все три составляющих DTO (структура, конструктор, метаданные), то получается примерно такой es-модуль:

import ConfigEmailAuth from './ConfigEmailAuth.mjs';

export default class ConfigEmail {
    /** @type {ConfigEmailAuth} */
    auth;
    /** @type {string} */
    from;
    // ...

    /**
     * @param {ConfigEmail} data
     */
    constructor(data = null) {
        this.auth = (data?.auth instanceof ConfigEmailAuth)
            ? data.auth : new ConfigEmailAuth(data?.auth);
        this.from = data?.from || 'default@from.com';
        // ...
    }

}

ConfigEmail.AUTH = 'auth';
ConfigEmail.FROM = 'from';
// ...

Подобный подход позволяет извлекать знакомые подструктуры данных из больших структур (конфигурационных файлов, ответов от сервисов и т.п.), непосредственно относящиеся к текущему контексту, а IDE, за счёт аннотаций, имеет возможность помогать разработчику ориентироваться в этой подструктуре.

"Вот и всё, что я могу сказать об этом." (с)

Информационные системы предназначены для обработки данных, а DTO (Data Transfer Object) является важным концептом в современной разработке. В “классическом” понимании DTO являются простыми объектами (без логики), описывающими структуры данных, передаваемых “по проводам” между разнесенными процессами (remote processes). Зачастую данные "по проводам" передаются в виде JSON.