Design Pattern
Créationnels

Builder

Construire des objets complexes étape par étape

Problème

Quand un objet a de nombreux paramètres optionnels ou doit être construit en plusieurs étapes, le constructeur devient illisible.

// Difficile à lire, facile à se tromper dans l'ordre
const query = new QueryBuilder(
  'users',
  ['id', 'name', 'email'],
  { role: 'admin' },
  'name',
  'ASC',
  0,
  20,
  true
);

Builder classique

interface Query {
  table: string;
  fields: string[];
  conditions: Record<string, unknown>;
  orderBy?: string;
  orderDir?: 'ASC' | 'DESC';
  offset: number;
  limit: number;
  withDeleted: boolean;
}

class QueryBuilder {
  private query: Partial<Query> = {
    fields: [],
    conditions: {},
    offset: 0,
    limit: 20,
    withDeleted: false,
  };

  from(table: string): this {
    this.query.table = table;
    return this;
  }

  select(...fields: string[]): this {
    this.query.fields = fields;
    return this;
  }

  where(conditions: Record<string, unknown>): this {
    this.query.conditions = { ...this.query.conditions, ...conditions };
    return this;
  }

  orderBy(field: string, dir: 'ASC' | 'DESC' = 'ASC'): this {
    this.query.orderBy = field;
    this.query.orderDir = dir;
    return this;
  }

  paginate(offset: number, limit: number): this {
    this.query.offset = offset;
    this.query.limit = limit;
    return this;
  }

  includeSoftDeleted(): this {
    this.query.withDeleted = true;
    return this;
  }

  build(): Query {
    if (!this.query.table) throw new Error('Table is required');
    return this.query as Query;
  }
}

// Lisible, ordre flexible, paramètres explicites
const query = new QueryBuilder()
  .from('users')
  .select('id', 'name', 'email')
  .where({ role: 'admin' })
  .orderBy('name')
  .paginate(0, 20)
  .build();

Builder fonctionnel

En TypeScript, le pattern Builder s'exprime souvent mieux avec des fonctions chaînées :

type RequestConfig = {
  url: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers: Record<string, string>;
  body?: unknown;
  timeout: number;
};

function buildRequest(url: string) {
  const config: RequestConfig = {
    url,
    method: 'GET',
    headers: { 'Content-Type': 'application/json' },
    timeout: 5000,
  };

  return {
    method(m: RequestConfig['method']) {
      config.method = m;
      return this;
    },
    header(key: string, value: string) {
      config.headers[key] = value;
      return this;
    },
    body(data: unknown) {
      config.body = data;
      return this;
    },
    timeout(ms: number) {
      config.timeout = ms;
      return this;
    },
    build(): RequestConfig {
      return { ...config };
    },
  };
}

const req = buildRequest('/api/users')
  .method('POST')
  .header('Authorization', `Bearer ${token}`)
  .body({ name: 'Alice', email: 'alice@example.com' })
  .timeout(3000)
  .build();

En React : Builder de formulaire

type FieldConfig = {
  name: string;
  label: string;
  type: 'text' | 'email' | 'password' | 'select';
  required?: boolean;
  options?: { value: string; label: string }[];
  validation?: (value: string) => string | null;
};

class FormBuilder {
  private fields: FieldConfig[] = [];

  addTextField(name: string, label: string, required = false): this {
    this.fields.push({ name, label, type: 'text', required });
    return this;
  }

  addEmailField(name: string, label: string): this {
    this.fields.push({
      name, label, type: 'email', required: true,
      validation: (v) => /\S+@\S+\.\S+/.test(v) ? null : 'Email invalide',
    });
    return this;
  }

  addSelectField(name: string, label: string, options: { value: string; label: string }[]): this {
    this.fields.push({ name, label, type: 'select', options });
    return this;
  }

  build(): FieldConfig[] {
    return [...this.fields];
  }
}

const loginFormFields = new FormBuilder()
  .addEmailField('email', 'Adresse email')
  .addTextField('password', 'Mot de passe', true)
  .build();