| Moshe Zadka ( @ 2004-01-19 08:35:00 |
| Current mood: | awake |
| Current music: | Radio Buffy -- Show 81 (New Songs) |
Interfaces and Casting
Due to popular demand, I'm writing another entry about programming.
I'm going to talk about a "pattern" (what Alex Martelli calls a "non-pattern" since it hasn't had usage research despite having all other parts) -- "Thin Interface/Thick Interface". The case in question is when it is possible to define many high-level operations based on small number of primitives. Commonly, the solution is to have a "thin interface" (the primitives) and a number of generic functions which accept the thin-interface-implementing objects as arguments, and implement the high-level operations. However, the problem begins when sometimes, it is needed to implement the high-level operations in a special way for a specific class that implements the thin interface, because it is possible to do so more efficiently -- or perhaps because the canonical relations don't exist between the primitives and the high-level operations in this case.
Edit: added a more comprehensive example.
In order to explain the solution I propose, I am going to use the language of Twisted's components framework.
The first stage is deciding on the interfaces. I assume IThinInterface is, more or less, given. For simplicity, I will assume that we just use IThickInterface, so other possibilities include dividing IThickInterface into several aspects (IFirstAspect, ISecondAspect), and optionally inheriting IThickInterface from all of those. Where ever one wants to use the thick methods, one uses something like IThickInterface(o).thickMethod(). Implementing objects usually go something like
class ThinInterfaceImpl:
__implements__ = IThinInterface
def simpleMethod(self):
pass # implement
The library should define an IThinInterface->IThickInterface adapter.
When one needs to have a special implementation of the thick interface, there are two ways to go about it. Either define a class that implements IThickInterface, or define a specialized adapter based on class from to IThickInterface.
Advice: IThickInterface will usually want to inherit from IThinInterface. The canonical adapter should just relay all IThinInterface methods to the original adaptee.
Here is another example. Suppose we have things we want to write to files. However, some things may be large, and so might want to write themselves to files in a piece-wise manner: have a startup, and each time have a method called which will write a bit more, and finally closed down. Here is an example:
class ISimpleLoggable(Interface):
def __str__(self): "return a string"
class ILoggable(Interface):
def start(self, fp):
"prepare writing yourself to fp"
def writeBits(self):
"write something to fp. return whether there is anymore"
class SimpleToLoggable(Adapter):
__implements__ = ILoggable
def start(self, fp):
self.fp = fp
def writeSomething(self):
self.fp.write(str(self.original))
return False
class FileCopier:
__implements__ = ILoggable
def __init__(self, fin):
self.fin = fin
def start(self, fp):
self.fout = fp
def writeSomething(self):
buffer = self.fin.read(4096)
if buffer:
self.fp.write(buffer)
return bool(buffer)
registerAdapter(SimpleToLoggable, ISimpleLoggable, ILoggable)
Now, classes can decide whether to settle for a simple __str__ implementation (which often has to be written anyway) or whether to serialize themselves in a more complicated manner. Note that FileCopier chose to implement the more complicated way, because it doesn't want to slow down an event loop -- maybe each writeSomething takes place in another loop.
References: Note that, using a slightly different mechanism, Python itself uses a similar concept for iterators. The thin interface here is __getitem__, and the iter() builtin can be thought of asking for something that implements the iterator interface. If __iter__ exists, then it is used (the "thick" interface), otherwise __getitem__ is used to generate an iterator.