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

Reply via email to