DisposableOwnership<T>

Afin d’éviter une exclamation du type « tout ça pour ça ? », je tiens à avertir tout de suite que cet article ne présente rien de plus spectaculaire que cet objet :

struct DisposableOwnership<T> where T : IDisposable
{
	public readonly T Resource;
	public readonly bool IsOwned;

    public DisposableOwnership(T resource, bool isOwned)
    {
        Resource = resource;
        IsOwned = isOwned;
    }
}

Le principe est évident: gérer de façon adéquate la libération d’un IDisposable. Ce contrat est implémenté par tout objet qui détient des ressources à libérer en fin d’utilisation. Cependant dans beaucoup de cas, il n’est pas simple de savoir quand libérer un tel objet, en particulier lorsque c’est une ressource partagée entre différents objets.

Le cas typique est la gestion d’un IDisposable par injection de dépendance, notamment lorsque ce IDisposable est un singleton. On me répondra que dans ce cas le conteneur IoC gère lui-même la libération des singletons IDisposable. Ça peut être vrai mais voilà pourquoi ce n’est pas toujours idéal pour moi:

  • Tous les conteneurs ne gèrent pas eux-même la libération d’objets. Par exemple, SimpleInjector ne le gère pas directement (c’est implémentable).
  • Le cas échéant, la libération interviendra généralement à la libération du conteneur lui-même, c’est-à-dire à l’arrêt de l’application. On me répondra qu’il y a les scopes et ce sera juste. Seulement on n’utilise pas toujours des scopes: c’est typique de sites web où la requête HTTP est un choix évident de scope, mais c’est bien plus rare sur un service et autres applications lourdes.
  • Si la ressource n’est pas un singleton mais bien un IDisposable partagé par différents objets, le conteneur IoC sera plus démuni encore: même s’il gère la libération des IDisposable qu’il crée, il ne pourra le faire qu’à la fin de son cycle de vie (ou de son scope) alors que, peut-être, ce cycle de vie sera bien plus long que le cycle des objets concernés. J’admet que l’exemple est un peu tordu, mais il me semble qu’il se tient.
  • Enfin, comme suite du premier point, j’ai une tendance personnelle à préférer les frameworks les plus simples (je préfère SimpleInjector à Unity ou CastleWindsor, je préfère ADO.NET à Entity ou NHibernate). Less is more. Pourquoi ? Parce que le jour où l’on souhaite changer son choix, par exemple parce qu’on s’aperçoit avec le recul que le framework choisi n’est pas/plus efficace, il sera bien plus facile de le faire si le framework utilisé offre finalement le minimum des fonctionnalités de son domaine plutôt que le maximum avec toutes les options évoluées que supporteront un nombre très limité d’alternatives.

Rappel des best practices sur l’utilisation de IDisposable

Ces bonnes pratiques font régulièrement l’objet d’un rappel dans les grandes conférences sur le développement en .NET.

L’implémentation de IDisposable n’étant pas l’objet de cet article, je n’inclus pas dans les points ci-dessous les détails d’implémentation de ce contrat. Je m’intéresse ici aux objets qui manipulent un IDisposable.

  • Celui qui libère un IDisposable est celui qui le crée. Malheureusement, même la BCL ne respecte pas toujours cette règle. Par exemple, un des constructeurs de StreamReader permet de désactiver explicitement la libération du flux qui lui est transmis (libéré par défaut par StreamReader).
  • Un objet qui implémente IDisposable doit toujours être libéré (malheureusement quelques exceptions sont apparues avec TPL).
  • La méthode IDisposable.Dispose doit pouvoir être invoquée plus d’une fois sans effet de bord. Elle doit donc être idempotente.
  • Un objet IDisposable doit être libéré le plus tôt possible lorsqu’il n’est plus utilisé (et pas en attendant l’arrêt de l’application).

Si les trois derniers points sont évidents, le premier est le plus important. Et le choix des mots l’est également: si on le respecte (ce qui est pour le mieux), seul l’objet qui invoque le constructeur d’un IDisposable a le droit d’invoquer la méthode Dispose.

Le problème posé est la notion de possession de la ressource.

Si une factory est utilisée, celle-ci doit donc gérer la libération des objets qu’elle crée.

Cas du pattern Factory

Malheureusement, la majorité des exemples d’introduction au pattern factory n’abordent pas ce point et ne présentent qu’une méthode pour l’obtention de l’objet. Je suppose que la raison de cette omission est que la notion de libération est plus propre au langage utilisé qu’au pattern. Si un langage gérait la libération de manière automatique de telles ressources, c’est-à-dire si le concept du contrat IDisposable n’existait pas, nous n’aurions pas moins besoin de factory et celle-ci n’aurait effectivement pas à gérer la libération des objets qu’elle crée. En .NET, la « bonne » façon d’implémenter une factory est d’exposer deux méthodes: Create() et Release(T). Peu importe que la factory crée ou non un nouvel objet à chaque appel, ou qu’elle gère un pool d’objets (ou un seul objet, singleton), et peu importe que les ressources retournées par la factory ont un cycle de vie particulier: ces notions sont transparentes pour l’utilisateur de la factory. L’utilisateur doit juste se conformer à invoquer la première méthode pour demander un objet et la seconde pour indiquer que l’objet obtenu n’est plus utilisé.

