5 anti-patterns que Claude reproduit en silence dans vos codebases

Par KamangaMay 7, 202615 mins de lecture

En construisant crmcoaching avec Claude (et au fil de mes accompagnements), j'ai vu Claude reproduire les mêmes 5 patterns à chaque nouvelle fonctionnalité. Copiés-collés sans relecture, ils pourrissent la maintenabilité à bas bruit. Aucun ne fait planter la prod tout de suite. Tous coûtent cher à 12 mois.

Voici les 5 anti-patterns que je vois revenir, pourquoi Claude les écrit, et les contre-patterns craft que j'impose pour les neutraliser. Vous pouvez ouvrir votre repo principal et chercher ces signatures dès lundi matin.


Pourquoi Claude reproduit certains patterns en silence

Le constat de base est documenté. Le GitClear AI Copilot Code Quality Report 2024 montre que sur 153 millions de lignes de code analysées, le code "copié" (par opposition à refactoré) a augmenté de 8 fois depuis l'arrivée massive des assistants IA dans les workflows de développement. Le code "moved or refactored" a baissé de 39%. Les équipes consomment de la suggestion brute et la collent dans leur repo, parfois sans modification.

Ce n'est pas un problème de modèle, c'est un problème de training data. Claude apprend à partir de milliards de lignes de code public. Sur GitHub, le code public est massivement écrit par des juniors, des bibliothèques en alpha, des exemples de tutoriels, des copier-collers de Stack Overflow. Quand vous promptez Claude pour une God Function, il vous en livre une, parce que c'est la moyenne statistique de ce qu'il a vu. Et cette moyenne, comme je l'analyse dans le retour d'expérience sur la code review IA, est très en dessous du standard craft attendu en production sérieuse.

Ce que j'ai observé sur crmcoaching : au fil du développement du CRM, j'ai mesuré le ratio de fonctions de plus de 60 lignes dans les commits que j'acceptais tels quels versus ceux que je repassais en review avant de les intégrer. Sans grille de review active : 22% des fonctions Claude dépassaient ce seuil. Avec re-prompt explicite et review systématique : 4%. La codebase ne s'effondre pas tout de suite, mais le coût de modification grimpe linéairement avec la longueur moyenne des fonctions. À 6 mois, c'était le sujet n°1 de mes sessions de refactoring.

Robert C. Martin l'avait écrit dans Clean Code en 2008 : "The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that." Cette règle, exposée en français dans les principes Clean Code et software craftsmanship, Claude ne l'applique pas par défaut. C'est à vous de la lui imposer par le prompt et la review.


Pattern #1 : la God Function

C'est le pattern le plus visible. Claude livre une fonction de 60 à 120 lignes qui fait tout : validation, transformation, accès DB, logging, envoi d'événement. Tout dans le même bloc, parce que c'est ce qui ressort statistiquement le plus dans le training data.

Voici un exemple réel que j'ai vu apparaître dans crmcoaching sur la conversion d'un lead en client :

// apps/api/src/application/use-cases/convert-lead-to-client.use-case.ts
// Généré par Claude sans prompt de découpe (tel quel)

@Injectable()
export class ConvertLeadToClientUseCase {
  constructor(
    private readonly prisma: PrismaClient,
    private readonly brevoService: BrevoService,
    private readonly auditService: AuditService,
    private readonly eventEmitter: EventEmitter2,
  ) {}

  async execute(leadId: string, coachId: string): Promise<ClientDto> {
    // 1. Validation (15 lignes)
    if (!leadId) throw new BadRequestException('leadId requis');
    const lead = await this.prisma.lead.findUnique({ where: { id: leadId } });
    if (!lead) throw new NotFoundException(`Lead ${leadId} introuvable`);
    if (lead.coachId !== coachId) throw new ForbiddenException('Lead hors scope');
    if (lead.status === 'CONVERTED') throw new ConflictException('Lead déjà converti');
    const existingClient = await this.prisma.client.findFirst({
      where: { email: lead.email, coachId },
    });
    if (existingClient) throw new ConflictException('Client existant avec cet email');
    // ... 8 autres lignes de validation métier

    // 2. Création du client (15 lignes)
    const client = await this.prisma.client.create({
      data: {
        email: lead.email,
        firstName: lead.firstName,
        lastName: lead.lastName,
        phone: lead.phone,
        coachId,
        status: 'ACTIVE',
        createdBy: coachId,
        convertedFromLeadId: leadId,
      },
    });
    await this.prisma.lead.update({
      where: { id: leadId },
      data: { status: 'CONVERTED', convertedAt: new Date(), convertedToClientId: client.id },
    });

    // 3. Notification Brevo (12 lignes)
    await this.brevoService.sendTransactionalEmail({
      to: [{ email: client.email, name: `${client.firstName} ${client.lastName}` }],
      templateId: 42,
      params: { firstName: client.firstName, coachName: lead.coachId },
    });
    await this.brevoService.addContactToList(client.email, 'clients-actifs');

    // 4. Audit (6 lignes)
    await this.auditService.log({
      action: 'LEAD_CONVERTED',
      entityType: 'client',
      entityId: client.id,
      userId: coachId,
      meta: { fromLeadId: leadId },
    });

    // 5. Événement domaine (4 lignes)
    this.eventEmitter.emit('client.created', { clientId: client.id, coachId });

    return ClientDto.fromPrisma(client);
  }
}

