Pour donner suite à mon précédent post sur « [.NET] Classes abstraites et les interfaces » j’ai décidé de vous donner un exemple concret pour illustrer l’avantage que vous pouvez en tirer en privilégiant la composition à l’héritage. Un petit exemple sera plus utile qu’un long discours.

Je vais prendre un exemple simple, et je suis désolé pour les puristes si je me suis permis de prendre quelques raccourcis de « conception » mais je l’ai fait pour ne pas alourdir le code et pour mieux illustrer les propos que je vais exposer dans la suite de ce poste.

Admettons que nous devons implémenter pour une application de musique une classe « InstrumentMusique ». Cette classe doit servir comme classe de base pour instancier tous les instruments que nous pouvons manipuler dans notre application comme par exemple guitare, piano, harmonica... Dans un premier temps on nous demande d’implémenter le comportement « Apparaitre » pour afficher un instrument et « Jouer » pour jouer de l’instrument. On se dit, facile ! Puisque chaque instrument a une apparence différente nous pouvons déclarer une méthode abstraite « Apparaitre » au niveau de la classe de base. Ensuite, les classes qui en héritent implémenterons chacune à leur manière le code pour afficher une guitare, un piano, un harmonica ou un autre instrument. La méthode « Jouer » peut être directement implémentée au niveau de notre classe abstraite puisque on peut jouer de chaque instrument. La tentation est grande de le faire de cette manière :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    /// <summary>
/// Notre classe de base qui servira à instancier les implémentations
/// concrètes pour chaque instrument
/// </summary>
public abstract class InstrumentMusique
{
public void Jouer()
{
Console.WriteLine("Jouer de l'instrument");
}

public abstract void Apparaitre();
}

public class Guitare : InstrumentMusique
{
public override void Apparaitre()
{
Console.WriteLine("Je suis une guitare");
}
}

public class Piano : InstrumentMusique
{
public override void Apparaitre()
{
Console.WriteLine("Je suis un piano");
}
}

public class Harmonica : InstrumentMusique
{
public override void Apparaitre()
{
Console.WriteLine("Je suis un harmonica");
}
}

Cela marche très bien et tout le monde est content Smile.

Cependant on nous signale qu’une fonctionnalité a été oubliée. Il faut pouvoir accorder un instrument. De plus nous devons pouvoir accorder notre instrument des manières différentes. Une guitare peut être accordée à l’aide d’un diapason, pour un piano il faut faire venir un accordeur professionnel et pourquoi pas…ne pas permettre à l’utilisateur de notre application musicale de décider comment il voudrait accorder son instrument. Il peut décider par exemple d’accorder la guitare une fois à l’aide d’un diapason et la fois suivante à l’aide d’un accordeur électronique ou pourquoi pas à l’oreille. Facile, on ajoute une méthode « Accorder » au niveau de la classe abstraite comme ça on pourra accorder facilement nos instruments qui en héritent. Oui mais…on y réfléchissant bien tous les instruments ne s’accordent pas comme le harmonica par exemple. Comment faire ? Ajouter une méthode abstraite « Accorder » au niveau de la classe de base « InstruementMusique » comme ça toutes les classes des instruments qui en héritent peuvent fournir leur propre implémentation ? Cependant cette solution n’est pas suffisante car au niveau de la classe « Harmonica » cette méthode sera vide puisque l’harmonica ne s’accorde pas donc on n’a pas besoin d’implémenter le code. De plus pour la guitare on doit gérer dans la méthode « Accorder » les différents cas (accorder à l’aide d’un diapason, d’un accordeur électronique, etc.)

Heureusement qu’il y a des interfaces ! Je défini une interface « IAccorder » avec une méthode « Accorder » Ainsi, tous les instruments qui sont susceptibles d’être accordés implémenteront cette interface et auront une méthode « Accorder ». Notre exemple ressemble à présent à ceci :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/// <summary>
/// Les instruments qui héritent de cette interface
/// auront une capacité à êre accordés
/// </summary>
public interface IAccorder
{
void Accorder();
}

/// <summary>
/// Notre classe de base qui servira à instancier les implémentations
/// concrètes pour chaque instrument
/// </summary>
public abstract class InstrumentMusique
{
public void Jouer()
{
Console.WriteLine("Jouer de l'instrument");
}

public abstract void Apparaitre();
}

public class Guitare : InstrumentMusique, IAccorder
{
public override void Apparaitre()
{
Console.WriteLine("Je suis une guitare");
}

public void Accorder()
{
Console.WriteLine("Accorder la guitare");
}
}
public class Harmonica : InstrumentMusique
{
public override void Apparaitre()
{
Console.WriteLine("Je suis un harmonica");
}
}

Cependant ceci n’est pas une bonne solution. Pourquoi ? Si nous devons modifier le comportement d’une 50aine d’instrument afin qu’ils puissent être accordés ça serait une vraie galère.

A ce stade nous pouvons voir que l’héritage n’est pas la bonne réponse à nos soucis car le comportement des instruments de musique ne cesse de varier d’une sous-classe à l’autre et il n’est pas approprié à toutes les sous-classes. De même, le fait d’isoler la méthode « Accorder » au niveau de l’interface ne nous aide pas non plus car l’interface ne contient pas de code donc nous n’avons pas de code réutilisable. Un autre désavantage de cette solution est que nous devons manipuler une instance concrète de l’instrument pour pouvoir utiliser la méthode « Accorder » de cette manière :

Guitare guitare = new Guitare();

Au lieu d’utiliser une référence de notre instrument de musique de manière polymorphe :

InstrumentMusique guitare = new Guitare();

Alors comment faire ?

