Aller au contenu principal

Pattern Decorator

Attachez dynamiquement du comportement supplémentaire à une fonction, sans modifier la fonction originale.


Le Problème

Vous avez une fonction d'analyse de séquences ADN. Maintenant il faut ajouter un filtrage qualité. Puis du cache pour les séquences de référence. Puis de la logique de retry pour les longues séquences. Puis des métriques de temps.

L'approche naïve :

async function analyzeSequence(dna: string) {
console.log(`Analyzing ${dna.length} base pairs`); // logging mélangé
const start = Date.now(); // timing mélangé

if (getQualityScore(dna) < 30) { // filtrage mélangé
throw new Error("Low quality sequence");
}

const cached = cache.get(dna); // cache mélangé
if (cached) return cached;

try {
const result = await runAnalysis(dna);
cache.set(dna, result);
console.log(`Took ${Date.now() - start}ms`);
return result;
} catch (e) {
// logique de retry mélangée...
}
}

L'analyse principale est enterrée sous les préoccupations transversales. Tester est pénible.


La Solution

Gardez la fonction principale pure. Empilez des decorators qui wrappent chacun le précédent — même signature en entrée, même signature en sortie :

import { decorate } from "@pithos/core/eidos/decorator/decorator";

// Fonction principale pure — juste l'analyse
const analyzeSequence = async (dna: string) => runAnalysis(dna);

// Chaque decorator wrappe le précédent, en préservant la signature
const enhanced = decorate(
analyzeSequence,
withQualityFilter(30), // rejeter les séquences de mauvaise qualité
withCache(new Map()), // mettre en cache les résultats des séquences connues
withRetry(3), // retry en cas de timeout
withTiming("analysis"), // journaliser le temps d'exécution
);

const result = await enhanced("ATCGATCG...");

Le consommateur voit la même signature (dna: string) => Promise<Result> quel que soit le nombre de couches empilées. C'est l'idée clé : les decorators sont invisibles pour l'appelant.

Pour les cas plus simples, utilisez les helpers before, after et around :

import { before, after, around } from "@pithos/core/eidos/decorator/decorator";

// before/after pour les effets de bord
const withLogging = before((dna) => console.log(`Analyzing ${dna.length}bp`));
const withMetrics = after((_, result) => metrics.record(result));

// around pour un contrôle total (cache, retry, etc.)
const withCache = (cache: Map<string, Result>) => around((fn, dna) => {
const cached = cache.get(dna);
if (cached) return cached;
const result = fn(dna);
cache.set(dna, result);
return result;
});

Démo

Code
/**
* DNA Analysis Pipeline using Pithos Decorator pattern.
*
* decorate(fn, ...decorators) stacks behaviors without modifying the core function.
* Each decorator wraps the previous one — same signature in, same signature out.
*/
import { decorate, around, type Decorator } from "@pithos/core/eidos/decorator/decorator";
import type { AnalysisResult, DecoratorOption, LogEntry } from "./types";

/** Shared execution log for visualization */
export const executionLog: LogEntry[] = [];

export function clearLog() { executionLog.length = 0; }

function log(decorator: string, action: string, duration?: number) {
executionLog.push({ decorator, action, timestamp: Date.now(), duration });
}

/** Core analysis function — pure, no side effects */
export async function analyzeSequence(dna: string): Promise<AnalysisResult> {
await new Promise((r) => setTimeout(r, 100 + Math.random() * 200));
const upper = dna.toUpperCase();
const gcCount = (upper.match(/[GC]/g) || []).length;
const gcContent = (gcCount / dna.length) * 100;
const quality = Math.min(100, 50 + gcContent / 2 + Math.random() * 20);
return { gcContent: Math.round(gcContent * 10) / 10, length: dna.length, quality: Math.round(quality), isValid: true };
}