Le contre-pattern craft, c'est l'application du Single Responsibility Principle. Cette fonction devient un orchestrateur de 10 lignes qui appelle des helpers nommés : assertLeadIsConvertible, createClientFromLead, markLeadAsConverted, notifyClientOnboarding, logConversionAudit, emitClientCreated. Chacun est testable isolément. Chacun est lisible en 30 secondes.

Le prompt que j'utilise : "Découpe ce use case en responsabilités nommées. Chaque helper doit tenir en moins de 20 lignes et avoir un seul motif de changement." Claude exécute proprement. Le problème est qu'il ne le fait jamais spontanément.


Pattern #2 : couplage métier/ORM

Celui-là est plus insidieux. Claude injecte directement le PrismaClient dans les use cases métier. Le résultat : vos règles de gestion ne peuvent plus tourner sans une base Postgres, vos tests ne peuvent plus être unitaires, et changer de stratégie de persistence devient une refonte de plusieurs mois.

Concrètement, j'ai vu ça dans crmcoaching sur le bounded context slot-hold :

// apps/api/src/application/use-cases/check-slot-availability.use-case.ts
// Couplage direct Prisma dans le use case (anti-pattern)

@Injectable()
export class CheckSlotAvailabilityUseCase {
  constructor(
    private readonly prisma: PrismaClient, // couplage direct infra
  ) {}

  async execute(slotId: string, coachId: string): Promise<boolean> {
    const holds = await this.prisma.slotHold.count({
      where: { slotId, status: 'ACTIVE', expiresAt: { gt: new Date() } },
    });
    const slot = await this.prisma.slot.findUnique({ where: { id: slotId } });
    return slot != null && slot.coachId === coachId && holds === 0;
  }
}

Le contre-pattern, c'est l'architecture hexagonale d'Alistair Cockburn (2005). La règle métier ne connaît pas Prisma. Elle prend un port ISlotHoldRepository qui expose countActiveHolds(slotId) et un port ISlotRepository qui expose findById(slotId). L'implémentation Prisma vit dans infrastructure/. Les tests unitaires utilisent un fake repository en mémoire.

// apps/api/src/domain/ports/slot-hold.repository.port.ts
export interface ISlotHoldRepository {
  countActiveHolds(slotId: string): Promise<number>;
}

// apps/api/src/application/use-cases/check-slot-availability.use-case.ts
@Injectable()
export class CheckSlotAvailabilityUseCase {
  constructor(
    private readonly slotRepo: ISlotRepository,
    private readonly slotHoldRepo: ISlotHoldRepository,
  ) {}

  async execute(slotId: string, coachId: string): Promise<boolean> {
    const slot = await this.slotRepo.findById(slotId);
    if (slot == null || slot.coachId !== coachId) return false;
    const activeHolds = await this.slotHoldRepo.countActiveHolds(slotId);
    return activeHolds === 0;
  }
}

Sur crmcoaching, j'ai 47 use cases qui ne dépendent d'aucun module externe. Je peux faire tourner toute la logique métier en CI sans démarrer Postgres. Le jour où je voudrai migrer vers Drizzle, ce sera 2 jours de travail au lieu de plusieurs mois.


Pattern #3 : tests qui ne testent rien

Celui-là me fait grincer des dents à chaque fois. Claude génère des tests qui ont l'air sérieux, avec des describe, des vi.fn(), des assertions, et qui ne testent en réalité aucun comportement métier. Exemple typique sur crmcoaching :

// apps/api/src/application/use-cases/__tests__/create-mentoring-checkout-intent.test.ts
// Test généré par Claude sans prompt de comportement

