Julien Chomarat
En architecture logicielle, il arrive assez souvent de vouloir étendre le comportement d'une classe pour lui ajouter de nouvelles fonctionnalités.
Dans cet article nous allons voir comment implémenter ce pattern pas à pas.
Tout d'abord voici notre solution de base, un petit programme qui va requêter plusieurs fois une API pour récupérer son adresse IP.
L'interface IPClient
va définir notre contrat pour nos différents clients.
Enfin on définit notre classe SimpleIPClient
qui va s'occuper de faire un appel à l'API et nous afficher notre IP.
var httpClient = new HttpClient();
IIPClient ipClient = new SimpleIPClient(httpClient);
for (int i = 0; i < 5; i++)
{
var ip = await ipClient.GetMyIp();
Log.Info($"My public IP address is: {ip}");
}
public static class Log
{
public static void Info(string message)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {message}");
}
}
public interface IIPClient
{
Task<string> GetMyIp();
}
public class SimpleIPClient : IIPClient
{
private readonly HttpClient _httpClient;
public SimpleIPClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<string> GetMyIp()
{
Log.Info("Using SimpleClient");
var ip = await _httpClient.GetStringAsync("https://api.ipify.org");
return ip;
}
}
Si on exécute notre programme tel quel, on va faire 5 appels à l'API et cela peut prendre un peu de temps comme on peut le voir sur les logs.
[11:41:37] Using SimpleClient
[11:41:38] My public IP address is: 1.1.1.1
[11:41:38] Using SimpleClient
[11:41:43] My public IP address is: 1.1.1.1
[11:41:43] Using SimpleClient
[11:41:45] My public IP address is: 1.1.1.1
[11:41:45] Using SimpleClient
[11:41:48] My public IP address is: 1.1.1.1
[11:41:48] Using SimpleClient
[11:41:52] My public IP address is: 1.1.1.1
Maintenant, ce que l'on souhaiterait faire, c'est récupérer la première fois notre adresse IP depuis l'API et la stocker en cache. Enfin pour les appels suivants on utilisera notre cache directement.
Pour cela, on va donc créer notre classe SimpleIPClientWithCaching
qui elle aussi hérite de IIPClient
.
Comme expliqué au début, le principe du pattern Decorator
et d'ajouter des fonctionnalités sans réécrire le client.
Regardons ensemble les points importants dans le code ci-dessous.
public class SimpleIPClientWithCaching : IIPClient
{
private readonly SimpleIPClient _ipClient;
private string _ip = "";
// Le constructeur prend en paramètre un SimpleIPClient
public SimpleIPClientWithCaching(SimpleIPClient ipClient)
{
_ipClient = ipClient;
}
// Ici on implémente l'interface et on augmente le comportement de base.
public async Task<string> GetMyIp()
{
// On vérifie si le champ _ip est vide.
if (string.IsNullOrEmpty(_ip))
{
// Si c'est le cas on appelle la méthode de notre SimpleIPClient
// qui effectuera un appel à l'API et on stocke en cache la réponse.
_ip = await _ipClient.GetMyIp();
return _ip;
}
// Si une valeur est déjà renseignée, alors on la retourne directement
// sans passer par un appel API.
Log.Info("Using CachedClient");
return _ip;
}
}
Maintenant qu'on a mis en place notre décorateur, on va juste modifier une ligne dans notre programme comme suit.
var httpClient = new HttpClient();
// On change de client ici pour notre SimpleIPClientWithCaching
IIPClient ipClient = new SimpleIPClientWithCaching(new SimpleIPClient(httpClient));
for (int i = 0; i < 5; i++)
{
var ip = await ipClient.GetMyIp();
Log.Info($"My public IP address is: {ip}");
}
En regardant les logs, on constate que le premier appel est un peu lent et les autres sont immédiats.
[11:46:54] Using SimpleClient
[11:46:55] My public IP address is: 1.1.1.1
[11:46:55] Using CachedClient
[11:46:55] My public IP address is: 1.1.1.1
[11:46:55] Using CachedClient
[11:46:55] My public IP address is: 1.1.1.1
[11:46:55] Using CachedClient
[11:46:55] My public IP address is: 1.1.1.1
[11:46:55] Using CachedClient
[11:46:55] My public IP address is: 1.1.1.1
Comme vous pouvez le voir, ce pattern est très efficace pour faire évoluer le comportement de certains objets.
On pourrait tout à fait imaginer utiliser la version sans cache pendant le développement et la remplacer avec la version cache quand l'application est en production.
Ceci est une version possible du pattern Decorator
qui a l'avantage d'être relativement simple à mettre en place. Il existe des versions un peu plus compliquées faisant intervenir des classes abstraites intermédiaires et un héritage plus complexe.