Heureusement, nous avons à notre disposition un pattern qui s’appelle « Stratégie ». Le pattern stratégie est prévu pour définir une famille d’algorithmes, encapsuler chacun comme objet et les rendre interchangeables. Cela permet à l’algorithme de varier indépendamment des clients qui l’utilisent. Regardons d’abord le schéma UML :

Stratégie

Si on analyse bien notre problème en vue de le régler avec notre pattern « Stratégie » nous arrivons aux conclusions suivantes :

  • Il faut isoler les parties de notre programme qui sont susceptible d’être modifiées de celles qui ne bougent pas. Dans notre cas nous pouvons isoler le comportement lié à l’accordage d’un instrument donc la méthode « Accorder ».
  • Ensuite nous devons programmer notre interface. Cela signifie que nous devons fournir une implémentation concrète de notre interface et qui soit indépendante de la classe qui l’utilise. Ceci permettra de programmer l’interface « Accorder » pour obtenir les comportements indépendants « AccorderAvecDiapason », « AccorderAvecAccElec », « AccorderAOreille », « NePasAccorder », etc.
  • Maintenant notre instrument de musique va déléguer ses comportements au lieu d’utiliser la méthode « Accorder » définie dans la classe « InstrumentMusique » ou dans une sous classe.

Stratégie Instruement

Voici à quoi peut ressembler notre implémentation :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
    /// <summary>
/// Les instruments qui héritent de cette interface
/// auront une capacité à êre accordés
/// </summary>
public interface IAccorder
{
void Accorder();
}

/// <summary>
/// Implémentation concrète pour accorder notre
/// instrument avec le diapason
/// </summary>
public class AccorderAvecDiapason : IAccorder
{
public void Accorder()
{
Console.WriteLine("Accorder avec diapason");
}
}

public class AccorderAvecAccElec : IAccorder
{
public void Accorder()
{
Console.WriteLine("Accorder avec accordeur électrionique");
}
}

public class NePasAccorder : IAccorder
{
public void Accorder()
{
Console.WriteLine("Ne pas accorder");
}
}
/// <summary>
/// Notre classe de base qui servira à instancier les implémentations
/// concrètes pour chaque instrument
/// </summary>
public abstract class InstrumentMusique
{
// lors de l'exécution contient une référence vers
// un comportement spécifique
protected IAccorder _accordage;

public void AccorderInstruement()
{
this._accordage.Accorder();
}

public void Jouer()
{
Console.WriteLine("Jouer de l'instrument");
}

public abstract void Apparaitre();
}

public class Guitare : InstrumentMusique
{
public Guitare(IAccorder accordage)
{
this._accordage = accordage;
}

public override void Apparaitre()
{
Console.WriteLine("Je suis une guitare");
}
}

Nous avons une implémentation similaire pour tous les instruments comme le piano l’harmonica et les autres. Pour faire marcher tout ça nous pouvons le faire de cette manière :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void Main(string[] args)
{
// guitare
InstrumentMusique guitare = new Guitare(new AccorderAvecAccElec());
guitare.Apparaitre();
guitare.AccorderInstruement();
guitare.Jouer();

// piano
InstrumentMusique piano = new Piano(new AccorderAvecDiapason());
piano.Apparaitre();
piano.AccorderInstruement();
piano.Jouer();

// harmonica
InstrumentMusique harmonica = new Harmonica(new NePasAccorder());
harmonica.Apparaitre();
harmonica.AccorderInstruement();
harmonica.Jouer();

Console.ReadKey(false);
}

Nous pouvons donc constater qu’au niveau de la classe « InstrumentMusique » nous avons une variable qui contiendra une référence vers un comportement concret qui peut être défini pendant l’exécution. Dans cette implémentation nous définissons le comportement à adopter au moment de l’instanciation de l’instrument concret, comme ceci :

InstrumentMusique guitare = new Guitare(new AccorderAvecAccElec());

Nous pouvons aussi déclarer une méthode au niveau de la classe « InstruementMusique » qui permettra au niveau de l’exécution à n’importe quel moment changer le comportement de notre classe.

1
2
3
4
        public void SetAccordage(IAccorder accordage)
{
this._accordage = accordage;
}

De cette manière nous pouvons modifier l’accordage de notre classe « Guitare » plus tard dans le programme de cette manière :

1
2
3
4
5
6
            // guitare
InstrumentMusique guitare = new Guitare(new AccorderAvecAccElec());
guitare.Apparaitre();
guitare.AccorderInstruement();
guitare.SetAccordage(new AccorderAvecDiapason());
guitare.AccorderInstruement();

Voici la sortie :

Conclusion

Grâce à notre dessign pattern «Stratégie » nous avons réussi à respecter les bons principes de programmation que voici :

  • Nous avons séparé le comportement de la classe « InstrumentMusique ». Nous avons extrait la méthode « Accorder » de la classe pour créer un nouvel ensemble des classes afin de représenter chaque comportement (AccorderAvecDiapason, etc.). Ainsi les classes des instruments de musique n’ont pas besoin de savoir les détails de leur propre comportement.
  • Nous avons programmé une interface et ainsi les autres types d’objet peuvent réutiliser nos comportements d’accordage car ils ne sont plus cachés dans notre classe « InstruementMusique ». C’est la REUTILISATION du code !
  • Un des principes de conception qui règle le pattern « Stratégie » dit « Open/Closed » est adressé par notre implémentation. Notre classe doit être fermé à la modification et ouverte à l’extensibilité. Nous pouvons ainsi ajouter d’autres comportements sans modifier la classe de base !
  • Nous avons préféré la composition à l’héritage.

J’espère que ce poste vous sera utile. N’hésitez pas à me faire un retour, comme ça dans un post future je peux traiter une autre problématique de conception objet.

A bientôt !