Si l’on voulait vraiment se conformer au principe de séparation entre contrat (interface) et implémentation, il faudrait utiliser des factories presque partout (ce que personne ne fait j’espère). Ou – beaucoup plus moche – implémenter IDisposable (vide) dans tous les objets. Même si aujourd’hui, un objet que vous utilisez n’a pas besoin d’être libéré, il est rarement garanti que cela ne puisse changer dans une évolution future ou dans un objet dérivé (en dehors du partage, c’est la seconde raison pour laquelle on ne libère pas un objet qu’on ne construit pas: on ne connait pas forcément sa nature exacte).

Comment utiliser un IDisposable

Si on respecte les quelques contraintes décrites précédemment, l’utilisation d’une ressource libérable est effectivement contraignante :

  • On peut la créer nous-même (par son constructeur). Cela ne pose pas de problème particulier: on libérera la ressource à la fin du cycle de vie de l’objet utilisateur.
  • On peut dépendre d’une factory. La factory est injectée dans le constructeur de l’objet utilisateur ou elle est créée par celui-ci. La factory est responsable du cycle de vie des objets qu’elle crée pour le compte de l’utilisateur qui les demande.
  • On peut injecter la ressource libérable dans le constructeur de l’objet utilisateur. Celui-ci ne gère donc pas la libération de la ressource.

Lorsqu’on souhaite proposer, optionnellement par exemple, la libération de la ressource par l’objet utilisateur, l’approche conventionnelle est donc d’utiliser une factory. Cela fonctionne très bien, mais c’est parfois contraignant.

DisposableOwnership<T>

Cette solution est une forme de factory générique, du point de vue du code utilisateur vers lequel elle est injectée. Il ne s’agit pas de l’implémentation de la factory, mais dans son utilisation, le concept est bien là.

Concrètement, la factory est une fonction générique Func<DisposableOwnership<T>>. L’objet obtenu est une combinaison formée par la ressource IDisposable et par un booléen qui indique si l’on « possède » ou non la ressource. Le cas échéant, il nous revient le devoir de la libérer en fin d’utilisation.

Voici un exemple:

sealed class CameraActor : IDisposable
{
	private readonly Camera _camera;
	private readonly bool _cameraOwned;

	public CameraActor(Func<DisposableOwnership<Camera>> cameraFactory)
	{
		var cameraInfos = cameraFactory();
		_camera = cameraInfos.Resource;
		_cameraOwned = cameraInfos.IsOwned;
	}

	public Picture TakePicture()
	{
		// Some impressive usage of _camera...
	}

	public void Dispose()
	{
		if (_cameraOwned)
			_camera.Dispose();
	}
}

Cela revient effectivement à passer une fonction factory et un booléen au constructeur. Mais je trouve l’expression de ce contrat plus parlante.

Voici un exemple d’utilisation de notre classe fictive CameraActor:

var singleCameraFactory = new SingleCameraFactory();
var cameraFactory = new Func<DisposableOwnership<Camera>>(singleCameraFactory.GetOrCreate);

using (var actor1 = new CameraActor(cameraFactory))
using (var actor2 = new CameraActor(cameraFactory))
{
	// ...
} 
// camera disposed by actor1

Effectivement, l’intérêt ne brille pas dans ce dernier exemple très simple.

L’avantage paraît plus évident dans un graphe d’objets (cf. Composition Root). Dans le cas d’un singleton par exemple, l’idée est que le premier objet à recevoir le singleton déclenchera sa création (par la factory) et que les autres objets, descendants dans son graphe, obtiendront le singleton existant. Dans la phase de libération, c’est le premier objet qui déclenchera la libération du singleton. Tout cela fonctionne « tout seul » si l’on s’assure de libérer le graphe d’objets dans l’ordre inverse de sa composition. C’est exactement comme plusieurs using empilés (il y a concrètement plusieurs blocs en poupées russes): à la fin du bloc, c’est le dernier using qui intervient, puis le précédent, etc. jusqu’au premier.

Ensuite, comme nous avons un objet (DisposableOwnerwhip<T>) et non un objet et un booléen (T et bool séparés), il est possible de faire plusieurs choses bien pratiques: des méthodes d’extension génériques, l’enrichir d’opérateurs de conversion, etc..

Enfin, les tests unitaires sont également simplifiés. Un composant n’a plus à se soucier de savoir s’il doit ou non libérer la ressource qu’on lui transmet. Il est donc facile de tester un composant qui occupe une position basse dans le graphe (qui habituellement ne libère pas la ressource). Cela sans changement du code testé et sans avoir à faire de mock de factory.

En résumé, ce mécanisme permet au composant d’être utilisable à n’importe quel niveau du graphe, en particulier comme composant racine ou non.

Pour terminer, voici une très bonne lecture sur l’implémentation de IDisposable, d’un certain Stephen Cleary.