/** Quality filter decorator */
function withQualityFilter(minQuality: number): Decorator<string, Promise<AnalysisResult>> {
return around(async (fn, dna) => {
log("Quality Filter", `Checking sequence (min: ${minQuality})`);
const start = Date.now();
const upper = dna.toUpperCase();
const estimatedQuality = ((upper.match(/[ATCG]/g) || []).length / dna.length) * 100;
if (estimatedQuality < minQuality) {
log("Quality Filter", `❌ Rejected (quality: ${estimatedQuality.toFixed(0)})`, Date.now() - start);
throw new Error(`Sequence quality ${estimatedQuality.toFixed(0)} below threshold ${minQuality}`);
}
log("Quality Filter", `✓ Passed (quality: ${estimatedQuality.toFixed(0)})`, Date.now() - start);
return fn(dna);
});
}

/** Cache decorator */
function withCache(cache: Map<string, AnalysisResult>): Decorator<string, Promise<AnalysisResult>> {
return around(async (fn, dna) => {
log("Cache", "Checking cache...");
const start = Date.now();
const cached = cache.get(dna);
if (cached) { log("Cache", "✓ Cache hit!", Date.now() - start); return cached; }
log("Cache", "○ Cache miss, analyzing...", Date.now() - start);
const result = await fn(dna);
cache.set(dna, result);
return result;
});
}

/** Retry decorator */
function withRetry(maxAttempts: number): Decorator<string, Promise<AnalysisResult>> {
return around(async (fn, dna) => {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
log("Retry", `Attempt ${attempt}/${maxAttempts}`);
const start = Date.now();
try {
const result = await fn(dna);
log("Retry", `✓ Success on attempt ${attempt}`, Date.now() - start);
return result;
} catch (e) {
lastError = e instanceof Error ? e : new Error(String(e));
log("Retry", `✗ Failed: ${lastError.message}`, Date.now() - start);
if (attempt < maxAttempts) await new Promise((r) => setTimeout(r, 100 * attempt));
}
}
throw lastError;
});
}

/** Timing decorator */
function withTiming(label: string): Decorator<string, Promise<AnalysisResult>> {
return around(async (fn, dna) => {
log("Timing", `Starting ${label}...`);
const start = Date.now();
const result = await fn(dna);
log("Timing", `${label} completed in ${Date.now() - start}ms`, Date.now() - start);
return result;
});
}

/** Shared cache instance */
const analysisCache = new Map<string, AnalysisResult>();
export function clearCache() { analysisCache.clear(); }

const DECORATOR_FACTORIES: Record<DecoratorOption, () => Decorator<string, Promise<AnalysisResult>>> = {
qualityFilter: () => withQualityFilter(80),
cache: () => withCache(analysisCache),
retry: () => withRetry(3),
timing: () => withTiming("analysis"),
};

/** Build the analysis pipeline based on selected decorators */
export function buildPipeline(options: DecoratorOption[]): (dna: string) => Promise<AnalysisResult> {
const decorators = options.map((opt) => DECORATOR_FACTORIES[opt]());
return decorators.length ? decorate(analyzeSequence, ...decorators) : analyzeSequence;
}
Result

Analogie

Une commande de café. Commencez par un espresso (le cœur). Ajoutez du lait (decorator). Ajoutez du sucre (decorator). Ajoutez de la crème fouettée (decorator). Chaque ajout wrappe le résultat précédent sans changer la façon dont l'espresso est préparé.


Quand l'Utiliser

  • Ajouter du logging, du cache, de la validation, du retry ou du timing à des fonctions existantes
  • Vous voulez composer plusieurs comportements indépendants
  • Les préoccupations transversales doivent être séparées de la logique métier

Quand NE PAS l'Utiliser

Si vous avez juste besoin d'une amélioration fixe, une simple fonction wrapper suffit. Decorator brille quand vous composez plusieurs comportements indépendants.


API

  • decorate — Appliquer plusieurs decorators à une fonction
  • before — Exécuter du code avant l'exécution de la fonction
  • after — Exécuter du code après le retour de la fonction
  • around — Wrapper la fonction avec un contrôle total sur l'exécution