describe('CreateMentoringCheckoutIntentUseCase', () => {
  it('should call repository save', async () => {
    const repo = { save: vi.fn().mockResolvedValue({ id: 'intent-1' }) };
    const useCase = new CreateMentoringCheckoutIntentUseCase(repo as any);

    await useCase.execute({ coachId: 'c1', clientId: 'cl1', offreId: 'o1' });

    expect(repo.save).toHaveBeenCalledWith(
      expect.objectContaining({ coachId: 'c1' }),
    ); // vérifie un appel, pas un comportement
  });
});

Ce test ne dit rien. Il vérifie que la fonction appelle une autre fonction. C'est tautologique. Si vous changez l'implémentation pour utiliser un event store au lieu d'un repository, le test casse, sans qu'aucun comportement métier ait changé. C'est le pire des deux mondes : couplage fort à l'implémentation, zéro garantie de correction.

Le contre-pattern, c'est ce que Dan North appelait "Tests as Specifications" dans ses articles BDD fondateurs de 2006. Le test décrit un comportement observable :

describe('CreateMentoringCheckoutIntentUseCase', () => {
  it('étant donné une offre active, quand on crée l\'intent, alors l\'intent contient le prix de l\'offre', async () => {
    // Given
    const offre = { id: 'o1', price: 150, currency: 'EUR', status: 'ACTIVE' };
    const repo = new InMemoryCheckoutIntentRepository();
    const offreRepo = new InMemoryOffreRepository([offre]);
    const useCase = new CreateMentoringCheckoutIntentUseCase(repo, offreRepo);

    // When
    const result = await useCase.execute({ coachId: 'c1', clientId: 'cl1', offreId: 'o1' });

    // Then
    expect(result.amount).toBe(150);
    expect(result.currency).toBe('EUR');
    expect(result.status).toBe('PENDING');
  });
});

L'implémentation peut changer, le test reste valable. C'est exactement la philosophie défendue dans l'adoption du Behaviour-Driven Development et dans la Definition of Done bien posée.

Sur crmcoaching, après avoir appliqué ce changement systématiquement, le ratio de tests "behavior-level" est passé de 15% à 70%. La couverture brute a parfois baissé. La couverture utile a triplé.


Pattern #4 : catch-all silencieux

C'est le pattern le plus dangereux, parce qu'il masque les bugs au lieu de les révéler. Claude écrit volontiers :

// apps/api/src/infrastructure/adapters/brevo-notification.adapter.ts

async sendSlotConfirmation(clientEmail: string, slotId: string): Promise<void> {
  try {
    await this.brevoService.sendTransactionalEmail({
      to: [{ email: clientEmail }],
      templateId: 12,
      params: { slotId },
    });
  } catch (e) {
    console.error('notification failed', e);
    // la suite s'exécute comme si tout allait bien
  }
}

Et la suite du code continue normalement. Le slot a-t-il été confirmé par email ? On ne sait pas. Le console.error est-il regardé par quelqu'un en prod ? Probablement pas. Le client voit "Réservation confirmée" alors que Brevo n'a rien envoyé.

Le contre-pattern craft repose sur trois règles. D'abord, ne jamais avaler une exception sans politique explicite : soit on re-throw, soit on dégrade volontairement (tracé dans une métrique), soit on rollback. Ensuite, ne jamais utiliser catch (e) générique sans typer l'exception attendue : une erreur réseau Brevo ne se traite pas comme une DomainException. Enfin, instrumenter chaque catch avec une métrique ou une alerte :

async sendSlotConfirmation(clientEmail: string, slotId: string): Promise<void> {
  try {
    await this.brevoService.sendTransactionalEmail({
      to: [{ email: clientEmail }],
      templateId: 12,
      params: { slotId },
    });
  } catch (err) {
    this.logger.error('brevo.slot_confirmation.failed', {
      clientEmail,
      slotId,
      error: err instanceof Error ? err.message : String(err),
    });
    this.metricsService.increment('notification_failures_total', { type: 'slot_confirmation' });
    throw new NotificationDeliveryException(`Confirmation slot ${slotId} non envoyée`);
  }
}

Michael Nygard a documenté ce sujet en profondeur dans Release It! (2007). Les patterns de résilience type circuit breaker et retry n'ont de sens que si les erreurs sont visibles. Un catch-all silencieux casse toute la stratégie de résilience en amont.


Pattern #5 : dépendances non auditées

Le dernier pattern est le plus invisible. Claude propose une dépendance pour résoudre un problème : "Ajoute stripe pour les checkouts", "Utilise jose pour signer ce token", "Importe date-fns pour ce calcul de durée". Le développeur lance pnpm add <pkg>, ça compile, ça passe les tests, ça merge. Et l'artefact entre dans le repo, sans audit, sans justification.

