Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Frédéric Hamel

Assert.Success();

[Back to basics] Encapsulation, bien designer ses classes dans le monde réel

Écrire du code de qualité dans un langage ne sous-entend pas seulement maîtriser sa syntaxe, mais aussi comprendre la philosophie et les méthodologies qui sont derrière. C# est un langage orienté objet, et en tant que tel, les principes et concepts de la programmation orientée objet s'appliquent à lui.

Le design d'objets est parfois un challenge redoutable. L'exercice peut se révéler périlleux, surtout si les bases ne sont pas parfaitement maîtrisées. Je me propose ici de discuter d'encapsulation sur des cas concrets, pour que ce concept soient utilisé au mieux dans nos programmes.

Introduction et définition

L'encapsulation est un concept qui vise à protéger l'intégrité des données d'un objet et à en masquer les détails d'implémentation. En effet, un objet va avoir une ou plusieurs responsabilités : c'est à lui de s'assurer que la charge qui lui a été confiée se passe bien et de faire en sorte que ses clients ne soient pas impactés par la manière dont il le fait.

Prenons par exemple un objet responsable d'un compte bancaire. L'objet devra exposer, par exemple, les méthodes de débit, de crédit et d'obtention du solde du compte. L'objet doit s'assurer qu'aucune autre opération que celles prévues ne soit faite sur le compte, et que si pour des raisons x ou y l'objet doit changer la manière dont est géré le compte, cela soit transparent pour les utilisateurs du compte.

Exemple de la classe Point

Démarrons par un cas simple. Je m'inspire ici d'un excellent exemple de Simon Robinson.

public class Point
{
    public double X;
    public double Y;
}

Ici la classe Point expose directement ses données : aucune encapsulation n'est réalisée.

Pour améliorer cela, on va classiquement rendre les membres privés et les exposer via des propriétés.

public class Point
{
    private double x;
    public double X{
        get { return x; }
        set { x = value; }
    }

    private double y;
    public double Y
    {
        get { return y; }
        set { y = value; }
    }
}

Grâce à ces propriétés, les données de la classe Point sont protégées, et on a mis un niveau d'indirection entre elles et le client. Ce qui permettra par exemple si on le souhaite d'ajouter du code dans le set ou le get en fonction des besoins de l'application (par exemple du log, des validations, la notification du changement des propriétés, etc.).

Regardons maintenant quelques cas d'utilisation de cette classe.

var point = new Point();

//Translate the point
point.X = point.X + 10;
point.Y = point.Y + 10;

//Move the point
point.X = 5;
point.Y = 5;

Pour faire une translation du point, je prends la valeur de chaque axe et je leur ajoute les "offset" désirés. Pour déplacer un point, je lui affecte simplement les nouvelles valeurs.

Le problème ici vient du fait que l'objet ne masque pas vraiment son implémentation interne. En effet, le client récupère les données à l'exérieur de l'objet et les remet dedans une fois le traitement fini. L'objet n'a aucun moyen de savoir si la translation a été faite selon les règles attendues ou pas. Par rapport à la définition posée au début, on voit que les détails de l'implémentation ne sont pas vraiment cachés au client malgré les propriétés, et que l'intégrité des données n'est pas du tout gérée. D'ailleurs, si l'on regarde la syntaxe .NET 3.5 de la classe Point, il est troublant de voir qu'on a l'impression d'être sur l'implémentation originale.

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }
}

Voila par exemple comment la classe Point pourrait être mieux encapsulée :

public class Point
{
    public double X { get; private set; }
    public double Y { get; private set; }

    public Point(double x, double y)
    {
        X = x;
        Y = y;
    }

    public void Translate(Point point) 
    {
        X += point.X;
        Y += point.Y;
    }

    public void Move(Point point)
    {
        X = point.X;
        Y = point.Y;
    }
}

Grâce au mot clé "private", on rend inaccessible la modification des data de l'extérieur. L'initialisation de l'objet est faite via le constructeur et la modification se fait via les méthodes. L'objet Point offre ainsi de meilleures garanties d'intégrité de ses données et ses détails d'implémentation sont mieux masqués.

Oui mais...

