Écrire du code n'est pas seulement une compétence technique, c’est aussi un art. Produire un clean code relève de la maîtrise d’une œuvre magistrale. Un code bien structuré et facilement maintenable est essentiel pour construire des applications performantes et évolutives.
Dans un domaine où les technologies évoluent sans cesse et à une vitesse rapide, certaines qualités du code qui tourne derrière les applications font toute la différence. Qu’est-ce qui distingue un bon développeur d’un excellent développeur ?
-La qualité du code : Ecrire un code fonctionnel est excellent, mais aussi lisible, évolutif ,bien testé et facile à maintenir. Adopter des principes comme le Clean Code et DRY peut transformer une application, un projet et un monde IT.
-La résolution de problèmes : Un bon développeur suit les spécifications et besoins pour produire un algorithme qui résout le problème. Un excellent développeur anticipe les obstacles, propose des solutions créatives et garde toujours en tête l'expérience utilisateur.
-La curiosité technique : Un bon développeur maîtrise son stack. Un excellent développeur va plus loin, explore de nouvelles technologies, se forme en permanence et ne se contente pas de ce qu’il sait déjà. Une veille technologique continue.
-La communication : La capacité à expliquer des concepts techniques de manière claire et à collaborer efficacement avec d'autres équipes est un atout rare et précieux. En plus, partager les expertises, expériences et bonnes pratiques créent un environnement là ou tout évolue ensemble. Au-delà des compétences techniques, c’est la volonté de partager, et de contribuer au succès collectif qui distingue les meilleurs développeurs.
-La force de proposition : Savoir remettre en question les choix techniques, suggérer des idées pour amélioration et prendre des initiatives pour optimiser le produit/processus, c’est ce qui permet à un développeur de passer à un niveau supérieur.
Dans cet article, nous explorons les principes fondamentaux du clean code dans le contexte du développement .NET. Nous présentons des techniques et des bonnes pratiques pour améliorer la lisibilité, la compréhension et la maintenabilité de votre code. L’objectif est de fournir des outils pratiques, accompagnés d’exemples concrets, afin de créer des applications de qualité, capables de s’adapter aux besoins en constante évolution.
1. Donnez des noms significatifs
Utiliser des noms clairs, significatifs et pertinents pour les variables et méthodes rend le code auto-explicatif, diminuant ainsi la nécessité de commentaires supplémentaires.
Mauvais exemple :
public class Emp { public string n; public int a; public void d() { Console.WriteLine($"Name: {name}, Age: {age}"); } }
Bon exemple :
public class Employee { public string name; public int age; public void DisplayInfo() { Console.WriteLine($"Name: {name}, Age: {age}"); } }
2. Éviter de retourner null
Retourner null peut provoquer des erreurs de type NullReferenceException lorsque l'appelant tente d'accéder aux propriétés ou méthodes de l'objet retourné. De plus, cela complique la lisibilité et la maintenabilité du code, car l'appelant doit systématiquement vérifier si la valeur retournée est nulle avant de l'utiliser.
Supposons que nous ayons ce morceau de code :
public Employee? GetEmployeeById(int id) { if (id <= 0) { return null; } Employee employee = new(); //La suite de la methode return employee; }
Nous pouvons améliorer cela en lançant une exception plutôt qu'en retournant null. Cela garantit que les erreurs sont traitées de manière explicite, rendant le code plus robuste. Par exemple, nous pourrions avoir:
public Employee? GetEmployeeById(int id) { if (id <= 0) { // throws an exception instead of returning null throw new ArgumentException("Invalid user Id"); } Employee employee = new(); //La suite de la methode return employee; }
3. Garder la taille des classes et des méthodes petites
Chaque classe doit avoir une seule raison de changer. Les classes plus petites sont plus faciles à gérer, tester et maintenir, ce qui nous amène à appliquer le principe de responsabilité unique (Single Responsibility Principle).
Prenons un exemple où le code est surchargé et aborde plusieurs aspects :
public sealed class OrderProcessing { public void ProcessOrder(Order order) { if (!ValidateOrder(order)) { throw new ArgumentException("Invalid order"); } ChargePayment(order); UpdateInventory(order); SendConfirmationEmail(order); LogOrderDetails(order); } private bool ValidateOrder(Order order) { } private void ChargePayment(Order order) { } private void UpdateInventory(Order order) { } private void SendConfirmationEmail(Order order) { } private void LogOrderDetails(Order order) { } }
Cela pourrait être amélioré de la manière suivante, où nous avons des classes séparées pour chaque aspect :
public sealed class OrderValidator { public bool Validate(Order order) { } } public sealed class PaymentProcessor { public void Charge(Order order) { } } public sealed class InventoryManager { public void Update(Order order) { } } public sealed class EmailService { public void SendConfirmation(Order order) { } } public sealed class ImprovedOrderProcessing { private readonly OrderValidator _validator; private readonly PaymentProcessor _paymentProcessor; private readonly InventoryManager _inventoryManager; private readonly EmailService _emailService; public ImprovedOrderProcessing( OrderValidator validator, PaymentProcessor paymentProcessor, InventoryManager inventoryManager, EmailService emailService) { _validator = validator; _paymentProcessor = paymentProcessor; _inventoryManager = inventoryManager; _emailService = emailService; } public void ProcessOrder(Order order) { if (!_validator.Validate(order)) { throw new ArgumentException("Invalid order"); } _paymentProcessor.Charge(order); _inventoryManager.Update(order); _emailService.SendConfirmation(order); } }
4. Appliquer les principes de la programmation orientée objet
Encapsulation : Elle permet de regrouper les données et les méthodes au sein d'une classe, protégeant ainsi les données des manipulations erronées ou des corruptions. L'encapsulation cache également certains attributs et méthodes aux utilisateurs extérieurs à la classe.
Abstraction : Ce principe consiste à dissimuler les détails inutiles à l'utilisateur final d'une classe. Cela permet à l'utilisateur d'interagir avec la classe sans avoir à connaître sa mise en œuvre interne.
Héritage : L'héritage établit des relations hiérarchiques entre les classes, permettant ainsi la réutilisation d'attributs et de méthodes communs, favorisant une structure de code plus cohérente.
Polymorphisme : Ce principe permet à une même méthode d'adopter des comportements différents, que ce soit par le biais de la surcharge de méthode ou du remplacement de méthode.
5. Les principes SOLID
- S : Single Responsibility Principle
- O : Open/Closed Principle
- L : Liskov Substitution Principle
- I : Interface Segregation Principle
- D : Dependency Inversion Principle
Principe de Responsabilité Unique (Single Responsibility Principle) :
Chaque classe, module ou fonction dans un programme doit avoir une seule responsabilité ou un seul objectif. Cela signifie qu'il ne devrait y avoir qu'une seule raison de modifier une classe. En respectant ce principe, on facilite la maintenance et la compréhension du code.Principe Ouvert/Fermé (Open/Closed Principle) :
Le code être flexibles face aux changements - peut modifier le comportement sans avoir à changer l'implémentation existante. En d'autres termes, une classe devrait être ouverte à l'extension, mais fermée à la modification.Principe de Substitution de Liskov (Liskov Substitution Principle) :
Ce principe stipule que les objets d'une classe dérivée doivent pouvoir remplacer les objets de la classe de base sans altérer les propriétés souhaitables du programme. Ainsi, on peut étendre une classe de base sans changer son implémentation originale.Principe de Ségrégation des Interfaces (Interface Segregation Principle) :
Il est préférable de créer des interfaces distinctes pour chaque opération ou exigence, plutôt que d'avoir une seule interface qui réalise toutes les tâches. Cela permet de réduire le couplage et d'améliorer la lisibilité du code.Principe d'Inversion des Dépendances (Dependency Inversion Principle) :
Ce principe encourage à utiliser des interfaces ou des classes abstraites pour définir les interactions entre les différents composants d'un système. Au lieu de faire référence directement à des classes concrètes, les classes de haut niveau devraient interagir avec des abstractions. Cela permet de réduire le couplage entre les différentes parties du code, ce qui rend le système plus flexible et plus facile à modifier.
6. Injection des dépendances
L'injection de dépendances est une technique qui consiste à fournir à une classe ses dépendances nécessaires depuis une source externe, plutôt que de les créer directement à l'intérieur de celle-ci. Cette approche permet de découpler les composants d'une application, rendant ainsi le code plus modulaire et flexible.
En d'autres termes, l'injection de dépendances permet à une classe de se concentrer sur sa fonctionnalité principale sans avoir à se soucier de la création et de la gestion de ses dépendances. Cela favorise un système où le processus de création d'objets est séparé de leur utilisation, facilitant ainsi les tests et les évolutions du code.
Voici un exemple simple d'injection de dépendances en C# à l'aide de la méthode d'injection par constructeur. Dans cet exemple, nous avons une classe EmailService qui dépend d'une interface IEmailSender.
Exemple d'Injection de Dépendances
Étape 1 : Définir l'interface
public interface IEmailSender { void SendEmail(string to, string subject, string body); }
Étape 2 : Implémenter l'interface
public class SmtpEmailSender : IEmailSender { public void SendEmail(string to, string subject, string body) { // Logique pour envoyer un email via SMTP Console.WriteLine($"Email envoyé à {to} avec le sujet : {subject}"); }}
Étape 3 : Créer la classe dépendante
public class EmailService { private readonly IEmailSender \_emailSender; // Injection par constructeur public EmailService(IEmailSender emailSender) { _emailSender = emailSender; } public void NotifyUser(string userEmail) { _emailSender.SendEmail(userEmail, "Notification", "Vous avez une nouvelle notification."); }}
Étape 4 : Configurer l'injection de dépendances
Dans une application ASP.NET Core, on configure l'injection de dépendances dans la classe Startup :
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<IEmailSender, SmtpEmailSender>(); // Enregistrement de l'implémentation services.AddScoped<EmailService>(); // Enregistrement du service }}
Étape 5 : Utiliser la classe dans un contrôleur
Voici comment on peut utiliser EmailService dans un contrôleur :
public class NotificationController : Controller { private readonly EmailService \_emailService; public NotificationController(EmailService emailService) { _emailService = emailService; } public IActionResult SendNotification(string email) { _emailService.NotifyUser(email); return Ok("Notification envoyée."); }}
Dans cet exemple, EmailService dépend de IEmailSender, mais elle ne crée pas d'instance de SmtpEmailSender elle-même. Au lieu de cela, cette dépendance est fournie via l'injection de dépendances, ce qui permet de maintenir un code propre, testable et flexible.
7. Conclusion
Dans un secteur où les technologies évoluent rapidement, certaines qualités sont cruciales pour se démarquer. Il existe encore d’autres bonnes pratiques qui distinguent un bon développement d’un excellent développement. Ces principes sont essentiels pour améliorer la qualité du code, optimiser les processus de développement et favoriser une collaboration efficace au sein des équipes.
Nous continuerons à explorer d'autres pratiques dans un prochain article, alors restez connectés pour découvrir de nouveaux conseils. Ces conseils permettront d'affiner vos compétences et d’aller plus loin dans votre parcours de développeur !