In Part 1 we learned about interfaces, in part 2 about utilities, one
main part left is adapters.
In part 1 I already mentioned those huge lists of superclasses in Zope 2
which mainly were used to share functionality between classes and make
it sort of more pluggable. Pluggability has it's limits though because
those superclasses are fixed. If you wanted to replace one of those you
can do so maybe in your own class (by overriding he methods used) but
you cannot do so for classes of packages you don't own.
An additional problem is that with all those superclasses you have a
huge namespace and have no idea anymore which methods are defined in
which class (with mixins you have also potential naming conflicts where
it depends on the order of superclasses which method actually is used).
So all in all it's a mess and testing is not becoming easier.
So let's look at adapters as one means to factor functionality out in
simple components which can be tested separated from the rest and can
even be replaced by packages which do not own them.
To keep our example going let's look at our Document class. In a CMS we
might want to list all documents with their respective sizes. Even more,
we want to list all those content types with their size.
Our document class did not have a size() method which we could use so we
need to implement it. We could either put it directly into the Document
class or we could subclass it and put it into a SizedDocument subclass.
Both would be problematic though if we cannot change the class because
the document class is defined in a package we don't own. Moreover
subclassing makes only sense if we can convince all packages which use
the Document class and are not owned by us to use the new subclass.
Imagine this is not possible and moreover we don't want to grow that
Document class with more and more functionality (size might not the only
thing we want to add).
This is where adapters can help us. An adapter is basically just a
wrapper around the original object which implement the functionality we
want. It might look as follows:
class DocumentSizeAdapter(object):
def __init__(self, context):
"""initialize the adapter for the document"""
self.context = context
def size(self):
"""return the size of the document"""
return len(self.context.title) + len(self.context.body)
We could instantiate it like this:
size_adapter = DocumentSizeAdapter(document)
print size_adapter.size()
The adapter stores the original document in self.context and uses this
reference to do it's work (here computing the size).
Now this can be formalized again with some interface. We can define an
interface for the size functionality:
class ISize(Interface):
"""handle size in adapter form"""
def size():
"""return the size of an object"""
This apparently is what our adapter implements, so let's say so:
from zope.interface import implements
class DocumentSizeAdapter(object):
implements(ISize)
...
The adapter also relies on "title" and "body" attributes of the object
which we pass in. These attributes are known to use because we defined
them in the IDocument interface.
We can thus say that this adapter converts or adapts an IDocument
interface to an ISize interface. To tell the system we can add this:
from zope.interface import implements
from zope.component import adapts
class DocumentSizeAdapter(object):
implements(ISize)
adapts(IDocument)
...
We now have an adapter which adapts any object which implements
IDocument (it does not need to be our implementation) to ISize. It does
so by using attributes and method of the IDocument interface and
provides all methods and attributes needed by the ISize interface.
The implementation is also rather small so there is not much to test and
the tests should look quite simple.
Moreover we can also replace this implementation by any other
implementation (maybe a faster one) if we want to. We can even do so in
a different package.
But of course we don't know yet how we can use the adapter without using
it's implementation. What we need is to look it up by just knowing the
interface because otherwise we cannot replace the implementation.
So first we need to register it again:
gsm.registerAdapter(DocumentSizeAdapter)
(gsm is again the global site manager, basically the global registry)
As the adapter defines everything needed (implements and adapts) we
don't need to specify it here. We simply give the factory of it (in this
case the class which always is a factory).
To actually use it we do the following:
document = Document(....)
size_adapter = ISize(document)
print document.size()
As you can see we don't use the implementation anymore. We basically
call the interface we want (ISize) and pass it the object we have
(document). The registry will check what interfaces are provided by the
object (IDocument and IContentType are the ones in our case because
IDocument derives from IContentType) and checks if there is some adapter
registered which adapts one of these interfaces to ISize (IDocument
matches in our case). It then calls the factory we passed (the class
DocumentSizeAdapter in our case) and passes the object to it (to our
__init__). This is the context of the adapter. Then it returns what the
factory returned (the adapter instance) and we can use the ISize
interface of it.
You might also read ISize(document) as a cast to a different type as
it's done in other languages.
One usage of this in pyogp is e.g. for serializing objects and dicts to
LLSD or JSON. The default serialization might be LLSD but some package
might want to replace this by JSON. It can now do so without changing
pyogp itself.
To do so we need to register an adapter which takes the object and
returns an ISerialization interface which might look like follows:
class ISerialization(Interface):
def serialize():
"""return the serialized representation of the object"""
And we could implement an adapter for dictionaries to return LLSD:
class DictLLSDSerializer(object):
implements(ISerialization)
adapts(dict) # note that we can also adapt classes and types, not
just to interfaces
def __init__(self, context):
"""context is a dictionary, we store it in context"""
self.context = context
def serialize(self):
"""serialize to LLSD via the llsd lib"""
return llsd.format_xml(self.context)
This can be registered:
gsm.registerAdapter(DictLLSDSerializer)
And used:
d={'caps': 'somecaps'}
serializer = ISerialization(d)
print serializer.serialize()
The beauty of this is that we can always use ISerialize(something) for
any object, dictionary etc. and it will always produce the serialization
we configured (as long as we have written adapters for it of course).
A separate package might simple re-register the adapter to e.g. an
DictJSONSerializer to produce JSON output.
All about how to register those components in different ways will be
shown in part 4.
--
Christian Scholz video blog: http://comlounge.tv
COM.lounge blog: http://mrtopf.de/blog
Luetticher Strasse 10 Skype: HerrTopf
52064 Aachen Homepage: http://comlounge.net
Tel: +49 241 400 730 0 E-Mail [EMAIL PROTECTED]
Fax: +49 241 979 00 850 IRC: MrTopf, Tao_T
neue Show: TOPFtäglich (http://mrtopf.de/blog/category/topf-taglich/)
_______________________________________________
Click here to unsubscribe or manage your list subscription:
https://lists.secondlife.com/cgi-bin/mailman/listinfo/pyogp