Fermer

janvier 5, 2023

Éviter l’obsession primitive dans .NET


Nous sommes souvent capables de résoudre des problèmes complexes avec des solutions simples. Éviter l’imperfection connue sous le nom d’obsession primitive nous permet de créer un code de haute qualité, moins sujet aux erreurs, sans trop d’efforts. Découvrez dans cet article de blog comment identifier l’obsession primitive et comment l’éviter grâce à des exemples pratiques.

L’obsession primitive est l’une des imperfections les plus courantes trouvées dans la plupart des systèmes. De nombreux développeurs, même expérimentés, ne peuvent pas détecter ce problème, ce qui se traduit par un code mal écrit, sujet à plusieurs échecs.

Dans cet article, nous verrons une explication de l’obsession primitive, comment l’identifier et comment l’éviter et ainsi créer un meilleur code en utilisant les bonnes pratiques.

Qu’est-ce que l’obsession primitive ?

Selon Wiki C2 documentation, l’obsession primitive est une odeur de code (toute caractéristique dans le code source d’un programme qui indique éventuellement un problème plus profond) où des types de données primitifs sont utilisés pour représenter des idées de domaine. Ce serait un cas où une chaîne est utilisée pour représenter un message ou un entier est utilisé pour représenter une somme d’argent.

Comme pour les autres odeurs de code, l’obsession primitive est générée lorsqu’il n’y a aucune inquiétude quant à d’éventuels problèmes futurs.

En tant que développeur, vous avez probablement dû créer une classe de modèle pour stocker certaines données. Cependant, si cette classe ne contenait que des champs avec des types primitifs, comme des chaînes par exemple, sans méthodes ni sous-classes, l’obsession primitive était sûrement présente dans le code car cela obligeait la classe de service à contenir des méthodes et des règles métier qui auraient pu être créées dans le modèle classe et ainsi garder la classe de service plus propre et plus organisée.

Identifier une obsession primitive

Pour illustrer un exemple d’obsession primitive, créons une application simple juste pour afficher quelques données. Pour garder l’accent sur le but du message, gardons-le aussi simple que possible, sans base de données, etc.

Vous pouvez accéder au code source complet du projet à ce lien : Code source.

Commençons donc par créer une application console. Cet exemple utilise .NET 6 (mais .NET 7 est maintenant disponible!).

Dans Visual Studio :

  • Sélectionnez « Créer un nouveau projet »
  • Sélectionnez « Application console »
  • Nommez le projet « BadExample » (ou ce que vous préférez)

Par borne :

dotnet new console --framework net6.0 -n BadExample

Créez ensuite un dossier appelé « Modèles » et à l’intérieur, créez la classe ci-dessous :

Client

namespace BadExample.Models;

public class Customer
{
    public Customer(string? name, string? email, string? phoneNumber, bool clubMember)
    {
        Name = name;
        Email = email;
        PhoneNumber = phoneNumber;
        ClubMember = clubMember;
    }

    public string? Name { get; set; }
    public string? Email { get; set; }
    public string? PhoneNumber { get; set; }
    public bool ClubMember { get; set; }
}

💡 Notez que dans cette classe de modèle, nous créons uniquement des champs pour stocker des valeurs – il n’y a pas de sous-classe ou de méthode. Cela démontre une odeur de code évidente d’obsession primitive – après tout, nous n’utilisons ici que des types primitifs, comme les champs de texte et les booléens.

Créons maintenant la classe de service qui utilisera l’entité client. Créez donc un nouveau dossier appelé « Services » et créez à l’intérieur la classe ci-dessous :

Service Clients

using BadExample.Models;

namespace BadExample.Services;
public class CustomerService
{
    public static void ServiceProcess(Customer customer, decimal productValue)
    {
        decimal amount = 0;

        if (customer.ClubMember == true)
        {
            amount = productValue - 10;
        }
        else
        {
            amount = productValue;
        }

       if (customer.PhoneNumber.Any(p => char.IsLetter(p)))
        {
            throw new Exception("Phone numbers must contain numbers only");
        }

        string message = @$"Name: {customer.Name} - Email: {customer.Email} - Amount: {amount}";
        Console.WriteLine(message);
    }
}

💡 Remarquez le code que nous venons d’implémenter. Dans la méthode « ServiceProcess », nous implémentons une logique que l’on retrouve couramment dans la grande majorité des systèmes qui ont une obsession primitive. Cela se produit parce que la classe de modèle n’a que des champs pour stocker des données, de sorte que la classe de service est liée pour contenir la logique « if » et « else ».

Bien sûr, les opérations conditionnelles comme « if » et « else » ont été créées pour être utilisées, mais nous devons éviter leur utilisation autant que possible, car elles polluent le code et le rendent difficile à lire et à interpréter lors de toute modification ou même lorsque le déboguer.

L’importance d’éviter l’obsession primitive

