Si vous n'avez jamais entendu parler du pattern 'Dependency Injection' ni d'IoC (Inversion of Control) vous trouverez plus d'informations à ce sujet dans le chapitre 'Injection de dépendances'
Ail est du type setter injector du fait que Ruby se prête
à ce genre d'implémentation, un des problèmes de ce choix est que
que les dépendances d'une ressource ne peuvent être injectées
que lorsque un objet représentant une ressource est créé.
Dans Ail les dépendances d'un objet peuvent
être utilisées dès la fonction 'initialize'.
Ail est un container léger : L'utilisation
de ses fontionnalités de base n'entraine pas d'overhead significatif
et peut souvent faire économiser des ressources.
Il est souple : Son API peut être utilisée de différentes
manières sans impliquer un cadre srict de développement,
les classes implémentant les ressources n'ont pas besoin de
connaître cette API, l'utilisation de Ail permet de réduire de manière
significative le code de ces classes et des classes non conçues pour Ail
peuvent être utilisées comme ressources.
Il est extensible : C'est un outil de base sur lequel viendront se
greffer des fonctionnalités de style AOP.
La différence avec d'autres injecteurs de dépendances peut être trouvée
dans son
API simple qui permet de définir de manière
descriptive et concise les dépendances envers les resources.
Il comprend également des méthodes permettant de visualiser le graphe
de dépendance des ressources utilisées dans une application.
Le terme Ressource correspond à ce ce l'on appelle souvent
'service' ou 'composant', je préfère utiliser le terme 'ressource'
parce qu'il est plus général.
Une ressource peut être considérée comme un objet, la relation
'ressource -> un objet désiré' est d'ailleurs vraie quand cet objet existe,
le reste du temps c'est plutôt un objet permettant d'accéder à cet objet
de notre désir.
Les logiciels non triviaux comprennent un certain nombre de composants que nous voulons souvent découpler de manière à pouvoir connecter ces composants sans rien changer à leur cablage interne et ainsi pouvoir les réutiliser plus facilement dans d'autres logiciels.
L'exemple suivant est un peu fabriqué, vos logiciels sont sans doute
un peu plus complexes et surtout, je l'espère, plus utiles.
Je vous demanderai d'essayer d'extrapoler plutôt
que de vous présenter un exemple complexe.
Vous avez écrit une class 'calc' (calculateur) et vous voulez l'utiliser
dans un script que nous appelerons 'main'.
class Calc require 'mylogger.rb' def initialize @logger = Logger.new('calc.log') end def add(a, b) a.to_i + b.to_i end def divide(a, b) a.to_i / b.to_i rescue => e @logger.log("divide #{a} #{b} failed: #{e.message} #{e.class}") nil end # snip substract, multiply, fact etc. end
Cette classe n'est pas si naïve qu'on pourrait le penser, l'auteur
a bien pensé que la première personne qui l'utilisera essaira de diviser
quelque chose par zero et il a raison de logger cette erreur pour
un examen subséquent. D'ailleurs cette solution doit marcher.
La classe Calc doit cependant connaître le nom de la classe du logger
et celui du fichier 'mylogger.rb' où elle est définie,
ce qui est un couplage fort entre la classe Calc et la classe Logger.
Vous devez avoir une classe Logger définie sous ce nom dans le fichier
mylogger.rb pour faire marcher le calculateur.
Il est probable que ce calculateur puisse être intéressant pour une application qui a d'autres composants qui aiment logger les erreurs sur un même support. Peut-être que a et b que l'on doit additionner ou diviser ont été obtenus via un GUI, des sockets sécurisées ou trouvés dans une database, toutes choses qui aussi logger leurs problèmes que vous pouvez vouloir voir apparaître sur un même fichier.
Il y a une solution simple, c'est de créer une instance de la class
Logger et de la donner en paramêtre à Calc en l'instanciant.
La methode initialize de Calc serait :
def initialize(logger) @logger = logger end
Ca a l'air mieux et ça doit marcher.
C'est là qu'il va vous falloir extrapoler, les instanciations de classes
demandent parfois déjà plusieurs arguments et il va falloir en ajouter
pour chaque composant avec lesquels elles sont connectées.
Le nombre d'arguments positionels à une méthode peut ne pas être
imposé par le langage, il doit l'être par l'attention que l'on porte
au confort des programmeurs.
Le problème ne tient pas seulement
au nombre d'arguments mais aussi à l'ordre dans lequel il faut
les fournir. il faudrait parfois fournir beaucoup d'arguments à new()
et dans le bon ordre lorsque cette classe fait appel a de nombreux
composants.
Il y a aussi un autre problème :
Dans l'exemple, un objet de la class Calc fait appel a un objet de
la classe Logger, cela demande que l'objet de la classe Logger
ait été instancié avant celui de la classe Calc.
Mettons que le logger veuille numéroter ses messages, il fera naturellement
appel à l'objet de la classe Calc qu'il aura reçu en argument,
ce qui demande que l'objet de la classe Calc ait été instancié
avant celui de la classe Logger.
Aïe!
Evidemment, le logger pourrait instancier son propre objet Calc, mais dans
le cas d'objets volumineux, c'est préjudiciable à l'espace d'objets.
Il est également possible de trouver un mécanisme compliqué pour
communiquer à chaque objet la référence de l'autre. cela augmente
le couplage entre les classes de ces objets.
Des personnes ont trouvé une solution très simple de résoudre
ce problème en disant qu'il s'agit là d'une mauvaise conception ou
d'une mauvaise techique de programmation et se passent de cette
possibilité. Il n'ont pas tort, ces dépendances croisées impliquent
dans le développement d'un composant une certaine connaissance
du fonctionnement d'autres composants
et elles sont assez généralement à éviter.
Le graphe de dépendances peut être représenté par ce schéma :
A ---> B signifie : A est requis par B
Le problème survient lorsque le graphe comporte des cycles.
Ail ne propose pas de limiter les possibilités de conception ni de
pallier ses éventuels défauts mais d'être un outil pour
implémenter un design, il aide donc aussi à implémenter de manière simple
ce genre d'architecture.
il le fait en allègeant le code des classes qu'il décharge de la gestion
des dépendences.
Plus de raisons de trouver une utilité à un injecteur de dépendances apparaîtront dans les chapitres suivant de cette introduction.
Ail résoud ces problèmes en extrayant la logique des dépendances
entre objets des définitions des classes de ces objets et en offrant
une possibilité de décrire ces dépendances.
Le graphe de dépendance peut être cyclique ou non, connexe ou non,
le code des classes peut être écrit en parfaite méconnaissance du nom des
classes dont elles dépendent, elles ont simplement à indiquer le symbole
qui leur servira de référence à cette resource.
Dans l'exemple suivant, nous avons ajouté une nouvelle ressource
correspondant à l'objet IO permettant d'accéder au fichier log.
Voici à quoi pourrait ressembler une application utilisant le calculateur.
et le logger.
Mais il existe plusieurs manières d'utiliser Ail comme 'framework', dans
le chapitre suivant vous trouverez d'autres façons sans doute plus
habiles de l'utiliser..
Son graphe de dépendances peut être représenté ainsi :
L'application est decoupée en plusieurs sources. (Vous pouvez trouver ces sources dans la directory ./samples de la distribution).
calc.rb# file calc.rb class Calc attr_writer :logger def add(a, b) a.to_i + b.to_i end def divide(a, b) a.to_i / b.to_i rescue => e @logger.log("divide #{a} #{b} failed: #{e.message} #{e.class}") nil end # snip substract, multiply, fact etc. end
Cela permet à Ail de pouvoir renseigner la variable d'instance @logger
sans avoir recours à un mauvais hack.
On peut remarquer que la classe se passe maintenant de methode
initialize.
# file mylogger.rb class Logger attr_writer :file, :calc def initialize @num = 0 if block_given? yield(self) log "Logger initialized." end end def log(str) @num = @calc.add(@num, 1) @file.puts "#{@num} : #{str}" end end
# file app.rb class App attr_writer :calc def run puts "1 + 1 = #{@calc.add(1, 1)}" puts "1 / 0 = #{@calc.divide(1, 0)}" end end
# file inject.rb module Inject require 'rubrix/ail.rb' include Ail require 'app.rb' require 'calc.rb' require 'mylogger.rb' def Inject.init resources = Resources.new( :resources, :app ) { |resources| resources.register(:calc, Singleton|Defer, :logger ) { Calc.new() } resources.register(:logger, Defaults, [:logFile, :file], :calc ) { |p| Logger.new(&p) } resources.register(:logFile, NoDefer ) { File.new('mylog.log', 'a') } resources.register(:app, Defaults, :calc ) { App.new() } } # end of Resources.new block end end
Il débute par des 'require', comme il concentre les dépendances entre les resources, cela peut sembler un bon endroit pour les mettre.
La methode init est chargée de créer l'injecteur et d'enregistrer les différentes ressources. Cette fonction retourne une instance de la classe Resource que l'on peut considérer comme l'injecteur de dépendances.
resources = Resources.new( :resources, :app ) { |resources| ... } # end of Resources.new block
resources.register(:calc, Singleton|Defer, :logger ) { Calc.new() }
resources.register(:logger, Defaults, [:logFile, :file], :calc ) { |p| Logger.new(&p) }
resources.register(:logFile, NoDefer ) { File.new('mylog.log', 'a') }
# file main.rb require ARGV[0] # resources = Inject.init root = resources.open root.run # or #Inject.init.open.run
Il y a plusieurs façons d'utiliser Ail dans une application. Dans l'exemple du chapitre précédant, main.rb était superflu (oui le reste aussi : ruby -e 'puts(1 + 1)' est bien plus rapide).
Une Variante pourrait être celle-ci :
# file inject2.rb require 'rubrix/ail.rb' include Ail require 'app.rb' require 'calc.rb' require 'mylogger.rb' resources = Resources.new( :resources, :app ) { |resources| resources.register(:calc, Singleton|Defer, :logger ) { Calc.new() } resources.register(:logger, Defaults, [:logFile, :file], :calc ) { |p| Logger.new(&p) } resources.register(:logFile, NoDefer ) { File.new('mylog.log', 'a') } resources.register(:app, Defaults, :calc ) { App.new } } # end of Resources.new block resources.open.run
ruby inject2.rb
Une possibilité plus souple serait de paramétrer l'injecteur de manière à appeler le script désiré.
#!/usr/bin/ruby # file inject3.rb module Inject require 'rubrix/ail.rb' include Ail require 'app.rb' require 'calc.rb' require 'mylogger.rb' def Inject.init resources = Resources.new( :resources, :app ) { |resources| resources.register(:calc, Singleton|Defer, :logger ) { Calc.new() } resources.register(:logger, Defaults, [:logFile, :file], :calc ) { |p| Logger.new(&p) } resources.register(:logFile, NoDefer ) { File.new('mylog.log', 'a') } resources.register(:app, Defaults, :calc ) { App.new() } } # end of Resources.new block end end require ARGV.shift
# file main3.rb Inject.init.open.run
./inject3.rb main3.rb
# file graphO3.rb require 'rubrix/ailgraph.rb' include AilGraph resources = Inject.init graph = Graph.new( resources ) graph.make_graph('nodes', 'edges', 'mkgraph' )
C'est du moins celle que je préfère.
Elle peut ajouter une flexibilité supplémentaire en inversant une fois
de plus le contrôle.
L'injecteur de dépendance ne gère plus que les dépendances entre les
objets, la nature exacte des classes de ces objets lui est retiré,
cela se fait en plaçant les 'require' dans le script appelé.
#!/usr/bin/ruby # file inject4.rb module Inject require 'rubrix/ail.rb' include Ail def Inject.init resources = Resources.new( :resources, :app ) { |resources| resources.register(:calc, Singleton|Defer, :logger ) { Calc.new() } resources.register(:logger, Defaults, [:logFile, :file], :calc ) { |p| Logger.new(&p) } resources.register(:logFile, NoDefer ) { File.new('mylog.log', 'a') } resources.register(:app, Defaults, :calc ) { App.new() } } # end of Resources.new block end end require ARGV.shift
# file main4.rb require 'app.rb' require 'calc.rb' require 'mylogger.rb' Inject.init.open.run
# file graphO4.rb require 'rubrix/ailgraph.rb' include AilGraph resources = Inject.init graph = Graph.new( resources ) graph.make_graph('nodes', 'edges', 'mkgraph' )
Nous approchons d'un parfait partage de la responsabilités et d'un
découplage maximal des resources.
Les classes permettant d'instancier les objets définissent le boulot que
ces objets font sans avoir besoin de connaître la nature des resources
auxquelles elles font appel.
L'injecteur de dépendances gère les dépendances entre des objets
qu'il ne connaît que de nom.
Le script main choisit les resources à utiliser, c'est dans ce script qui
ne peut faire que quelques lignes que se concentre les décisions
importantes dans la confection d'un projet.
Dans une phase de test, c'est dans ce script que vous pouvez changer
"require 'stub_logger.rb'" par "require 'logger.rb'" quand le logger
semble opérationnel, cela sans avoir à changer les dépendances entre
les resources ni les ressources elles-mêmes.
En changeant une ligne
de code vous pouvez revenir à un état précédant parce que le logger fait
inexpliquablement planter votre classe, cela sans affecter en rien les
développeurs à qui il est utile et qui utilisent 'mock_calc.rb' à la
place de 'calc.rb' que vous écrivez.
Pour les applications formées d'un grand nombre de composants,
les 'requires' peuvent être aussi être mis dans un source spécial.
Il y a encore beaucoup de moyens d'utiliser Ail pour construire
un framework qui vous convienne.
Pour le moment Ail ne fait guère plus de 400 lignes de Ruby, il
n'offre que les fonctionalités de base d'un injecteur de dépendances
ainsi que des possibilités de visualisation de graphes de dépendances.
Vous pourrez trouver des
références à d'autres injecteurs de dépendances sur cette page
du site de Jim Weirich.
Needle de Jamis Buck a l'avantage
de proposer beaucoup de fonctionalités et d'être bien documenté.
The Resources class is defined in this module.
Some methods can only be used in the block given to Resources.new,
some others cannot be used in this block, this doesn't mean they
must not appear in an instantiation block (defined inside
thé Resource.new block)
but that they must not be called before the initialize lethod of
the Resources object is terminated.
Resources.new([name,[root]] { |resources| block } -> resources
register(name, [options, [*dependencies]]) { |pr| block } -> name
instantiate([name])
open([name])
open_res([name])
each_connected_to(name) { |sym, dep| block } -> resources
each_depend { |sym, dep| block } -> resources
each_key { |sym| block } -> resources
empty? -> true or false
has_key?(sym) -> true or false
instance(sym) -> an object
instantiated?(sym) -> true or false
keys -> an array
name -> a symbol
root -> a symbol
size -> a Fixnum
The Graph class of his module provides methods which produce files
in the format expected by
Graphdot01.rb.
The graphdot methods produce files in the dot format which may be
used by to produce
visual graphs.
Graphs which appear in this documentation are made using these methods.
If you use a Graph class object as an Ail resource, you can inject it a Resource object.
new([resources = nil])
make_graph(nodefn, edgefn, scriptfn = nil) -> nil
make_connected(name ,nodefn, edgefn, scriptfn = nil) -> nil
# file graphO3.rb require 'rubrix/ailgraph.rb' include AilGraph resources = Inject.init graph = Graph.new( resources ) graph.make_graph('nodes', 'edges', 'mkgraph' )
./graph03.rb inject.rb logger ./mkgraph > logger.dot dot -Tpng -o logger.png logger.dot
Bien que le concept d'injection de dépendances ne soit pas compliqué, il n'est pas très facile à expliquer et une métaphore pourra m'y aider.
Beaucoup de plats cuisinés nécessitent de l'ail dans leur préparation
mais on n'a jamais vu de plats s'assaisonner d'ail eux même, ni de
gousse d'ail s'émincer elle-même pour venir garnir un plat. Cela
nécessite une tierce personne qui connait les ingrédients nécessaire
à la recette et le moment de les utiliser.
Le plat et l'ail ainsi que les éventuelles préparations intermédiaires
(aïoli) peuvent être vus comme des ressources correspondant à des
composants logiciels dont l'injecteur de dépendances serait le
cuisinier.
Certains cuisiniers piquent d'ail des rotis, si vous en croisez un,
vous pouvez lui dire que c'est un setter injector.
L'injecteur de dépendances est une tierce partie qui concentre la gestion
des dépendances en en déchargeant les autres composants. Bien entendu,
ces composants doivent savoir quelles ressources ils utilisent lorsqu'ils
les utilisent mais sans connaître leur nature exacte, la ressource utilisée
doit simplement répondre aux messages qui lui sont envoyés.
Les classes n'ont pas à s'occuper du cablage entre les ressources,
(il a été dit que cela prend 30% du code), de leur recherche et de leur
création au moment opportun.
Bien sûr, à un certain moment il faut indiquer qui a besoin de qui, mais ce cablage peut être fait en dehors des composants, ce qui a un certain nombre d'intérêts.
Le terme 'Dependency Injection' a, je crois, été employé la première fois par Martin Fowler, son article en anglais est une base difficilement contournable. Les exemples sont en Java et la 'Setter Injection' y est plus lourde qu'en Ruby.
Une très bonne introduction sur la 'Dependency Injection', en anglais avec des exemples en Ruby. sur le
site de Jim Weirich.
Sur le même site ces transparents avec des exemples en Java et Ruby
vous ferons comprendre l'interêt de ce 'pattern'.
Les autres articles du site sont également très intéressants.
Injection de dépendances, article en français avec des exemples en Java.
Ail is copyrighted : Copyright (c) 2006 Patrick Davalan.
All rights reserved.
Ail is free software,
it may be used, modified and distributed under the same terms as Ruby.
See the file COPYING in the Ail distribution.
Ail is provided "as is" without any warranty, including, but not limited to, any warranty you might dream of.
Download the latest version in the Download page
Unzip the tarball with tar xzvf ail-version.tar.gz, it ceates the ail-version directory, then read the README file in that directory.
In order to not pollute your Ruby libraries directory , Ail as well as
some other projects of my own installs itself into the subdirectory
'rubrix', before using it, you should write somewhere in your code :
require 'rubrix/ail.rb'
and if you want to visualize dependencies graphs :
require 'rubrix/ailgraph.rb'
Send bug reports to : almazz at wanadoo dot fr
Ail, its installation procedures and test suite are designed to work
on any platform where Ruby works, but they have only been
tested on a Debian GNU/Linux system.
Ail had been developped using Ruby 1.8, but it may work on
previous versions of Ruby.
As intantiation of a resource may be triggered by a message sent to it,
One have to know that the initialize method of the object may be
called before the message is handled by the newly created resource.
This may be a problem when the dependencies graph is cyclic.
The a method of a class A object may trigger the instantiation of
class B object by calling it's b method. If the initialize method of the
class B object retrieves it's dependencies using 'yield(self)' and
calls the a method of the A class object, this may be unexpected by
the A class.
This is the simplest case, there may be indirections.
The best solution is to avoid cyclic dependencies, but if you need it,
avoid to retrieve dependencies in resources initialize methods in a cycle,
but if you need it be careful.
La conception de Ail est basée sur
DIM de Jim Weirich, Le
matzdi_setter.rb de Yukihiro 'Matz' Matzumoto.
m'a aussi aidé.
La documentation des logiciels Copland et Needle de Jamis Buck m'a aussi montré tout l'avantage que l'on pouvait tirer de l'injection de dépendances et inspirera probablement les développements ulterieurs de Ail.
Mes remerciements vont aussi à Ruby lui-même pour avoir excité mon imagination.