La ZCA est ensemble de bibliothèque implémentant une série de design pattern :
- interface,
- registry,
- factory,
- adapter.
ZCA signifie Zope Component Architecture. La ZCA a été créé pour zope
mais est utilisable sans difficulté en dehors de zope. Il ne faut surtout pas
se laisser effrayer par le mote zope : la ZCA n'est pas zope. Il n'y a pas
besoin de comprendre zope pour s'en servir.
Les design patterns
Les design patterns ou patron de conceptions sont une série de concepts de
programation objet établie par le gang of four. Ces patrons sont des
organisations de code ou de classes récurrentes dans la programtion objet.
Le gang of four en établit 23. Je ne vais pas les détailler ici mais
seulement les quatre cités plus haut. Ces patrons ne sont pas spécifiques à
un langage mais sont implémentables dans la plupart des langages objets.
Installation
Seuls deux packages sont nécéssaires : zope.interface et zope.component.
Via pip :
$ pip install zope.interface zope.component
Buildout :
[buildout]
parts =
ZCA
[ZCA]
recipe = zc.recipe.egg
eggs =
zope.interface
zope.component
Interface
Utilisée seule, l'interface est le pattern le moins intéressant en python.
Les interfaces sont très (voire trop) utilisées en Java.
Une interface est une classe abstraite (classe non instansciable ni héritable
(sauf par une autre interface)) qui est un contrat pour les classes
l'implémentant.
from zope.interface import Interface
from zope.interface import Attribute
class IDuck(Interface):
"""
Duck description.
"""
name = Attribute("name of the duck")
def kwack():
"""
Sound of a duck
"""
Ce code définit une classe IDuck (Une convention assez fréquente quelque
soit le langage est de préfixer par un I.) qui est une interface. On définit
ici qu'un canard doit avoir un nom et caquete.
Définisons deux especes de canards : cygne et colvert.
from zope.interface import Interface
from zope.interface import Attribute
from zope.interface import implementer
class IDuck(Interface):
"""
Duck description.
"""
name = Attribute("name of the duck")
def kwack():
"""
Sound of a duck.
"""
@implementer(IDuck)
class Swan(object):
"""
Swan implementation of IDuck.
"""
def __init__(self, name):
self.name = name
def kwack(self):
print("swan kwack")
@implementer(IDuck)
class Mallard(object):
"""
Mallard implementation of IDuck.
"""
def __init__(self, name):
self.name = name
def kwack(self):
print("Mallard kwack")
s = Swan('foo')
m = Mallard('baz')
for duck in [s, m]:
print(duck.name)
duck.kwack()
A l'usage cela donne :
$ bin/python interface.py
foo
swan kwack
baz
Mallard kwack
Sauf que grace au duck typing de python le code suivant produit exactement le
même résultat.
class Swan(object):
"""
Swan implementation of Duck.
"""
def __init__(self, name):
self.name = name
def kwack(self):
print("swan kwack")
class Mallard(object):
"""
Mallard implementation of Duck.
"""
def __init__(self, name):
self.name = name
def kwack(self):
print("Mallard kwack")
s = Swan('foo')
m = Mallard('baz')
for duck in [s, m]:
print(duck.name)
duck.kwack()
So what ? Comme dit plus haut ça ne sert pas à grand chose en python...
Un petit rappel sur le duck typing : « Si ça fait coin, c'est un canard. ».
En python, il n'y a pas de typage statique. Les types ne comptent pas (ou très
peu), Les méthodes et les attributs sont plus importants.
Avec la ZCA et contrairement à java, les interfaces sont un contrat entre
developppeurs. Le premier s'engage à utiliser les méthodes d'une interface et
le second à écrire une classe qui implémente cette interface. En java, c'est
un contrat entre le code et le compilateur : si le contrat n'est pas respecté,
le code ne compile pas.
Avec la ZCA, il n'y a pas de contrainte :
@implementer(IDuck)
class Cat(object):
"""
Dude, this is a non sens !!!
"""
def __init__(self, name):
self.name = name
s = Swan('foo')
m = Mallard('baz')
c = Cat('bar')
for duck in [s, m, c]:
print(duck.name)
duck.kwack()
et donc à l'exécution :
$ bin/python interface.py
foo
swan kwack
baz
Mallard kwack
bar
Traceback (most recent call last):
File "bin/python", line 78, in <module>
execfile(__file__)
File "/tmp/interface.py", line 58, in <module>
duck.kwack()
AttributeError: 'Cat' object has no attribute 'kwack'
On peut implémenter une interface sans la respecter. Ce n'est pas très pertinent
mais possible. L'interface ne sert que de contrat et de documentation.
Utilisée seule l'interface ZCA n'apporte rien à part ajouter un tas de
lignes inutiles.
Les registres
Les registres (voire plutôt le registre) ne sont pas un pattern du GoF. Un
registre est un singleton (lui c'est un design pattern) qui mémorise l'instance
d'autres classes.
class Registry(object):
_instance = None
def __new__(cls):
if cls._instance is None:
cls._intance = Registry()
return cls._instance
def __init__(self):
self._memory = {}
def save(self, name, instance):
self._memory[name] = instance
def get(self, name):
return self._memory[name]
reg1 = Registry()
reg2 = Registry()
print(reg2 is reg1)
Nous avons ici une classe qui n'est instanciée qu'une seule fois et qui nous
permet de sauver des objets puis de les récupérer. Ce mécanisme est intéressant
si l'on utilise des objets couteux à intancier et très rapide à utiliser. Dans
mon boulot, je travaille beaucoup sur des fichiers xmls que je dois valider à
l'aide d'une grammaire (xsd le plus souvant). La construction de l'outil de
validation prends un temps certain.
from glob import iglob
from lxml import etree
# ces 2 lignes coutent
xsdRoot = etree.parse(pathToXsdFile)
xsd = etree.XMLSchema(xsdRoot)
for xml in iglob("*.xml"):
root = etree.parse(xml)
print(xsd.validate(root))
Ce code représente un cas idéal où tous les xml sont au même endroit et en
même temps. Dans une version plus proche de la réalité, les xmls sont servis
par un webservice qui doit répondre le plus vite possibe.
Dans un __init__.py, on aurait le code suivant :
reg = Registry()
reg.save('xsd', xsd) # le xsd de notre bloc de code précédent.
et dans le fichier views.py :
# récupération du xml à valider plus dans un code imaginaire avant
reg = Registry()
xsd = reg.get('xsd')
return xsd.validate(xml)
Une autre solution à ce problème aurait été de faire un singleton avec cette
xsd mais si on a besoin d'une autre classe de ce genre il faut refaire un
singleton. De plus, on pourrait avoir plusieurs types de grammaires avec chacun
leur xsd associée et on ne peut plus faire un singleton dans ce cas la.
reg = Registry()
reg.save('xsd1', xsd1)
reg.save('xsd2', xsd2)
reg.save('xsd3', xsd3)
reg = Registry()
xsd = reg.get(request.POST['xsd'])
return xsd.validate(xml)
Revenons à la ZCA, celle-ci offre un registry[2] : le gsm. gsm signifie
Global Site Manager. Le gsm peut mémoriser des objets selon une interface.
Reprenons notre exemple de xsd (inspiré de mon job).
from zope.interface import Interface
from zope.interface import implementer
from zope.component import getGlobalSiteManager
# Créons une classe IValid
class IValid(Interface):
def validate(document):
"""
Validate a document.
"""
# puis notre classe de validation
@implementer(IValiad)
class Xsd(object):
def __init__(self, xsdfile):
xsdRoot = etree.parse(pathToXsdFile)
self._xsd = etree.XMLSchema(xsdRoot)
def validate(document):
"""
Validate a document.
"""
return self._xsd.validate(document)
xsd = Xsd('/path/to/gramar.xsd')
gsm = getGlobalSiteManager()
gsm.registerUtility(xsd, IValid)
Et dans un lointain fichier :
from zope.component import getUtility
xsd = getUtility(IDuck)
xsd.validate(someDocument)
Avec ce mécanisme, on ne peut récupérer qu'un seule instance de Xsd ce qui
n'apporte rien par rapport à un singleton. L'intérêt est d'avoir des instances
différentes pour chacune des grammaires que l'on manipule.
gsm = getGlobalSiteManager()
xsd1 = Xsd('/path/to/gramar1.xsd')
gsm.registerUtility(xsd1, IValid, 'gramar1')
xsd2 = Xsd('/path/to/gramar2.xsd')
gsm.registerUtility(xsd2, IValid, 'gramar2')
xsd3 = Xsd('/path/to/gramar3.xsd')
gsm.registerUtility(xsd3, IValid, 'gramar3')
Si l'on reprend l'exemple plus haut :
from zope.component import getUtility
xsd = getUtility(IValid, request.POST['grammar'])
return xsd.validate(document)
Le registry nous permet de sauver des instances pour les utiliser plus tard.
C'est un outil extrèmement pratique mais il faut le réserver aux objets dont la
construction est coûteuse. Un usage abusif du registry va transformer votre
code en code spagetti. Un inconveniant majeur de cet outil est qu'on perd la
notion d'import de nos parties métiers.
Les factories
Une factory est une classe[1] qui instancie une autre classe.
class Duck(object):
"""
This is a duck.
"""
def __init__(self, name):
self.name = name
class FactoryDuck(object):
"""
This is an egg ?
"""
def __call__(self, name):
"""
Lets make a duck.
"""
return Duck(name)
fd = FactoryDuck()
duck = fd('foo')
print(type(duck))
print(duck.name)
$ bin/python /tmp/factory.py
<class '__main__.Duck'>
foo
Une factory sert à déporter l'instanciation d'un objet à une autre classe pour
plusieurs raisons comme :
- on appelle fréquement une classe qui a tendance à changer de chemin au fil
de ses versions,
- lors de l'écriture du code, on ignore la classe à instancier (ex: la
connexion à une base donnée).
Explorons le premier cas. C'est un pis-aller à un mauvais problème. Notre code
utilise un code tier qui à tendance à changer d'arborescence. Ce code est fourni
par le pypi, par un fournisseur avec qui il faut utiliser un protocole proprio
dont il faut utilise le code fourni. Bref un cas ou on est contraint d'utiliser
un code mouvant.
Dans la première version, la classe Connection est dans fournisseur.class,
dans la seconde, le fournisseur a lu un bouquin sur java et donc la classe se
trouve dans com.fournisseur.class et dans une troisième version
fournisseur.classes.connector etc, etc. L'effet de bord de cette promenade
est qu'il faut repasser dans tous nos fichiers appelant ce code pour corriger.
Une solution est d'utiliser une factory[3] pour palier en partie à ce problème.
# cet import est soumis au bon vouloir du fournisseur
from fournisseur.classes.connector import Connection
class FactoryConnection(object):
def __call__(self, *args, **kwargs):
return Connection(args, kwargs)
Puis dans notre code, on utilise partout cette factory pour ne pas réécrire en
permanence nos imports mais seulement un dans la definition de la factory.
instance = FactoryConnection(args1, args2, kwargs1='value1')
On aurait également pu faire un héritage pour résoudre ce problème. C'est une
solution à un mauvais problème. Je mentionne cet usage qui n'est pas très
courant mais il peut arriver qu'on le croise.
L'usage plus courant (et plus sain) est le cas où l'on ignore la classe à
instancier lors de l'écriture du programe. L'exemple typique est la connexion
à la base de données dans un ORM comme SQLAlchemy.
Lorsque le programme est écrit on ignore (volontairement parfois) le SGBD qui
sera utilisé lors du déploiement. Lors du lancement, le connecteur au SGBD
utilise une factory pour savoir quelle classe de connexion à la base de données
utiliser.
Imaginons qu'on doive décompresser des archives zip ou tar selon le type de
fichier.
import os.path
from zipfile import ZipFile
from tarfile import TarFile
class FactoryArchive(object):
"""
Build Archive.
"""
_choices = {'zip': ZipFile,
'tar': TarFile}
def __call__(self, filename):
"""
Return the correct class.
"""
_, ext = os.path.splitext(os.path.basename(filename))
return self._choices[ext.lower()](filename, mode='r')
L'usage du code serait le suivant :
# filename is something like /foo/bar/baz.tar or /foo.bar.baz.zip
factory = FactoryArchive()
archive = factory(filename)
archive.extractall('/tmp')
À l'écriture du code, on ignore si TarFile ou ZipFile seront appelés [4]
La ZCA contient déja une classe Factory qui implémente une classe IFactory.
# factories for build ZipFile or TarFile.
from zipfile import ZipFile
from tarfile import TarFile
from zope.component.factory import Factory
factoryZip = Factory(ZipFile, 'zip')
factoryTar = Factory(TarFile, 'tar')
Et à l'usage :
import os.path
choices = {'zip': factoryZip,
'tar': factoryTar}
_, ext = os.path.splitext(os.path.basename(filename))
factory = choices[ext.lower()]
archive = factory(filename)
archive.extractall('/tmp')
Jusque là cela ne réduit pas beaucoup le code ni le simplifie vraiment.
Couplé au gsm vu plus haut les choses commencent à devenir plus
intéressantes.
# factories for build ZipFile or TarFile.
from zipfile import ZipFile
from tarfile import TarFile
from zope.component.interfaces import IFactory
from zope.component import getGlobalSiteManager
from zope.component.factory import Factory
factoryZip = Factory(ZipFile, 'zip')
factoryTar = Factory(TarFile, 'tar')
gsm = getGlobalSiteManager()
gsm.registerUtility(factoryZip, IFactory, 'zip')
gsm.registerUtility(factoryTar, IFactory, 'tar')
Ce qui devient à l'usage :
import os.path
from zope.component import getUtility
from zope.component.interfaces import IFactory
_, ext = os.path.splitext(os.path.basename(filename))
factory = getUtility(IFactory, ext)
archive = factory(filename)
archive.extractall('/tmp')
On ne réduit pas le nombre de lignes de façon significative mais on réduit
l'écriture de code complexe qu'on écrit soit même pour le déléguer à la ZCA.[5]
La classe Factory implémente l'interface IFactory. On peut écrire soit même
des classes factory l'implémentant.
from zipfile import ZipFile
from tarfile import TarFile
from zope.component.interfaces import IFactory
from zope.component import getGlobalSiteManager
from zope.interface import implementer
@implementer(IFactory)
class ArchiveFactory(object)
_choices = {'zip': ZipFile,
'tar': TarFile}
def __call__(self, filename):
"""
Return the correct class.
"""
_, ext = os.path.splitext(os.path.basename(filename))
return self._choices[ext.lower()](filename, mode='r')
gsm = getGlobalSiteManager()
gsm.registerUtility(ArchiveFactory(), IFactory, 'archive')
Ce qui devient à l'usage :
from zope.component import getUtility
from zope.component.interfaces import IFactory
factory = getUtility(IFactory, 'archive')
archive = factory(filename)
archive.extractall('/tmp')
Les adaptateurs
Les adapters sont des classes qui présentent une classe implémentant une
interface en présentant une autre.
Une classe Foo ne sait manipuler que des IBar mais on n'a sous la main que
BazA et BazC qui implémentent tout deux IBaz.
from zope.interface import Interface
from zope.interface import implementer
from zope.interface import adapater
class IBar(Interface):
"""
Interface IBar.
"""
def bar():
"""
Method bar.
"""
class IBaz(Interface):
"""
Interface IBaz
"""
def foo1(arg1):
"""
First action.
"""
def foo2():
"""
And the second one.
"""
@impleteter(IFoo)
class Baz1(object):
def foo1(self, arg1):
self.arg1 = arg1
def foo2(self):
print('Baz1', self.arg1)
@impleteter(IFoo)
class Baz2(object):
def foo1(self, arg1):
self.arg1 = arg1
def foo2(self):
print('Baz2', self.arg1)
@adapater(IFoo)
@impleteter(IBar)
class AdapaterFoo(object):
"""
"""
def __init__(self, baz):
self._baz = baz
def baz(self):
self._baz.foo1("default args")
self._baz.foo2()
#registering the adapater
gsm = getGlobalSiteManager()
gsm.registerAdapter(AdapaterFoo, name='adapt foo')
Nous avons donc créé deux interfaces, classes et un adapteur.
Si on veut ce servir de ce code cela donnera ceci :
from zope.component import getAdapter
# baz est un objet Baz1 ou Baz2
# barUSer est un objet utilisant des IBar
adapte = getAdapter(baz, IBar)
# adapte est un AdapterFoo
barUser.set(adapte)
#[...]
Cet exemple est quelque peu abstrait et un peu long. Le point intéressant est
que lorsque qu'une nouvelle classe implémentant IFoo sera créée il n'y aura
aucun code supplémentaire à écrire pour s'en servir. Sans les adaptateurs, il
aurait fallu écrire des classes spécifiques pour Baz1, Baz2 et toute autre
nouvelle classe implémentant IFoo.
Mélangeons tout ça
Jusque ici tout mes exemples étaient assez abstraits, je vous en propose un
plus constistant en mélangeant tout ce qu'on a vu précédement.
L'exemple est un programme qui va acquérir des archives, les décompresser,
les modifier puis les stocker via différents services. Notre programme prendra
ses instructions d'un fichier texte passé en argument.
Nous allons supposer que nos différentes parties viennent de différents
composants déja écrits par des tiers ou pour d'autres projets.
La gestion des ftp et http se fera par une interface IRemote avec deux
implémentations : http et ftp (ainsi qu'un abstract pour factoriser).
L'API simpliste spécifiée par l'interface IRemote nous permet de télécharger
et d'uploader un fichier.
class IRemote(Interface):
"""
Interface for handle remote file.
"""
def get(filename):
"""
Get a file.
"""
def put(filename):
"""
Upload a file.
"""
class AbstractRemote(object):
def __init__(self, url):
"""
Take an url as argument.
"""
self._url = url
@implementer(IRemote)
class FTPRemote(AbstractRemote):
def __init__(self, url):
super(FTPRemote, self).__init__(url)
self._ftp = FTP(self._url)
def get(self, filename):
self._ftp.login()
self._ftp.retrbinary('RETR %s' % os.path.basename(filename),
open(filename, 'wb').write)
self._ftp.close()
def put(self, filename):
self._ftp.login()
self._ftp.storbinary('STOR %s' % os.path.basename(filename), 4,
open(filename, 'rb').read)
self._ftp.close()
@implementer(IRemote)
class HTTPRemote(object):
def get(self, filename):
with open(filename, 'wb') as tmp:
req = requests.get(self._url + '/' + os.path.basename(filename))
tmp.write(req.read())
def put(self, filename):
with open(filename, 'rb') as tmp:
files = {'data': tmp}
requests.post(self._url + '/' + os.path.basename(filename), files)
Le composant suivant est la gestion des archives. Comme pour les accès distants,
on adope une interface simpliste IArchive avec deux mréethodes : extractAll et
compressAll.
class IArchive(Interface):
"""
Inferface for handle compressed archive.
"""
def extractAll(pathTo):
"""
Exctract all file in dir pathTo.
"""
def compressAll(pathFrom):
"""
Compress all files from dir pathFrom.
"""
class AbstractArchive(object):
def __init__(self, filename):
self._filename = filename
@implementer(IArchive)
class ZipArchive(AbstractArchive):
def extractAll(self, pathTo):
with ZipFile(self._filename, 'r') as tmp:
tmp.extractall(pathTo)
def compressAll(self, pathFrom):
with ZipFile(self._filename, 'w') as tmp:
for todo in os.walk(pathFrom):
tmp.write(todo, os.path.basename(todo))
@implementer(IArchive)
class TarArchive(AbstractArchive):
def extractAll(self, pathTo):
with TarFile(self._filename, 'r') as tmp:
tmp.extractall(pathTo)
def compressAll(self, pathFrom):
with TarFile(self._filename, 'w') as tmp:
for todo in os.walk(pathFrom):
tmp.write(todo, os.path.basename(todo))
Et pour finir, puisées dans notre mirobolante bibliothèque de composants, les
modifications à apporter aux contenus des fichiers.
class ITransform(Interface):
def open(filename):
def close():
def transform(newName):
filename = Attribute("filename")
class AbstractTransform(object):
def open(self, filename):
self._filename = filename
self._content = open(self._filename, 'rb')
def close(self):
self._content.close()
@property
def filename(self):
return filename
@implementer(ITransform)
class LowerTransform(AbstractTransform):
def transform(self, newName):
with open(newName, 'wb') as tmp:
for line in self._content:
tmp.write(line.lower())
@implementer(ITransform)
class UpperTransform(AbstractTransform):
def transform(self, newName):
with open(newName, 'wb') as tmp:
for line in self._content:
tmp.write(line.upper())
@implementer(ITransform)
class Rot13Transform(AbstractTransform):
def transform(self, newName):
import string
rot13 = string.maketrans( "ABCDEFGHIJKLMabcdefghijklmNOPQRSTUVWXYZnopqrstuvwxyz",
"NOPQRSTUVWXYZnopqrstuvwxyzABCDEFGHIJKLMabcdefghijklm")
with open(newName, 'wb') as tmp:
for line in self._content:
tmp.write(string.translate(line, rot13))
@implementer(ITransform)
class Base64Transform(AbstractTransform):
def transform(self, newName):
import base64
with open(newName, 'wb') as tmp:
for line in self._content:
tmp.write(base64.b64encode(line))
Tous ces composants sont pré-écrits pour des projets précédents ou sont
écrits pour être utilisés dans d'autre projets dont les spécificités sont
différentes du projet actuel. Il nous faudra enregistrer les factories de
ces classes dans le gsm pour les utiliser plus tard.
Mais d'abord il nous faut désigner nos actions à l'aide d'une interface
IAction.
class IAction(Interface):
"""
Interface of action classes
"""
def init(filename):
"""
"""
def process(args):
"""
"""
Une fois IAction décrite, il suffit d'écrire la série d'adapters et de les
enregistrer :
# Now we need adapters
gsm = getGlobalSiteManager()
@implementer(IAction)
@adapter(IRemote)
class AdapterGet(object):
def __init__(self, adapte):
self._adapte = adapte
def process(self, arg):
self._adapte.get(arg)
@implementer(IAction)
@adapter(IRemote)
class AdapterPut(object):
def __init__(self, adapte):
self._adapte = adapte
def process(self, arg):
self._adapte.put(arg)
gsm.registerAdapter(AdapterGet, 'get')
gsm.registerAdapter(AdapterPut, 'put')
@implementer(IAction)
@adapter(IArchive)
class AdapterExtract(object):
def __init__(self, adapte):
self._adapte = adapte
def process(self, arg):
self._adapte.extractAll(arg)
@implementer(IAction)
@adapter(IArchive)
class AdapterCompress(object):
def __init__(self, adapte):
self._adapte = adapte
def process(self, arg):
self._adapte.compressAll(arg)
@implementer(IAction)
@adapter(ITransform)
class AdapterTranform(object):
def __init__(self, adapte):
self._adapte = adapte
def process(self, arg):
tmp = tempfile.mkstemp()
self._adapte.open()
self._adapte.transform(tmp)
self._adapte.close()
os.move(tmp, self._adapte.filename)
gsm.registerAdapter(AdapterTranform)
On enregistre ensuite la série de factories. Les factories ne sont pas
enregistrées par les composants mais dans notre programme. Cela permet de les
nommer comme on le désire dans chaque programme.
ftpFactory = Factory(FTPRemote, 'ftp')
gsm.registerUtility(ftpFactory, IFactory, 'ftp')
httpFactory = Factory(HTTPRemote, 'http')
gsm.registerUtility(httpFactory, IFactory, 'http')
zipFactory = Factory(ZipArchive, 'zip')
gsm.registerUtility(zipFactory, IFactory, 'zip')
tarFactory = Factory(TarArchive, 'tar')
gsm.registerUtility(tarFactory, IFactory, 'tar')
Et parce que j'en ai marre, on factorise le truc.
# I'm bored, I refactore the next factories
for klass, doc in [(LowerTransform, 'lower'),
(UpperTransform, 'upper'),
(Rot13Transform, 'rot13'),
(Base64Transform, 'base64')]:
factory = Factory(klass, doc)
gsm.registerUtility(factory, IFactory, doc)
Et pour finir la partie spécifique de notre programme :
def splitInstruction(instruction):
"""
"""
action, arg1, arg2 = instruction.split(' ')
if '_' in action:
return action.split('_'), arg1, arg2
else:
return action, ' ', arg1, arg2
instructions = open(sys.argvs[1])
for instruction in instructions:
actionName, variante, arg1, arg2 = splitInstruction(instruction)
factory = getUtility(IFactory, actionName)
instance = factory()(arg1)
if variante:
action = getAdapter(instance, IAction, variante)
else:
action = getAdapter(instance, IAction)
action.process(arg2)
Si on doit rajouter une classe qui implémente une interface que nous avons déja
adaptée l'écriture du code devient très facile et très courte.
L'ajout d'une nouvelle classe Base64Decode qui implémente déja ITransform se
résume à l'import de Base64Decode et l'ajout d'une factory pour Base64Decode.
factory = Factory(Base64Decode, 'base64decode')
gsm.registerUtility(factory, IFactory, 'base64decode')
De même pour une série de classes implémentant la même interface, il suffira
d'écrire l'adapater et la série de factories.
Conclusion
La ZCA n'est pas spécifique à zope ou plone ; on la retrouve dans des projets
comme pyramid ou twisted.
La ZCA oblige à penser sous forme de composants « agnostiques » et plus
généralistes puis à les incorporer, les adaptant à nos applications métiers.
À court terme, le processus est plus coûteux car il faut concevoir de façon
générique mais à moyen terme et surtout à long terme le temps de développement
se réduit beaucoup car on réutilise en permanence des composants déja écrits.
Ces composants étant plus petits et plus mono taches sont faciles à tester.
Je n'ai pas tout abordé voici quelques pointeurs qui peuvent être utiles pour
aller plus loin ou completer mes dires :
- un article en anglais sur la ZCA,
- sa traduction en français,
- billet de blog du traducteur du lien précédent.
Merci à jpcw pour la relecture.