Éviter l’obsession primitive permet d’éviter de nombreux problèmes rencontrés dans les systèmes d’aujourd’hui. Parmi les plus courants, citons :

  • Complexités inutiles
  • Règles métier dans différentes parties du code
  • Duplication de code

En conséquence, nous obtenons un code de qualité professionnelle, où nous parvenons à conserver de nombreuses règles métier « liées » à la classe de modèle, où elles sont explicites et facilement accessibles pour tout développeur qui a besoin d’effectuer une maintenance sur le code, tandis que le service La classe est réservée uniquement aux méthodes non dépendantes du modèle, ce qui facilite la lecture et maintient le code propre et cohérent.

Comment éviter l’obsession primitive

Pour éviter l’obsession primitive, on peut suivre les principes décrits dans le livre de Martin Fowler « Refactoring : Improving the Design of Existing Code », où il conseille de remplacer la valeur des données par un objet. Dans ce contexte, « objet » fait référence à une nouvelle classe contenant tous les détails qui composent sa valeur.

Alors, créons maintenant un nouveau projet, mais cette fois en utilisant un bon exemple, sans la présence d’obsession primitive.

Dans Visual Studio :

  • Sélectionnez « Créer un nouveau projet »
  • Sélectionnez « Application console »
  • Nommez le projet « GoodExample » (ou ce que vous préférez)

Par borne :

dotnet new console --framework net6.0 -n GoodExample

Créez ensuite un dossier appelé « Modèles » et à l’intérieur, créez les classes ci-dessous :

namespace GoodExample.Models;

public class ClubMember
{
    public ClubMember(bool isClubMember)
    {
        IsClubMember = isClubMember;
    }

    public bool IsClubMember { get; set; }

    public decimal CalculateAmount(decimal productValue) =>
        IsClubMember ? productValue - 10 : productValue;
}
namespace GoodExample.Models;
public class Customer
{
    public string? Name { get; set; }
    public string? Email { get; set; }
    public PhoneNumber PhoneNumber { get; set; }
    public ClubMember ClubMember { get; set; }

    public Customer(string? name, string? email, PhoneNumber phoneNumber, ClubMember clubMember)
    {
        Name = name;
        Email = email;
        PhoneNumber = phoneNumber;
        ClubMember = clubMember;
    }
}
namespace GoodExample.Models;

public class PhoneNumber
{
    public PhoneNumber(string phoneNumber)
    {
        Value = phoneNumber.Any(p => char.IsLetter(p)) ? throw new Exception("Phone numbers must contain numbers only") : phoneNumber;
    }

    public string Value { get; set; }
}

💡A noter que dans cette nouvelle version de la classe « Client », le champ « ClubMember », qui était auparavant de type booléen, est désormais devenu une sous-classe. Dans celui-ci, nous avons ajouté la méthode « CalculateAmount », qui reçoit une valeur, et, si la propriété « IsClubMember » « est vraie », la valeur du produit est soustraite de la valeur de remise (10). De la même manière, le champ « PhoneNumber » est maintenant une sous-classe, et il y a déjà une méthode pour valider si la valeur contient des lettres, évitant ainsi l’obsession primitive lors de la liaison du calcul de la remise et de la validation des caractères aux classes du modèle client .

L’étape suivante consiste à créer la classe de service qui utilisera les classes de modèle et leurs méthodes.

Créez donc un nouveau dossier appelé « Services » et créez à l’intérieur la classe ci-dessous :

Service Clients

using GoodExample.Models;

namespace GoodExample.Services;
public class CustomerService
{
    public static void ServiceProcess(Customer customer, decimal productValue)
    {
        decimal amount = customer.ClubMember.CalculateAmount(productValue);

        string message = @$"Name: {customer.Name} - Email: {customer.Email} - Amount: {amount}";
        Console.WriteLine(message);
    }
}

💡Notez que maintenant la classe de service n’a plus de logique inutile. Après tout, la méthode « CalculateAmount » de la classe de modèle est utilisée pour vérifier la valeur, et la vérification des caractères est couplée à la classe de modèle, de sorte que le code est propre et facile à comprendre.

Il convient également de noter que nous respectons l’un des principes de SOLIDE (Responsabilité unique) car la responsabilité du calcul du total était attribuée à la méthode « CalculateAmount », qui n’était auparavant affectée à aucune méthode spécifique mais était répartie sur la classe de service.

Conclusion

En créant des classes modèles composées de méthodes et de sous-classes liées à l’entité, nous laissons les classes de service libres de ne contenir que ce qui a du sens, comme des règles métier spécifiques qui n’ont aucun rapport direct avec le domaine.

C’est le moyen idéal d’éviter l’obsession primitive et de créer un code propre et maintenable. Par conséquent, envisagez toujours d’ajouter des méthodes et des sous-classes à vos classes de modèle et de garder la classe de service aussi simple que possible.




Source link

janvier 5, 2023