Design patterns en TypeScript - 09 - Les anti-patterns : ce qu'il ne faut PAS faire

Les anti-patterns les plus frequents : God Object, Spaghetti Code, Lava Flow, premature abstraction. Comment les reconnaitre et les corriger.

09 - Les anti-patterns : ce qu'il ne faut PAS faire

Ce que tu vas apprendre

  • Six anti-patterns courants en TypeScript et comment les reconnaitre
  • Des exemples avant/apres pour chaque anti-pattern
  • Les signaux d'alerte dans ton code

Prerequisites

Avoir lu les articles précédents de la serie, surtout le Repository et la DI.


Qu'est-ce qu'un anti-pattern ?

Un pattern est une solution recurrente a un problème recurrent. Un anti-pattern est une solution recurrente qui créé plus de problèmes qu'elle n'en resout. La différence avec du "mauvais code" : un anti-pattern a un nom, une structure reconnaissable et une solution connue. On le reconnait, on le nomme, on le corrige.

1. God Object (God Class)

Une classe qui fait tout. Elle gere les commandes, envoie les mails, valide les donnees, formate les réponses HTTP et calcule les promotions. 1000+ lignes, 40 méthodes, des imports dans tous les sens.

typescript// AVANT : le God Object
class OrderManager {
  async createOrder(data: any) { /* 80 lignes */ }
  async cancelOrder(id: string) { /* 60 lignes */ }
  async refundOrder(id: string) { /* 50 lignes */ }
  async sendConfirmationEmail(order: Order) { /* 30 lignes */ }
  async sendRefundEmail(order: Order) { /* 30 lignes */ }
  async calculateDiscount(items: CartItem[]) { /* 40 lignes */ }
  async validateCoupon(code: string) { /* 25 lignes */ }
  async updateInventory(order: Order) { /* 35 lignes */ }
  async generateInvoicePdf(order: Order) { /* 60 lignes */ }
  async syncWithAccounting(order: Order) { /* 45 lignes */ }
  // ... 20 autres methodes
}

Le signal d'alerte : tu ouvres un fichier et tu scrolles pendant 10 secondes pour trouver la méthode que tu cherches.

typescript// APRES : une responsabilite par classe
class OrderService {
  constructor(
    private repo: OrderRepository,
    private emailer: OrderEmailer,
    private inventory: InventoryService,
    private pricing: PricingService,
  ) {}

  async createOrder(data: OrderData): Promise<Order> {
    const discount = this.pricing.calculateDiscount(data.items);
    const order = Order.create(data, discount);
    await this.repo.save(order);
    await this.inventory.reserve(order.items);
    await this.emailer.sendConfirmation(order);
    return order;
  }
}

Chaque service a sa responsabilité. OrderService orchestre, PricingService calcule, OrderEmailer envoie. C'est le S de SOLID (Single Responsibility).

2. Spaghetti Code

Pas de structure. Les controllers appellent la base directement. Les services appellent d'autres services en boucle. Un module utilitaire de 500 lignes avec des fonctions qui n'ont rien a voir entre elles. Tu modifies une ligne et trois features cassent.

typescript// AVANT : tout est mele
app.post("/orders", async (req, res) => {
  const user = await db.query("SELECT * FROM users WHERE id = $1", [req.body.userId]);
  if (!user.rows[0]) return res.status(404).send("User not found");
  const items = req.body.items;
  let total = 0;
  for (const item of items) {
    const product = await db.query("SELECT * FROM products WHERE id = $1", [item.id]);
    if (product.rows[0].stock < item.qty) return res.status(400).send("Out of stock");
    total += product.rows[0].price * item.qty;
  }
  if (req.body.coupon) {
    const coupon = await db.query("SELECT * FROM coupons WHERE code = $1", [req.body.coupon]);
    if (coupon.rows[0]) total *= (1 - coupon.rows[0].discount / 100);
  }
  await db.query("INSERT INTO orders ...", [/* ... */]);
  await fetch("https://hooks.slack.com/...", { method: "POST", body: JSON.stringify({ text: "New order!" }) });
  res.json({ total });
});

SQL, validation, logique métier, notification Slack, tout dans un seul handler. Le fix : des couches. Controller (recoit la requête) -> Service (logique métier) -> Repository (acces donnees). Comme je l'explique dans la serie architecture hexagonale.

typescript// APRES : des couches
app.post("/orders", async (req, res) => {
  const result = await orderService.createOrder(req.body);
  res.json(result);
});

Le controller fait une ligne. La logique est dans OrderService. Le SQL est dans OrderRepository.

3. Lava Flow

Du code mort que personne n'ose supprimer. Des fonctions commentees "au cas ou". Des fichiers utils-old.ts, order-service-v2.ts, helpers-backup.ts. Des branches if (false).

typescript// AVANT : de la lave figee
function calculateTotal(items: CartItem[]): number {
  // NOTE: ancienne methode, garder pour reference
  // const oldTotal = items.reduce((s, i) => s + i.price, 0);

  // v2: avec remise volume (Thomas, mars 2024)
  // TODO: verifier si c'est encore utilise
  const total = items.reduce((sum, item) => {
    // if (USE_NEW_PRICING) {
    return sum + item.price * item.qty;
    // } else {
    //   return sum + item.price;
    // }
  }, 0);

  return total;
}

