RAII Pattern
Divulgâchage : Permettant d’éviter les problèmes de gestion des ressources (acquisition et libération), le patron RAII est un must known de la programmation orientée objet. Pour éviter les problèmes (bogues et maintenance), et si votre langage le permet, initialisez dans les constructeurs, libérez dans les destructeurs, et gérez les erreurs avec des exceptions.
S’il y a bien un problème que tout développeur rencontre, c’est la gestion des ressources. La mémoire, les fichiers, les connexions réseaux, les bases de données, les librairies à initialiser (openssl, opengl, …), les mutex, … La liste est longue car si on veut être exact, toute structure est en réalité une ressource.
Dans une zone du code où on a besoin d’une ressource (sa portée, qu’on peut voir comme une petite bulle d’univers), la gestion d’une ressources se résume aux trois phases suivantes :
- L’acquisition : avez-vous obtenu une ressource valide ?
- La manipulation : la ressource reste-t-elle valide ?
- La libération : que se passe-t-il si vous ne libérez pas la ressource ?
Premier (mauvais) exemple
Prenons un exemple simple qui consiste à incrémenter la valeur
enregistrée dans un fichier. Le code suivant, en C
, va
effectuer les opérations nécessaires :
- ouvrir le fichier,
- obtenir un mutex,
- lire la valeur précédente,
- écrire la nouvelle valeur,
- fermer le descripteur.
Pour simplifier, on considère que les fonctions de lecture et d’écriture sont fournies ailleurs et n’allons pas nous y attarder.
// Fonctions de lecture / écriture
void read(int fd, int * value) ;
void write(int fd, int value) ;
// Manipulation d'une ressource
void increment(const char * filename)
{
int fd = open(filename, O_RDWR | O_CREAT, 0666);
(fd, LOCK_EX) ;
flockint value ;
(fd, &value) ;
read(fd, value + 1) ;
write(fd) ;
close}
Le problème, dans cette version du code, c’est qu’aucune vérification n’est faite sur la validité de la ressource (le descripteur de fichier). Que se passera-t-il si le fichier n’existe pas ? Si le mutex n’est pas acquis ? En cas d’erreur, l’exécution peut quitter la fonction sans que le fichier soit fermé…
Vous pouvez me croire, c’est toujours lorsqu’on oublie une de ces vérification que le cas se produit. Avec de la chance, c’est le département qualité qui vous le dira (et vous pourrez prendre votre temps pour corriger), sinon, ce sera l’équipe support parce qu’un client pas content a trouvé un bug (et votre temps sera fonction de la patience du client).
Deuxième (meilleur) exemple
Dans cette nouvelle version, toujours en C
, nous allons
donc effectuer les vérifications nécessaires et être prudent à bien
libérer la ressource en cas de problème. Cette fois, les fonctions
retournent un code d’erreur (-1) lorsque quelque chose se passe mal.
int read(int * fd, int * value) ;
int write(int * fd, int value) ;
int increment(const char * filename)
{
int fd = open(filename, O_RDWR | O_CREAT, 0666);
if (fd == -1) {
// Error while opening the file
return -1 ;
}
if (! flock(fd, LOCK_EX)) {
// Error while trying to acquire a lock
(fd) ;
fclosereturn -1 ;
}
int value ;
if (! read(fd, &value)) {
// Error while reading the file content
(fd) ;
fclosereturn -1 ;
}
if (! write(fd, value + 1)) {
// Error while writing the new value
(fd) ;
flosereturn -1 ;
}
if (! close(fd)) {
// Error while closing the file
return -1 ;
}
return 0 ;
}
Ce qui était clair est maintenant compliqué. De moins de 10 lignes, on passe à une trentaine, la complexité cyclomatique est multipliée par 6. Et d’un problème de gestion de ressource, on arrive à celui de la maintenabilité car pour tout changement sur le code, il faudra vérifier les codes de retour, libérer la ressource et retourner un code d’erreur.
Vous pouvez en être sûr, quelqu’un finira par modifier votre code et oubliera une vérification ou une libération, ce sera le drame et des heures de débogage en perspective.
La solution : RAII
Démontrant encore une fois la supériorité de la programmation orientée objet, le patron RAII résout ces problèmes de gestion de manière très élégante.
Le RAII est un idiome de programmation qui nous vient du
C++
et dont le but est de garantir qu’une ressource acquise
est valide et que sa libération sera automatiquement effectuée
lorsqu’elle ne sera plus à portée (retour, arrêt, exception, …).
Derrière son nom barbare et ses concepts se cache en fait une technique toute simple et plutôt classique en POO :
- encapsuler la ressource dans un objet ;
- gérer l’acquisition dans le constructeur ;
- gérer la libération dans le destructeur ;
- utiliser les exception pour signifier les erreurs.
Exemple RAII en C++
En reprenant l’exemple précédent, nous allons maintenant encapsuler
la gestion du fichier dans une classe File
qui gérera
l’ouverture dans le constructeur, la fermeture dans le destructeur et
fournira des méthodes pour les autres opérations. Le tout avec des
exceptions pour que ça soit plus propre.
! Le code suivant, en C++, est un exemple d’implémentation minimale
d’une telle classe. On pourrait améliorer cette classe, ou utiliser les
classes de la STD
voir de boost
en lieu et
place mais le but est purement illustratif. Pour faire les choses
vraiment bien, on pourrait même utiliser les templates pour
implémenter le mutex sous forme de politique mais on déborderait
carrément du sujet.
class File
{
private:
int fd ;
// Make File not copyable
( const File& other );
File& operator=( const File& );
File
public:
(std::string const & filename)
File{
= open(filename.c_str(), O_RDWR | O_CREAT, 0666);
fd if (fd == -1) {
throw std::exception("Opening file failed") ;
}
}
~File()
{
(fd) ;
close// No check since one can not throw in destructor
}
void lock()
{
if (flock(fd, LOCK_EX) == -1) {
throw std::exception("Locking file failed") ;
}
}
int read() { ... }
void write(int value) { ... }
}
Grâce à cette abstraction, la fonction d’incrément devient bien plus simple puisqu’elle n’a plus besoin de gérer les différents cas d’erreur mais uniquement de définir un fichier et d’utiliser ses méthodes.
void increment(std::string const & filename)
{
(filename) ;
File f.lock() ;
f.write(f.read() + 1) ;
f}
Exemple RAII en PHP
L’implémentation en PHP
est similaire et ne diffère que
par la syntaxe, les fonctions utilisées sont presque les mêmes et le
principe ne change donc pas.
La subtile différence réside dans le fait que là où le
C++
détruit les objets d’un bloc lorsque l’exécution en sort, lePHP
utilise un ramasse miette qui détruit les objets lorsqu’ils ne sont plus référencés (cf. la documentation sur les Constructeurs et destructeurs). Ça revient au même la plupart du temps mais dans certains cas subtils, ça peut poser problème.
<?php
class File
{private $fd ;
public function __construct($filename)
{$this->fd = fopen($filename, "rw") ;
if ($this->fd === false) {
throw new Exception("Opening file failed") ;
}
}
public function __destruct()
{if (fclose($this->fd) === false) {
throw new Exception("Closing file failed") ;
}
}
public function lock()
{if (! flock($this->fd, LOCK_EX)) {
throw new Exception("Locking file failed") ;
}
}
public function read() { ... }
public function write($value) { ... }
}
function increment($filename)
{$f = new File($filename) ;
$f->lock() ;
$f->write($f->read() + 1) ;
}
Quels langages supportent le RAII ?
Cette technique fonctionne avec tous les langages supportant les
exceptions et qui garantisse que vos destructeurs soient appelés lorsque
l’exécution quitte la portée de vos objets. C’est le cas des vrais
langages objets comme le C++
et le PHP
dont on vient de voir deux exemples.
Pour les autres langages, c’est un peu plus compliqués. Ils
fournissent souvent une méthode finalize
appelée lors de la
libération mais comme ils ne peuvent vous garantir de libérer vos objets
dès que vous sortez des blocs, vous n’êtes pas plus avancés.
Certains langages ont choisi de bricoler un truc syntaxique pour combler cette lacune. Avec cette syntaxe particulière, vous pouvez définir une méthode qui sera appelée lorsque l’exécution quittera le bloc particulier correspondant.
- python: propose le concept de with statement context manager,
- Java: à partir de la version 7 propose la notion de try-with-ressource,
- C#: définis le Dispose Pattern
D’autres langages ont carrément snobé le principe et considéré qu’il
n’était pas nécessaire. Comme en node.js
ou vous devrez
définir vos macros vous-même et espérer ne pas oublier un cas
d’erreur.
Exemple (dangereux) de faux RAII
Le problème de ces méthodes, en dehors de l’incohérence syntaxique, est qu’elle n’est pas garantie par la classe qui fournit la ressource mais par celle qui l’utilise.
Transposé au contrôle d’accès d’un site web, c’est comme laisser les visiteurs déterminer eux-même s’ils peuvent ou non voir une page. Tant qu’ils jouent le jeux, tout marche très bien.
L’exemple suivant, en python
illustre le problème, la
classe File
fournit le même genre de méthode que
précédemment. La fonction goodOne
montre l’utilisation du
with
. Le problème est que rien n’oblige à utiliser cette
construction, comme le montre la fonction evilOne
.
class File:
def __init__(self, filename):
self.fd = open(filename, "rw")
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.fd.close()
def lock(self):
self.fd, LOCK_EX)
fcntl.flock(
def read(self):
# ...
def write(self, value):
# ...
def goodOne(filename):
with File(filename) as f:
+ 1)
f.write(f.read()
def evilOne(filename):
= File(filename)
f + 1) f.write(f.read()
Alors oui, on pourra me répondre qu’on pourrait déléguer l’ouverture
du descripteur de fichier dans la fonction
__enter__
et ajouter des exceptions dans les autres
méthodes si le descripteur n’est pas disponible et donc forcer
l’utilisation du with
:
class File:
def __init__(self, filename):
self.filename = filename
self.fd = None
def __enter__(self):
self.fd = open(filename, "rw")
return self
def __exit__(self, type, value, traceback):
self.check()
self.fd.close()
def check(self):
if (self.fd is None):
raise Exception("File have not been correctly created")
def lock(self):
self.check()
self.fd, LOCK_EX)
fcntl.flock(
def read(self):
self.check()
# ...
def write(self, value):
self.check()
# ...
Mais cette solution est loin d’être aussi pratique qu’un vrai RAII :
- elle complexifie la classe
File
en imposant un appel àcheck()
systématique, qu’un seul soit oublié/supprimé/… et ce sont des heures de débogage en perspective, - elle ne détecte l’oubli du
with
qu’à l’exécution, donc, à moins d’avoir une couverture du code à 100% par les tests automatiques, ça arrivera en production chez un client…
Bref, vous l’aurez compris, ça ne reste que du bricolage pour imiter les pros.
Et après ?
Si vos langages permettent l’utilisation de cette technique, je ne peux que vous conseiller d’en abuser car elle permet à la fois de simplifier les codes métiers mais surtout de les rendre plus sûrs en évitant les problèmes de gestions d’erreur et de libération.