Une fois arrivé là, on se demande pourquoi toutes les classes Point ne sont pas implémentées comme cela ! En réalité, un point peut être considéré quasiment comme un type de base, une donnée en elle-même. De plus, il est difficile voire impossible d'imaginer à l'avance tous les traitements que l'on souhaite faire sur un point. Pour ajouter à la difficulté, les points sont des petits objets qui risquent d'être instanciés de très nombreuses fois et manipulés de manière intensive : pour des raison d'optimisation, l'objet Point est souvent implémenté sous forme de structure et ses membres sont directement accessibles pour manipulation.

Généralement, ce n'est pas le cas pour un objet business : l'objet Compte par exemple doit être correctement encapsulé si la banque ne veut pas avoir de sérieux problèmes.

Exemple des collections

Prenons l'exemple suivant : une banque possède un ensemble de comptes. Elle permet de consulter les comptes et de d'ouvrir un compte si tout est valide pour ce compte.

public class Bank
{
    public List<Account> Accounts { get; private set; }

    public Bank(){
        Accounts = new List<Account>();
    }

    public void OpenAccount(Account account) 
    {
        if (Validate(account))
        {
            Accounts.Add(account);
        }
    }

    private bool Validate(Account account) { 
        //Do something
        return true;
    }
}

public class Account
{
}

Ici, il est donc possible de voir les comptes via la propriété Accounts et d'ouvrir un compte grâce à OpenAccount (qui va en plus s'assurer que tout est ok avant de l'ajouter à la liste des comptes de la banque).

Le design de cette classe est catastrophique, car il existe une backdoor qui permet d'ajouter un compte sans que le code de validation ne soit appelé.

//Breaking business rules
bank.Accounts.Add(new Account());
//Instead of
bank.OpenAccount(new Account());

Il faut toujours se méfier quand on expose directement une collection, et se poser la question de si c'est vraiment ce que l'on veut ou pas.

Une solution classique à ce problème est d'exposer un énumérateur qui va offrir un accès en lecture seule à la collection.

public class Bank
{
    private List<Account> accounts = new List<Account>();
    public IEnumerable<Account> Accounts
    {
        get
        {
            return accounts.AsEnumerable();
        }
    }

    public void OpenAccount(Account account) 
    {
        if (Validate(account))
        {
            accounts.Add(account);
        }
    }

    public bool Validate(Account account) { 
        //Do something
        return true;
    }
}

Ici, le fait d'exposer un énumerateur va permettre de donner de la visibilité sur la collection, sans pour autant compromettre son encapsulation. De plus, grâce aux méthodes d'extension de .NET 3.5, exposer un énumerateur est vraiment simple. Si l'on n'a pas d'accès aux méthodes d'extension, on peut toujours utiliser le mot clé "yield".

Exemple ObservableCollection

En WPF par exemple, il n'est pas rare d'avoir à utiliser une ObservableCollection pour que les contrôles synchronisés dessus puissent se mettre à jour automatiquement si des objets sont enlevés ou rajoutés à la collection. En général, on est confrontés au même problème que pour les collections dans le cas précédent. Sauf qu'en plus, l'ObservableCollection ne peut pas vraiment être exposée au framework WPF via un enumerator, car on perd alors tout le côté observable de la collection. Heureusement, le framework .NET propose une solution :

public class Bank
{
    private ObservableCollection<Account> accounts 
        = new ObservableCollection<Account>();

    public ReadOnlyObservableCollection<Account> Accounts 
    {
        get {
            return new ReadOnlyObservableCollection<Account>(accounts);
        }
    }
}

La collection est ainsi protégée et les fonctionalités "Observable" sont préservées.

Remarque : si l'on a besoin de meilleures performances, il n'est pas nécessaire de recréer une nouvelle ReadOnlyObservableCollection à chaque fois.

Conclusion

L'encapsulation est un principe fondamental, et il faut parfois se creuser la tête pour le mettre en oeuvre dans certaines situations. Cependant, cela vaut généralement le coup de se poser ce genre de questions, surtout pour les objets qui vont être réutilisés ou utilisés par d'autres. J'espère que ce post vous sera utile dans vos développements.

Happy programming!

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :
Posted: dimanche 29 juin 2008 10:49 par fredhamel
Classé sous : , ,

Commentaires

Pas de commentaires

Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Merci par Blog de Jérémy Jeanson le 10-01-2019, 20:47

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01