← tous les articles

C#

La POO en C# : les exemples qui ont enfin fait tilt

Dog, Animal, et Bark() n'ont jamais tenu. Encapsulation, abstraction, inheritance, et polymorphism ont enfin fait tilt quand je les ai vus dans un bank account, un notification service, et un payment processor à la place.

La POO en C# : les exemples qui ont enfin fait tilt

Chaque tutoriel de POO par lequel j'ai commencé utilisait les mêmes exemples de manuel : un Dog qui hérite de Animal et appelle Bark(), une Car qui hérite de Vehicle et appelle Drive(). Rien de tout ça n'a tenu, parce que je ne construis ni chenils ni concessions automobiles. Je construis des flux de paiement et des intégrations d'API. Les quatre piliers n'ont vraiment fait tilt que lorsque je les ai vus dans du code qui ressemblait à ce que j'écris vraiment.

Encapsulation : BankAccount

La version qui n'a pas fait tilt était un champ public que n'importe quoi pouvait définir directement :

public class BankAccount
{
    public decimal Balance;
}

Rien n'empêche n'importe quel code de la solution de définir Balance à un nombre négatif. Ce qui a fait tilt pour l'encapsulation, ce n'était pas « cachez le champ », c'était de réaliser que l'object devrait être la seule chose autorisée à changer son propre état, et seulement via des règles qu'il applique lui-même :

public class BankAccount
{
    public decimal Balance { get; private set; }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Deposit amount must be positive.");

        Balance += amount;
    }

    public void Withdraw(decimal amount)
    {
        if (amount > Balance)
            throw new InvalidOperationException("Insufficient funds.");

        Balance -= amount;
    }
}

Abstraction : un notification service

L'abstraction a fait tilt quand j'ai arrêté de la voir comme « faire une interface » et que j'ai commencé à la voir comme cacher le bazar dont l'appelant ne devrait pas avoir à se soucier :

public interface INotificationService
{
    Task SendSmsAsync(string phoneNumber, string message);
}

public class TwilioNotificationService : INotificationService
{
    private readonly HttpClient _httpClient;

    public async Task SendSmsAsync(string phoneNumber, string message)
    {
        // formatage du payload HTTP, signature de l'auth header, gestion des erreurs
    }
}

Peu importe ce qui appelle SendSmsAsync, ça n'a pas besoin de connaître les status codes HTTP ou la signature des headers. Cet écart entre le contrat simple et l'implémentation en désordre derrière lui, c'est ce que l'abstraction vous offre vraiment.

Inheritance : un template de payment processor

Les chaînes d'inheritance profondes n'ont jamais fait sens pour moi jusqu'à ce que je voie l'inheritance utilisée pour un workflow partagé plutôt qu'une taxonomie partagée :

public abstract class PaymentProcessor
{
    public async Task ProcessTransactionAsync(decimal amount)
    {
        ValidateAmount(amount);
        await ExecutePaymentAsync(amount);
        LogReceipt();
    }

    private void ValidateAmount(decimal amount) { /* logique partagée */ }
    private void LogReceipt() { /* logique partagée */ }

    protected abstract Task ExecutePaymentAsync(decimal amount);
}

public class StripePaymentProcessor : PaymentProcessor
{
    protected override async Task ExecutePaymentAsync(decimal amount)
    {
        // logique d'intégration de l'API Stripe
    }
}

La base class possède le processus. Chaque subclass ne fait que surcharger la seule étape qui diffère vraiment. C'est un engagement bien plus léger que les chaînes Dog -> Mammal -> Animal que je n'arrêtais pas de voir dans les tutoriels.

Polymorphism : remplacer des payment providers

L'exemple qui a fait tilt pour le polymorphism a été de remplacer une chaîne grandissante de if/else par une seule abstraction :

public class CheckoutService
{
    private readonly PaymentProcessor _paymentProcessor;

    public CheckoutService(PaymentProcessor paymentProcessor)
    {
        _paymentProcessor = paymentProcessor;
    }

    public async Task CompleteOrderAsync(Order order)
    {
        await _paymentProcessor.ProcessTransactionAsync(order.TotalAmount);
    }
}

CheckoutService ne sait pas et ne se soucie pas de quelle subclass concrète de PaymentProcessor il a reçue, StripePaymentProcessor ou tout ce qui sera ajouté plus tard. Ajouter une nouvelle méthode de paiement veut dire écrire une nouvelle subclass, pas modifier un switch statement enterré ailleurs dans le codebase.

Ce qui a vraiment changé

Ce qui a fait tenir ces quatre concepts, c'est qu'ils sont tous des choses que j'écrirais vraiment. Le concept a enfin eu un endroit réel où s'accrocher, au lieu de reposer sur une classe Dog que je n'allais plus jamais utiliser.