Le fix : supprime. Git a l'historique. Si tu as besoin du code dans 6 mois, git log et git show sont la. Le code commente est du bruit visuel qui ralentit la lecture.

typescript// APRES : propre
function calculateTotal(items: CartItem[]): number {
  return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}

4. Premature Abstraction

Creer une interface avant d'avoir deux implementations. Creer un BaseService<T> générique pour un seul service. Écrire un "framework" interne pour un besoin qui existe une seule fois.

typescript// AVANT : abstraction prematuree
interface IUserService {
  getUser(id: string): Promise<User>;
}

class UserServiceImpl implements IUserService {
  async getUser(id: string): Promise<User> {
    return this.repo.findById(id);
  }
}

// L'interface n'a qu'une seule implementation. Elle ne sert a rien.

La regle : attends la duplication. Quand tu as besoin d'un deuxieme fournisseur d'images, la, tu créés l'interface ImageSearchPort et tu refactorises. Pas avant. C'est ce qui s'est passe sur paltemps.fr : le premier provider etait Unsplash, sans abstraction. Quand Pexels est arrive, on a créé l'interface et les adaptateurs.

typescript// APRES : pas d'interface inutile
class UserService {
  async getUser(id: string): Promise<User> {
    return this.repo.findById(id);
  }
}
// Le jour ou tu as besoin d'une deuxieme implementation, tu extrais l'interface.

Exception : si tu veux tester avec un fake (comme le InMemoryRepository), l'interface se justifie meme avec une seule implementation production. C'est un cas réel de polymorphisme (prod vs test).

5. Callback Hell

Moins frequent depuis async/await, mais ca existe encore, surtout avec des API anciennes ou des EventEmitters imbriques :

typescript// AVANT : l'enfer des callbacks
fs.readFile("config.json", (err, data) => {
  if (err) return handleError(err);
  const config = JSON.parse(data.toString());
  db.connect(config.dbUrl, (err, conn) => {
    if (err) return handleError(err);
    conn.query("SELECT * FROM users", (err, users) => {
      if (err) return handleError(err);
      users.forEach((user) => {
        sendEmail(user.email, "Hello", (err) => {
          if (err) console.error("Failed for", user.email);
        });
      });
    });
  });
});

Le fix en 2026 devrait etre évident :

typescript// APRES : async/await
const data = await fs.promises.readFile("config.json", "utf-8");
const config = JSON.parse(data);
const conn = await db.connect(config.dbUrl);
const users = await conn.query("SELECT * FROM users");

await Promise.allSettled(
  users.map((user) => sendEmail(user.email, "Hello"))
);

Plat, lisible, erreurs gerees par try/catch. Si une API ne supporte que les callbacks, enveloppe-la dans une Promise avec util.promisify ou manuellement.

6. Magic Numbers et Magic Strings

Des valeurs literals dans le code sans explication. if (status === 3). setTimeout(fn, 86400000). if (role === "adm").

typescript// AVANT : des nombres magiques
if (order.status === 3) {
  // ... qu'est-ce que 3 ?
}

setTimeout(cleanup, 86400000); // c'est quoi en heures ?

if (retries > 5) throw new Error("Too many retries");

Le fix : des constantes nommees ou des enums.

typescript// APRES : des noms qui parlent
const OrderStatus = {
  PENDING: 0,
  CONFIRMED: 1,
  SHIPPED: 2,
  DELIVERED: 3,
  CANCELLED: 4,
} as const;

type OrderStatus = (typeof OrderStatus)[keyof typeof OrderStatus];

if (order.status === OrderStatus.DELIVERED) {
  // clair
}

const ONE_DAY_MS = 24 * 60 * 60 * 1000;
setTimeout(cleanup, ONE_DAY_MS);

const MAX_RETRIES = 5;
if (retries > MAX_RETRIES) throw new Error("Too many retries");

Comment reperer les anti-patterns dans ton code

Quelques signaux :

  • Un fichier de plus de 300 lignes ? Probablement un God Object. Decoupe-le.
  • Tu copies-colles plus de 10 lignes ? Extrais une fonction. Mais attends la troisieme copie avant d'abstraire (Rule of Three).
  • Tu n'oses pas supprimer du code ? C'est du Lava Flow. git log est ton filet de sécurité.
  • Tu créés une interface pour une seule classe ? Demande-toi si tu la testeras avec un fake. Si non, supprime l'interface.
  • Tu as un switch avec 6+ cas dans un controller ? C'est du Spaghetti qui demande un pattern Strategy.

Résumé

  • God Object : trop de responsabilités dans une classe (fix : découper)
  • Spaghetti Code : pas de couches, tout appelle tout (fix : controller/service/repository)
  • Lava Flow : code mort qu'on garde "au cas ou" (fix : supprimer, git a l'historique)
  • Premature Abstraction : interfaces sans deuxieme implementation (fix : attendre la duplication)
  • Callback Hell : callbacks imbriques (fix : async/await)
  • Magic Numbers : valeurs literals sans nom (fix : constantes et enums)

Article précédent : 08 - Singleton, Service Locator et DI

Article suivant : 10 - Glossaire

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.