Le problème, ce sont les chiffres du Snyk Open Source Security Report 2024 : les attaques sur la supply chain ont augmenté de 156% en un an. Les écosystèmes les plus touchés incluent npm, PyPI et Maven Central, précisément ceux que Claude suggère le plus. Sur le cas xz-utils (CVE-2024-3094, mars 2024), une backdoor s'est glissée pendant 2 ans dans un package que tout le monde utilisait sans relire. Le sujet de fond est traité dans les vulnérabilités de sécurité dans le code LLM-généré.

Le contre-pattern craft repose sur 3 réflexes. D'abord, refuser toute nouvelle dépendance qui n'a pas été justifiée par un commentaire de PR (quelle alternative native Node ? quel score de vulnérabilités npm audit ? combien de mainteneurs actifs ?). Ensuite, mesurer la time-to-patch sur les CVE critiques (en dessous de 7 jours = sain). Enfin, automatiser la veille via Dependabot configuré craft avec auto-merge sur les patches et review humaine sur les majors.

Sur crmcoaching, j'audite chaque dépendance que Claude propose. Je vérifie le nombre de mainteneurs, la date du dernier commit, le score pnpm audit. 30 secondes par dépendance. C'est ce qui fait la différence entre un repo sain et un repo qui exfiltre des données dans 2 ans.


Les 5 garde-fous craft à installer dès lundi

Voici la grille condensée. Mettez-la en checklist GitHub PR template.

PatternSignal en reviewContre-pattern craft
God FunctionFonction > 40 lignes ou > 3 niveaux d'indentationDécoupe SRP en sous-fonctions nommées
Couplage ORMPrismaClient injecté dans application/ ou domain/Interface Repository (port), adapter Prisma en infrastructure/
Tests videsexpect(repo.save).toHaveBeenCalled() sans assertion comportementaleFormat Given/When/Then sur comportement observable
Catch-all silencieuxcatch (e) { console.error(e) } sans politiqueRe-throw typé, métrique, ou rollback
Dépendance non auditéeEntrée package.json sans commentaire de justificationAudit 30 sec (pnpm audit) + Dependabot configuré craft

Cette grille tient en 5 minutes par PR. Elle attrape 80% des patterns dangereux que j'ai vus en construisant crmcoaching et lors de mes accompagnements. Elle s'inscrit naturellement dans la philosophie Boy Scout Rule : on laisse le code un peu mieux qu'on l'a trouvé, à chaque PR.


Vous voulez voir ces patterns dans une PR Claude avant qu'ils ne deviennent de la dette ?

Repérer une God Function ou un catch-all silencieux dans une suggestion de Claude, ça ne s'apprend pas en lisant un article : ça se travaille. En mentoring 1:1, je relis votre code avec vous, on rejoue vos prompts, et on installe ensemble le réflexe de review qui transforme Claude en exécutant discipliné. Vous repartez en voyant ce que le modèle ne voit pas.


Ce que ça change concrètement

Sur crmcoaching, après avoir installé cette grille et appliqué une vraie discipline de code review, voici les chiffres avant/après mesurés sur 6 mois.

MétriqueAvant grilleAprès 6 mois
Fonctions > 40 lignes dans le repo22% des fonctions4% des fonctions
Couverture de tests behavior-level15%70%
Incidents prod liés à exception swallowed4 par trimestre0 par trimestre
Time-to-patch CVE critique38 jours6 jours
Heures consacrées au refactoring d'urgence12h/semaine3h/semaine

Le gain est mesurable, et il est financier. Une heure de dev senior coûte entre 80 et 150 euros chargée. Économiser 9 heures par semaine, c'est une différence considérable sur l'année. Pour une équipe qui a peur de "ralentir" en imposant des règles de review, le calcul retourne le sens : c'est l'absence de règles qui ralentit, pas leur présence. C'est exactement l'angle que je détaille dans l'ingénierie logicielle comme avantage concurrentiel.


Conclusion

Ce que je veux que vous reteniez de cet article, c'est que Claude n'écrit pas de mauvais code par malveillance. Il écrit le code statistiquement moyen de son training data, et ce code moyen est plein d'anti-patterns silencieux que les équipes humaines apprenaient à éviter en 5 ans de mentoring. Sans grille explicite, ces patterns rentrent dans votre repo à 2 PR par jour, et au bout de 12 mois votre dette technique a triplé.

La bonne nouvelle, c'est que Claude répond très bien à un prompt précis et à un contre-pattern nommé. Il n'a pas d'ego. Il ne défend pas ses choix. Si vous lui dites "réécris ce use case en respectant le SRP, chaque helper de moins de 20 lignes", il s'exécute proprement. Le craft, en 2026, consiste à savoir nommer les patterns qu'on attend, à les imposer en review, et à transformer Claude en exécutant discipliné plutôt qu'en générateur de moyenne statistique.

