Qualité de Code Contextuelle — Compétence Agent TypeScript
1. Objectif
Agir en tant qu'agent d'amélioration de la qualité du code TypeScript pour rendre le code existant plus facile à comprendre, maintenir, tester et étendre.
N'effectuez pas un refactoring uniquement pour rendre le code sophistiqué. Améliorez-le afin que les développeurs puissent comprendre :
- Ce que fait chaque fonction.
- Pourquoi une règle métier existe.
- Ce que représente chaque variable.
- Quelles données une fonction attend et retourne.
- Ce qui se produit lorsque des données sont manquantes ou invalides.
- Quelles parties de l'application sont responsables de la logique métier, de l'accès aux données et de l'interaction utilisateur.
- Comment une modification affecte le comportement existant de l'application.
Utilisez un langage spécifique au domaine plutôt que des noms de variables et de fonctions vagues.
Lorsque vous travaillez avec du code CRM de vente, utilisez des concepts métier clairs tels que Lead, Contact, Opportunity, SalesRepresentative, LeadStatus, PipelineStage et FollowUpDate.
L'objectif est un logiciel compréhensible, fortement typé, fiable, testable et maintenable.
2. Quand Activer
Utilisez cette compétence lorsque :
- Le code TypeScript contient des variables telles que
x,data,obj,temp,thingouresultsans contexte suffisant. - Les fonctions ont des noms vagues comme
process(),handle(),manage()oudoStuff(). - Une fonction effectue à la fois la validation, les calculs métier, les mises à jour de base de données, les notifications et la journalisation.
- L'application repose sur
any, des assertions de type non sécurisées, des chaînes de caractères non expliquées ou des nombres magiques. - Les règles métier sont dupliquées à plusieurs endroits.
- Les erreurs sont silencieusement ignorées ou converties en réponses de succès trompeuses.
- Les fonctions contiennent des conditions profondément imbriquées ou un branchement excessif.
- La terminologie CRM est incohérente entre les composants, les points d'accès API et les modèles de base de données.
- Le code est difficile à tester sans se connecter à une base de données réelle ou à un service externe.
- Un développeur souhaite améliorer le code existant sans modifier son comportement prévu.
3. Principes Fondamentaux
3.1 Préférer les noms spécifiques au domaine
Les noms doivent expliquer leur signification dans l'application.
Préférez :
const leadId = "lead_123";
const salesRepresentativeId = "user_456";
const nextFollowUpDate = new Date();
const qualifiedLeadCount = 12;
Évitez :
const id = "lead_123";
const user = "user_456";
const date = new Date();
const count = 12;
Les noms courts sont acceptables lorsque leur signification est évidente dans un petit contexte, comme i dans une courte boucle. Ne développez pas chaque nom mécaniquement ; développez les noms lorsque le contexte supplémentaire améliore la compréhension.
3.2 Utiliser TypeScript pour représenter les concepts métier
Préférez les types explicites, les interfaces, les unions discriminées et les modèles de domaine nommés aux objets non structurés.
Évitez d'utiliser any comme échappatoire. Utilisez unknown pour les données véritablement non fiables et validez-les avant utilisation.
3.3 Séparer les responsabilités
Chaque fonction ou module doit avoir une responsabilité principale claire.
Un service CRM ne doit pas avoir à construire des réponses HTTP, manipuler des composants d'interface utilisateur, envoyer des e-mails et exécuter des requêtes de base de données dans une seule fonction.
3.4 Préserver le comportement prévu
Avant le refactoring, identifiez les règles métier existantes et les résultats attendus.
Ne modifiez pas silencieusement :
- Les critères de qualification des leads.
- Les règles de transition du pipeline.
- Les affectations des représentants commerciaux.
- Les règles de planification des suivis.
- Le comportement de détection des doublons.
- Les autorisations et les contrôles d'accès.
- Les formes des réponses API.
- La sémantique de la base de données.
Si le comportement existant semble incorrect ou ambigu, identifiez le problème explicitement au lieu de déguiser un changement de comportement en refactoring.
3.5 Préférer un code simple aux abstractions inutiles
N'introduisez pas de modèles de conception, de classes, de génériques, de fabriques ou de dépendances supplémentaires à moins qu'ils ne résolvent un problème réel.
Une fonction simple avec un nom descriptif est souvent meilleure qu'une abstraction inutilement complexe.
4. Exemple de Code CRM de Vente de Mauvaise Qualité
Le code suivant est intentionnellement problématique. Il illustre les problèmes courants de qualité de code dans une application de gestion de leads.
4.1 Avant : Code de mauvaise qualité
// bad-crm.ts
export async function go(d: any, db: any, mail: any) {
try {
let x = d.name;
let e = d.email;
let p = d.phone || "";
let s = d.status || "new";
let v = Number(d.value || 0);
let u = d.user || null;
if (!x || !e) {
return { ok: false, msg: "bad data" };
}
let old = await db.leads.findOne({ email: e });
if (old && old.status != "lost") {
old.v = old.v + v;
old.s = v > 5000 ? "hot" : old.s;
old.p = p || old.p;
await db.leads.update(old.id, old);
if (old.s == "hot") {
await mail.send(
old.email,
"Hot lead",
"Lead is hot!"
);
}
return { ok: true, data: old };
}
let a = {
name: x,
email: e,
phone: p,
status: s,
value: v,
user: u,
created: new Date(),
updated: new Date(),
};
let r = await db.leads.insert(a);
if (v > 5000) {
await mail.send(
e,
"Hot lead",
"Lead is hot!"
);
}
return { ok: true, data: r };
} catch (e) {
console.log(e);
return { ok: false, msg: "error" };
}
}
4.2 Problèmes que l'agent doit identifier
| Problème | Exemple | Pourquoi c'est important | Amélioration |
|---|---|---|---|
| Nom de fonction flou | go() | Ne communique pas l'opération métier | Utilisez createOrUpdateLead() si cela décrit précisément le comportement |
| Variables floues | d, x, e, p, s, v, u, a, r | Les développeurs doivent déduire le but de chaque variable | Utilisez des noms spécifiques au domaine |
| Types non sécurisés | any pour l'entrée, la base de données et le service d'e-mail | Supprime la capacité de TypeScript à détecter de nombreuses erreurs | Définissez des interfaces et des types d'entrée explicites |
| Règle métier cachée | v > 5000 | Le seuil de qualification n'est pas expliqué | Définissez une constante nommée ou une règle métier documentée |
| Terminologie incohérente | status, hot, lost | Le statut du lead et la qualification peuvent représenter des concepts différents | Définissez un modèle de statut de lead explicite |
| Responsabilités mélangées | Une fonction valide, interroge, met à jour, crée, envoie des e-mails et gère les erreurs | Les comportements individuels sont difficiles à tester | Séparez les responsabilités applicatives, de domaine et d'infrastructure |
| Incohérence potentielle des données | Une mise à jour de base de données peut réussir avant qu'un e-mail échoue | L'appelant peut recevoir une erreur même si le lead a été enregistré | Définissez explicitement le comportement en cas d'échec de notification |
| Validation faible | Vérifie uniquement x et e | Des adresses e-mail invalides, des mauvaises valeurs et des champs malformés peuvent entrer dans le système | Validez l'entrée et les invariants métier |
| Règles de statut implicites | Les leads existants sont mis à jour sauf si leur statut est "lost" | La signification de cette règle est cachée | Nommez et documentez la politique de mise à jour |
| Contrat de persistance flou | db.leads.findOne() et .update() utilisent des formes non documentées | Les hypothèses de base de données sont difficiles à vérifier | Définissez une interface de référentiel |
| Réponse d'erreur générique | "error" | Perd un contexte utile et complique le dépannage | Gérez les échecs attendus explicitement et journalisez les erreurs inattendues de manière appropriée |
| Réponses incohérentes | { ok, msg } et { ok, data } | Les appelants doivent déduire quels champs existent dans chaque cas | Utilisez un type de résultat discriminé ou un modèle d'erreur d'application cohérent |
| Données mutables et peu structurées | old.v, old.s, old.p | Les noms de champs ne correspondent pas au modèle persistant apparent | Définissez un type Lead cohérent |
| Politique de doublon floue | Les enregistrements existants sont fusionnés par e-mail | La signification exacte de la fusion est cachée | Documentez quels champs peuvent être mis à jour et comment la valeur est agrégée |
| Dépendances difficiles à tester | La base de données et le service d'e-mail sont passés en any | Les contrats et les cas d'échec ne sont pas vérifiés par le compilateur | Définissez des interfaces étroites et injectez des dépendances |
Important : Ceci est un exemple illustratif, pas une implémentation CRM de production complète. Un refactoring doit d'abord confirmer le comportement réel du CRM existant, le modèle de données, la sémantique de persistance et les règles métier.
5. Workflow de Refactoring
Suivez ce workflow lors de l'amélioration du code TypeScript existant.
Étape 1 : Comprendre le contexte
Inspectez le code concerné avant de le modifier.
Identifiez :
- Le but de la fonction.
- Ses appelants et dépendances.
- Ses formats d'entrée et de sortie.
- Le schéma de base de données ou le contrat de référentiel.
- Les entités du domaine et la terminologie métier.
- Les règles de validation et d'autorisation existantes.
- Les tests actuels et le comportement attendu.
- La gestion des erreurs et les effets secondaires.
Lisez les modules environnants si nécessaire. Ne déduisez pas le contrat complet d'une fonction à partir de son seul nom.
Étape 2 : Enregistrer le comportement actuel
Décrivez la logique existante en langage simple.
Pour l'exemple ci-dessus, le comportement observé est :
- Lire les détails du lead à partir de l'entrée.
- Rejeter une requête si le nom ou l'e-mail est manquant.
- Trouver un lead existant par e-mail.
- Si un lead correspondant existe et que son statut n'est pas
"lost", ajouter la valeur soumise à la valeur existante. - Marquer ce lead existant comme
"hot"lorsque la valeur résultante dépasse le seuil. - Remplacer son numéro de téléphone lorsqu'un nouveau numéro de téléphone non vide est fourni.
- Mettre à jour l'enregistrement et envoyer une notification si le statut résultant est
"hot". - Sinon, créer un nouvel enregistrement avec les horodatages.
- Notifier le lead lorsque sa valeur soumise dépasse le seuil.
- Retourner un objet de succès ou d'échec.
Cette description distingue intentionnellement le comportement d'implémentation observé du comportement métier prévu. Par exemple, le code teste la valeur résultante de l'enregistrement existant pour un chemin de notification, mais la valeur soumise pour un autre. Ne traitez pas silencieusement ces règles comme équivalentes.
Étape 3 : Améliorer le nommage
Remplacez les noms génériques par des noms qui expriment leur rôle.
| Mauvais nom | Meilleur nom |
|---|---|
go | createOrUpdateLead |
d | leadInput |
x | leadName |
e | emailAddress, ou error dans un bloc catch |
p | phoneNumber |
s | leadStatus |
v | leadValue |
u | salesRepresentativeId |
old | existingLead |
a | newLead |
r | createdLead |
db | leadRepository ou databaseClient, selon son rôle réel |
mail | emailNotificationService |
Le nom amélioré doit représenter avec précision la valeur. Ne renommez pas une valeur en salesRepresentative si elle contient un identifiant numérique. Utilisez salesRepresentativeId dans ce cas.
Concentrez-vous sur la clarté de l'intention, pas sur la longueur du nom.
Étape 4 : Remplacer les types non sécurisés
Remplacez les paramètres any par des types spécifiques.
Définissez une interface pour l'entrée :
interface LeadInput {
leadName: string;
emailAddress: string;
phoneNumber?: string;
leadStatus?: LeadStatus;
leadValue?: number;
salesRepresentativeId?: string;
}
Définissez des types pour les entités du domaine :
type LeadStatus = "new" | "contacted" | "qualified" | "hot" | "lost" | "converted";
interface Lead {
id: string;
leadName: string;
emailAddress: string;
phoneNumber: string;
leadStatus: LeadStatus;
leadValue: number;
salesRepresentativeId: string | null;
createdAt: Date;
updatedAt: Date;
}
Si la dépendance de base de données a un comportement complexe, définissez une interface de référentiel :
interface LeadRepository {
findByEmail(email: string): Promise<Lead | null>;
update(lead: Lead): Promise<Lead>;
insert(lead: Omit<Lead, "id" | "createdAt">): Promise<Lead>;
}
Utilisez des unions discriminées pour les résultats d'opération au lieu d'un objet { ok, data, msg } générique :
type CreateOrUpdateLeadResult =
| { type: "validation_error"; message: string }
| { type: "lead_updated"; lead: Lead }
| { type: "lead_created"; lead: Lead }
| { type: "notification_failed"; cause: Error; lead: Lead };
Ces types décrivent explicitement chaque résultat possible, ce qui rend le code plus facile à suivre, à tester et à étendre.
Étape 5 : Séparer les responsabilités
Divisez la fonction originale en fonctions plus petites et ciblées.
Séparez :
- Validation : Vérifier que l'entrée est complète et correcte.
- Récupération : Charger l'état existant à partir du référentiel.
- Logique métier : Appliquer les règles de mise à jour ou de création.
- Persistance : Enregistrer les modifications dans la base de données.
- Notification : Envoyer des alertes si nécessaire.
- Coordination : Une fonction orchestratrice qui appelle les étapes ci-dessus dans l'ordre et compose les résultats.
Ce modèle de séparation rend chaque étape testable individuellement et rend explicite l'ordre exact des opérations.
Étape 6 : Rendre les règles métier explicites
Remplacez les valeurs magiques et les chaînes de caractères non documentées par des constantes nommées.
const HOT_LEAD_THRESHOLD = 5000;
const EMAIL_TEMPLATE_HOT_LEAD = "hot-lead-notification";
Documentez la règle métier dans le code ou dans un commentaire adjacent si elle n'est pas évidente à partir du nom de la constante.
Lors de l'écriture de documents de conception ou de spécifications, utilisez un langage qui relie le code à l'intention métier. Par exemple, une spécification de règle métier pourrait être :
Règle de requalification des leads : Lorsqu'un lead existant (dont le statut n'est pas
"lost") reçoit une nouvelle valeur de lead, la valeur du lead est mise à jour en ajoutant la nouvelle valeur à la valeur existante. Si la valeur résultante dépasse le seuil de lead chaud (5 000), le statut du lead passe à"hot"et une notification est envoyée.
Gardez les règles métier proches des données qu'elles gouvernent. Évitez de les répartir dans des fichiers non liés ou des modules d'utilitaires vagues.
Étape 7 : Améliorer la gestion des erreurs
Définissez des classes d'erreur ou des types d'échec explicites au lieu de propager des chaînes génériques.
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
}
}
class NotificationFailure extends Error {
constructor(public readonly cause: Error) {
super("Failed to send notification");
this.name = "NotificationFailure";
}
}
Traitez les échecs de notification comme des problèmes séparés des échecs de persistance. N'autorisez pas un échec de notification à faire échouer silencieusement une mise à jour de base de données.
Étape 8 : Améliorer les tests
Utilisez l'injection de dépendances via des interfaces étroites plutôt que de passer la base de données et le service de messagerie directement en tant que any. De cette façon, les tests unitaires peuvent facilement simuler ces dépendances sans avoir besoin d'une base de données réelle ou d'un serveur SMTP.
// Exemple de test
const mockRepository: LeadRepository = {
findByEmail: async () => null,
insert: async (lead) => ({ ...lead, id: "lead-1", createdAt: new Date() }),
update: async (lead) => lead,
};
const mockEmailService: EmailNotificationService = {
send: async () => {},
};
const result = await createOrUpdateLead(input, mockRepository, mockEmailService);
Étape 9 : Valider sans changer de comportement
Après le refactoring, exécutez les tests existants. Comparez les sorties de l'ancien et du nouveau code à l'aide de tests d'instantanés ou de tests de régression déterministes.
Si le comportement existant n'est pas testé, créez d'abord des tests de caractérisation qui capturent le comportement réel, puis refactorez.
6. Exemple de Code Refactoré
Voici une version refactorée de l'exemple précédent, illustrant les principes de cette compétence. Il s'agit d'une amélioration, pas d'une réécriture complète. Dans un projet réel, il faudrait adapter davantage l'implémentation à l'architecture existante de l'application.
// refactored-crm.ts
import { Lead, LeadInput, LeadRepository, EmailNotificationService, CreateOrUpdateLeadResult } from "./domain";
const HOT_LEAD_THRESHOLD = 5000;
function validateLeadInput(input: LeadInput): ValidationError | null {
if (!input.leadName || !input.emailAddress) {
return new ValidationError("leadName and emailAddress are required");
}
return null;
}
function applyBusinessRules(existingLead: Lead, input: LeadInput): Lead {
const updatedLead = { ...existingLead };
updatedLead.leadValue += input.leadValue || 0;
if (updatedLead.leadValue > HOT_LEAD_THRESHOLD) {
updatedLead.leadStatus = "hot";
}
if (input.phoneNumber) {
updatedLead.phoneNumber = input.phoneNumber;
}
updatedLead.updatedAt = new Date();
return updatedLead;
}
export async function createOrUpdateLead(
input: LeadInput,
leadRepository: LeadRepository,
emailNotificationService: EmailNotificationService
): Promise<CreateOrUpdateLeadResult> {
const validationError = validateLeadInput(input);
if (validationError) {
return { type: "validation_error", message: validationError.message };
}
const existingLead = await leadRepository.findByEmail(input.emailAddress);
if (existingLead && existingLead.leadStatus !== "lost") {
const updatedLead = applyBusinessRules(existingLead, input);
await leadRepository.update(updatedLead);
if (updatedLead.leadStatus === "hot") {
try {
await emailNotificationService.send(updatedLead.emailAddress, EMAIL_TEMPLATE_HOT_LEAD, { lead: updatedLead });
} catch (error) {
return { type: "notification_failed", cause: error, lead: updatedLead };
}
}
return { type: "lead_updated", lead: updatedLead };
}
const newLead: Omit<Lead, "id" | "createdAt"> = {
leadName: input.leadName,
emailAddress: input.emailAddress,
phoneNumber: input.phoneNumber || "",
leadStatus: input.leadStatus || "new",
leadValue: input.leadValue || 0,
salesRepresentativeId: input.salesRepresentativeId || null,
updatedAt: new Date(),
};
const createdLead = await leadRepository.insert(newLead);
if (createdLead.leadValue > HOT_LEAD_THRESHOLD) {
try {
await emailNotificationService.send(createdLead.emailAddress, EMAIL_TEMPLATE_HOT_LEAD, { lead: createdLead });
} catch (error) {
return { type: "notification_failed", cause: error, lead: createdLead };
}
}
return { type: "lead_created", lead: createdLead };
}
7. Limitations et Mises en Garde
Le refactoring peut modifier involontairement le comportement. Vérifiez toujours la logique existante avant d'appliquer des modifications. Les correctifs automatisés de cette compétence sont indicatifs et doivent être examinés par un humain.
Cette compétence est conçue pour du code TypeScript dans un environnement de référentiel de code natif. Les codebases legacy compilées en JavaScript peuvent nécessiter des ajustements.
Dans les systèmes CRM existants, les modèles de données sont souvent incohérents en raison de migrations et d'exigences évolutives. Clarifiez l'état actuel avant de supposer un modèle cible.
Les changements de nommage peuvent casser les sérialiseurs, les fonctions de mappage ou les outils externes qui s'attendent à des noms de propriétés spécifiques. Mettez à jour ces dépendances si nécessaire.
Cette compétence se concentre sur l'amélioration de la qualité du code, pas sur l'optimisation des performances. N'introduisez pas de complexité inutile au nom de la "qualité" sans une raison métier vérifiable.
Gardez à l'esprit la pile technologique sous-jacente, les capacités de l'équipe et les objectifs du projet. Ce qui est une "amélioration" dans un contexte peut être un inconvénient dans un autre.