Builder Pattern — Строитель

Пошаговое построение объекта через чейнинг (obj.setX.setY.build). В JS — очень популярный стиль.

Проблема

Сложный объект с десятками опций. Конструктор с 15 параметрами нечитаем: new Request(url, method, headers, body, timeout, retries, cache). Нужно явно указывать, что задаётся: таймаут, логгер, retry-политика и т.д. Builder также упрощает валидацию: проверки в build гарантируют корректность итогового объекта.

Где используется

  • Построение HTTP-запросов
  • Генераторы SQL-запросов: db.select.from.where.limit
  • Конфигурация сложных объектов: webpack config, тест-фикстуры
  • Form builder: динамическое создание форм
  • Email/PDF-генераторы

Решение

  • Каждый шаг — отдельный метод, возвращающий this (чейнинг)
  • В конце — .build возвращает финальный объект
  • Может быть асинхронным: await new Builder.setX.build

Реализации

Query Builder (классический)

class QueryBuilder {
  #table = '';
  #conditions = ;
  #fields = ['*'];
  #limitValue = null;
  #orderByField = null;

  from(table) { this.#table = table; return this; }
  select(...fields) { this.#fields = fields; return this; }
  where(condition) { this.#conditions.push(condition); return this; }
  orderBy(field) { this.#orderByField = field; return this; }
  limit(n) { this.#limitValue = n; return this; }

  build {
    if (!this.#table) throw new Error('Таблица не указана');
    let sql = `SELECT ${this.#fields.join(', ')} FROM ${this.#table}`;
    if (this.#conditions.length) sql += ` WHERE ${this.#conditions.join(' AND ')}`;
    if (this.#orderByField) sql += ` ORDER BY ${this.#orderByField}`;
    if (this.#limitValue) sql += ` LIMIT ${this.#limitValue}`;
    return sql;
  }
}

const query = new QueryBuilder
  .from('users')
  .select('id', 'name', 'email')
  .where('active = true')
  .orderBy('name')
  .limit(10)
  .build;
// SELECT id, name, email FROM users WHERE active = true ORDER BY name LIMIT 10

HTTP Client Builder

class HttpClientBuilder {
  setTimeout(ms) { this.timeout = ms; return this; }
  setLogger(l) { this.logger = l; return this; }
  setRetry(n) { this.retry = n; return this; }
  build {
    return new HttpClient(this.timeout, this.logger, this.retry);
  }
}

const client = new HttpClientBuilder
  .setTimeout(5000)
  .setLogger(console)
  .setRetry(3)
  .build;

Функциональный билдер (через замыкание)

function createRequestBuilder(baseUrl) {
  const config = { url: baseUrl, method: 'GET', headers: {}, body: null, timeout: 5000 };

  const builder = {
    method(m) { config.method = m; return builder; },
    header(k, v) { config.headers[k] = v; return builder; },
    body(data) {
      config.body = JSON.stringify(data);
      config.headers['Content-Type'] = 'application/json';
      return builder;
    },
    timeout(ms) { config.timeout = ms; return builder; },
    build { return { ...config }; } // копия, чтобы дальнейшие изменения не повлияли
  };
  return builder;
}

Где используется в JS-экосистеме

  • Knex.js, TypeORM — query builder для SQL
  • D3.js — chainable API для визуализации
  • jQuery$('.item').addClass('x').on('click', ...) — chainable style
  • fluent assertion в test-фреймворках: expect(x).to.be.equal(y)

Подводные камни

  • Чейнинг затрудняет breakpoints в дебагере — нет промежуточных переменных.
  • Не для immutable объектов: если каждый шаг создаёт новый объект, билдер усложняется.
  • Возврат this ломает, если метод должен вернуть что-то другое — нужны parallel APIs.
  • Не возвращать this — без return this цепочка прерывается с TypeError.
  • Мутация конфигурации после build: если Builder не делает копию при build, изменение config после может испортить уже созданный объект.
  • Builder для простых объектов — избыточный overhead. Используйте объект-опций в конструкторе.

Главные тезисы автора

  • «Чейнинг — стиль JavaScript», билдер вписывается естественно.
  • Query Builder — золотая середина между ORM (скрывает) и сырым SQL (нужно ручное).
  • Билдер может быть асинхронным — await new Builder работает.

🎓 Источники

См. также