Pattern Builder
Construisez des objets complexes étape par étape. Le même processus de construction peut créer différentes représentations.
Le Problème
Vous développez une bibliothèque de graphiques. Les graphiques ont plein de paramètres optionnels : type, titre, labels, plusieurs datasets, légende, labels d'axes, lignes de grille.
L'approche naïve :
function createChart(
type: string,
title?: string,
labels?: string[],
data?: number[],
data2?: number[],
color?: string,
color2?: string,
showLegend?: boolean,
yAxisLabel?: string
) {
// 9+ paramètres, la plupart optionnels, l'ordre compte
}
// L'appel est pénible
createChart("bar", "Revenue", months, revenue, expenses, "#3b82f6", "#ef4444", true);
Trop de paramètres. Ajouter un second dataset oblige à passer tous les précédents. Facile de se tromper d'ordre.
La Solution
Construisez le graphique étape par étape avec une API fluide :
import { createBuilder } from "@pithos/core/eidos/builder/builder";
// TS infère tous les noms d'étapes — les typos sont des erreurs de compilation
const chartBuilder = createBuilder({
type: "bar",
title: "",
labels: [],
datasets: [],
})
.step("type", (s, type: "bar" | "line") => ({ ...s, type }))
.step("title", (s, title: string) => ({ ...s, title }))
.step("labels", (s, labels: string[]) => ({ ...s, labels }))
.step("data", (s, data: number[], color: string, label: string) => ({
...s,
datasets: [{ label, data, color }],
}))
// Fonctionnalité clé du builder : composer avec l'état précédent
.step("addDataset", (s, label: string, data: number[], color: string) => ({
...s,
datasets: [...s.datasets, { label, data, color }],
}))
.step("legend", (s, show: boolean) => ({ ...s, showLegend: show }))
.done();
// Fluide, lisible, composable
const chart = chartBuilder()
.type("bar")
.title("Monthly Revenue")
.labels(["Jan", "Feb", "Mar", "Apr", "May", "Jun"])
.data([120, 340, 220, 510, 480, 390], "#3b82f6", "Revenue")
.addDataset("Expenses", [80, 150, 120, 200, 180, 160], "#ef4444")
.legend(true)
.build();
Chaque étape est nommée. L'ordre est flexible. .addDataset() compose avec les données précédentes — un simple objet d'options ne pourrait pas faire ça.
Démo
Activez chaque étape du builder et regardez le graphique se mettre à jour en temps réel. Remarquez comment .addDataset() s'empile par-dessus le premier dataset — c'est le builder qui compose l'état en interne.
- builder.ts
- Usage
/**
* Chart builder using the Builder pattern.
*
* Fluent API: createChart().type("bar").title("...").labels([...]).data({...}).build()
* The key insight: .addDataset() composes with previous state — something
* a simple options object can't do.
*/
import { createBuilder } from "@pithos/core/eidos/builder/builder";
import type { ChartConfig, Dataset } from "./types";
const defaultConfig: ChartConfig = {
type: "bar",
title: "",
labels: [],
datasets: [],
showLegend: false,
yAxisLabel: "",
gridLines: true,
};
export const chartBuilder = createBuilder(defaultConfig)
.step("type", (state, type: "bar" | "line") => ({ ...state, type }))
.step("title", (state, title: string) => ({ ...state, title }))
.step("labels", (state, labels: string[]) => ({ ...state, labels }))
.step("data", (state, dataset: Dataset) => ({ ...state, datasets: [dataset] }))
.step("addDataset", (state, dataset: Dataset) => ({ ...state, datasets: [...state.datasets, dataset] }))
.step("legend", (state, show: boolean) => ({ ...state, showLegend: show }))
.step("yAxis", (state, label: string) => ({ ...state, yAxisLabel: label }))
.step("gridLines", (state, show: boolean) => ({ ...state, gridLines: show }))
.done();
export const createChart = () => chartBuilder();
import { useState, useMemo, useCallback } from "react";
import { createChart } from "@/lib/builder";
import { INITIAL_STEPS, MONTHS, REVENUE_DATA, EXPENSES_DATA } from "@/data/charts";
import type { ChartConfig } from "@/lib/types";
export function useChartBuilder() {
const [steps, setSteps] = useState(INITIAL_STEPS);
const [chartType, setChartType] = useState<"bar" | "line">("bar");
const toggleStep = useCallback((id: string) => {
setSteps((prev) => prev.map((s) => (s.id === id ? { ...s, enabled: !s.enabled } : s)));
}, []);
const isEnabled = useCallback((id: string) => steps.find((s) => s.id === id)?.enabled ?? false, [steps]);
const chartConfig = useMemo((): ChartConfig => {
let builder = createChart().type(chartType);
if (isEnabled("title")) builder = builder.title("Monthly Revenue");
if (isEnabled("labels")) builder = builder.labels(MONTHS);
if (isEnabled("data")) builder = builder.data({ label: "Revenue", data: REVENUE_DATA, color: "#3b82f6" });
if (isEnabled("addDataset") && isEnabled("data")) builder = builder.addDataset({ label: "Expenses", data: EXPENSES_DATA, color: "#ef4444" });
if (isEnabled("legend")) builder = builder.legend(true);
if (isEnabled("yAxis")) builder = builder.yAxis("Amount ($)");
return builder.build();
}, [steps, chartType, isEnabled]);
const codePreview = useMemo(() => {
const lines = [`const chart = createChart()\n .type("${chartType}")`];
if (isEnabled("title")) lines.push(' .title("Monthly Revenue")');
if (isEnabled("labels")) lines.push(" .labels([...])");
if (isEnabled("data")) lines.push(' .data({ label: "Revenue", data: [...], color: "#3b82f6" })');
if (isEnabled("addDataset") && isEnabled("data")) lines.push(' .addDataset({ label: "Expenses", data: [...], color: "#ef4444" })');
if (isEnabled("legend")) lines.push(" .legend(true)");
if (isEnabled("yAxis")) lines.push(' .yAxis("Amount ($)")');
lines.push(" .build();");
return lines.join("\n");
}, [steps, chartType, isEnabled]);
return { steps, chartType, setChartType, toggleStep, isEnabled, chartConfig, codePreview };
}
Analogie
Commander une pizza personnalisée. Vous commencez par la pâte, ajoutez la sauce, choisissez le fromage, sélectionnez les garnitures. Chaque étape est indépendante. Vous pouvez ajouter les garnitures dans n'importe quel ordre. Le .build() final, c'est l'enfourner.
Quand l'Utiliser
- Les objets ont beaucoup de paramètres optionnels
- La construction implique plusieurs étapes qui composent
- Vous voulez une API fluide et lisible
- Le même processus doit créer différentes configurations
Quand NE PAS l'Utiliser
Si votre objet peut être entièrement décrit par un simple objet d'options sans étapes composables, préférez ça. Builder ajoute de la cérémonie qui ne vaut le coup que quand les étapes composent ou que la construction est réellement séquentielle.
API
- createBuilder — Créer un builder fluide immuable
- createValidatedBuilder — Builder avec validation à chaque étape