Si en lisant ces lignes vous reconnaissez votre situation, vous avez deux choix. Vous pouvez laisser les 5 patterns s'installer un peu plus chaque jour. Ou vous pouvez commencer lundi matin, par une grille de 5 questions en review, et reprendre le contrôle de votre codebase.

Pour suivre la suite des patterns craft que je documente chaque semaine, retrouvez-moi sur mon profil Instagram kamangacode.


Ces 5 garde-fous ne sont qu'un début : il en existe 100

La grille de review décrite ici neutralise 5 anti-patterns que Claude reproduit en silence. Elle fait partie d'un référentiel bien plus large : le Craft Bundle, les 100 pratiques craft que j'applique pour coder propre. Ce sont celles que l'IA ne vous apprendra jamais, parce qu'elle ne les a jamais vues tenir en production sur le long terme.


FAQ sur les anti-patterns Claude et la review craft

1. Comment introduire cette grille sans braquer une équipe qui aime Claude ?

La meilleure approche que j'ai vue, c'est de présenter la grille non comme "Claude écrit du mauvais code" mais comme "Claude écrit le code moyen, et notre standard est plus haut que la moyenne". L'équipe ne se sent pas attaquée, elle se sent élevée. Combiné à une démo concrète sur leur propre repo (3 exemples de God Function trouvés en 5 minutes), l'adhésion vient en 2 sprints. C'est aussi ce que permet une vraie culture engineering de rituels bien posée.

2. Faut-il refuser Claude sur les use cases critiques (paiement, auth) ?

Non, mais il faut prompter différemment. Sur du critique, je donne explicitement à Claude le contexte de concurrence, de criticité, et les patterns attendus. Exemple : "Ce use case gère un checkout Stripe sur un SaaS multi-tenant. Utilise une transaction Prisma, type les exceptions, ne swallow rien." Le résultat est radicalement meilleur.

3. Comment automatiser la détection de ces 5 patterns ?

Partiellement. Biome détecte les fonctions trop longues (noExcessiveLinesPerFunction), la complexité cognitive (noExcessiveCognitiveComplexity), et les catch (e) génériques. Les patterns "tests vides" et "dépendances non auditées" demandent une vigilance humaine ou des outils spécialisés (Vitest coverage + mutation testing pour les tests, pnpm audit + Dependabot pour les packages npm). L'arsenal complet est détaillé dans les outils d'analyse statique en 2026.

4. Le pattern God Function vient-il vraiment de Claude ou existait-il déjà ?

Il existait déjà, mais Claude l'amplifie. Avant 2023, une fonction de 80 lignes existait dans 8% des PR humaines selon mes mesures. En 2025, elle existe dans 23% des PR contenant du code IA-généré non révisé. L'IA ne crée pas le pattern, elle le multiplie par 3.

5. Quelle taille maximum recommander pour une fonction ?

Robert C. Martin disait dans Clean Code : "Functions should be small. Then they should be smaller than that." Concrètement, je m'impose 20 lignes maximum pour une fonction métier, 5 niveaux d'indentation maximum, 3 paramètres maximum (au-delà, c'est un objet). Ce sont des règles arbitraires, mais elles forcent à découper. Sur crmcoaching, 94% des fonctions respectent ces seuils, et Biome (noExcessiveLinesPerFunction) fait échouer le lint si on dépasse.

6. Les tests behavior-level ne ralentissent-ils pas la CI ?

Au contraire. Les tests behavior-level sont souvent plus rapides que les tests unitaires couplés à l'implémentation, parce qu'ils ne re-testent pas la même chose à 5 endroits. Sur crmcoaching, la CI tourne en 4 minutes pour 1 200 tests behavior-level. Le facteur limitant n'est plus le temps, c'est la qualité du test, ce qui est aussi le sujet de fond traité dans la checklist pour tester du code généré par IA.


Ressource gratuite : Engineering Maturity Assessment

L'EMA est l'outil que je propose au début de chaque mission. Il mesure la maturité de votre équipe sur plusieurs axes engineering : qualité du code, gouvernance IA, dette technique, code review. Quelques minutes pour identifier lequel des 5 patterns vous touche le plus, et où concentrer vos efforts en priorité.


Ecris par Kamanga

Expert IT avec 25 ans d'expérience en développement logiciel, diplômé EPITECH et MBA. Spécialisé en software craftsmanship, gestion du changement, stratégie, direction des systèmes d'information, coaching et certifié en agilité.