Aller au contenu

Ouvert/Fermé (OCP)

Principe Ouvert/Fermé (OCP)

Une classe ou un composant doit pouvoir être enrichi sans qu'il soit nécessaire de modifier son implémentation. On privilégie ainsi l'ajout de nouveaux comportements, plutôt que la modification d'un code déjà validé, limitant les risques de régression.

Les mécanismes de la programmation orientée objet, permettant d'implémenter ce concept sont les interfaces, le polymorphisme, l'injection de dépendances, ainsi que les design patterns Strategy et Decorator.

Pour illustrer ce principe, voici la classe Invoice :

public class Invoice
{
    public decimal Amount { get; set; }

    public CustomerType CustomerType { get; set; }
}

public enum CustomerType
{
    Individual = 1,
    Professional = 2
}

public class InvoiceService
{
    public decimal CalculateTotal(Invoice invoice)
    {
        var total = invoice.Amount;

        switch (invoice.CustomerType)
        {
            case CustomerType.Individual:
                total *= 0.95m;     // 5% de remise
                break;

            case CustomerType.Professional:
                total *= 0.90m;     // 10% de remise
                break;
        }

        return total;
    }
}

Cette implémentation fonctionne correctement et calcule le prix remisé de la facture. Cependant, si un nouveau type de client apparaît (par exemple Premium), il sera nécessaire de modifier l'implémentation de la méthode CalculateTotal en ajoutant un nouveau cas dans le bloc switch, afin de définir le pourcentage de remise à appliquer. Or, le principe OCP recommande précisément d'éviter de modifier une implémentation existante. L'objectif est de pouvoir enrichir le comportement de l'application, sans remettre en cause un code déjà validé, limitant ainsi les risques de régression.

Une solution consiste à appliquer le design pattern Strategy, qui permet d'encapsuler chaque règle de calcul dans une classe indépendante. Ainsi, soit l'interface IDiscountStrategy :

public interface IDiscountStrategy
{
    bool CanHandle(CustomerType customerType);

    decimal Calculate(decimal amount);
}

Cette interface sera implémentée par toutes les stratégies de calcul de remise. Implémentons deux stratégies, correspondant aux deux case du bloc switch :

public class IndividualDiscountStrategy : IDiscountStrategy
{
    public bool CanHandle(CustomerType customerType)
        => customerType == CustomerType.Individual;

    public decimal Calculate(decimal amount)
        => amount * 0.95m;
}

public class ProfessionalDiscountStrategy : IDiscountStrategy
{
    public bool CanHandle(CustomerType customerType)
        => customerType == CustomerType.Professional;

    public decimal Calculate(decimal amount)
        => amount * 0.90m;
}

Lors de l'instanciation de la classe InvoiceService, l'ensemble des implémentations de IDiscountStrategy sont injectées automatiquement. La méthode CalculateTotal sélectionne ensuite la stratégie adaptée au type de client pour appliquer la remise correspondante :

public class InvoiceService
{
    private readonly IEnumerable<IDiscountStrategy> _strategies;

    public InvoiceService(IEnumerable<IDiscountStrategy> strategies)
    {
        _strategies = strategies;
    }

    public decimal CalculateTotal(Invoice invoice)
    {
        // Recherche de la stratégie adaptée au type de client
        return _strategies
            .Single(s => s.CanHandle(invoice.CustomerType))
            .Calculate(invoice.Amount);
    }
}

Lors de l'ajout d'un nouveau client (Premium), il nous suffira :

  • De modifier l'énumération CustomerType afin d'ajouter le nouveau type de client :
public enum CustomerType
{
    Individual = 1,
    Professional = 2,
    Premium = 3
}

D'implémenter une nouvelle stratégie correspondant à ce type de client :

public class PremiumDiscountStrategy : IDiscountStrategy
{
    public bool CanHandle(CustomerType customerType)
        => customerType == CustomerType.Premium;

    public decimal Calculate(decimal amount)
        => amount * 0.85m;
}

Ainsi, l'ajout d'un nouveau type de client consiste uniquement à implémenter une nouvelle stratégie implémentant l'interface IDiscountStrategy. Aucune modification n'est apportée à InvoiceService, qui reste stable malgré l'ajout de nouveaux comportements, évitant les éventuelles régressions. Dans cet exemple, cette classe est donc ouverte à l'extension et fermée à la modification, conformément au principe OCP.

Enfin, dans une application .NET, il suffit d'enregistrer les différentes stratégies dans le conteneur d'injection de dépendances, en utilisant la même interface :

services.AddScoped<IDiscountStrategy, IndividualDiscountStrategy>();
services.AddScoped<IDiscountStrategy, ProfessionalDiscountStrategy>();
services.AddScoped<IDiscountStrategy, PremiumDiscountStrategy>();