[PEAK] PyProtocols --

Phillip J. Eby pje at telecommunity.com
Mon Feb 16 11:19:45 EST 2004


At 04:17 PM 2/16/04 +0100, Gabriel Jägenstedt wrote:
>I should most likely try to explain this in more detail.
>
>1. A noun should be able to be instantiated and be usable as an everyday
>object like maybe a rock.
>
>2. Making more complex items like doors would mean implementing new
>behaviour. We need to have the door lead somewhere they should be able
>to be locked and opened. In the current(working) object system this is
>done by ways of inheritance. However when we have walked down a line of
>say 5 inheritences it's getting hard to understand what it really does.
>The code is cluttered and so on. Furthermore if we wish to add more than
>one subclass of Noun into a new objects we have to use multiple
>inheritence. I have heard from some that this is not a good idea.
>
>3. I also wish that as time passes this makeover on the code will make
>it easier to implement a more pretty aproach to prefixes such as on and
>with. Put money on table, hit man with gun and so on.
>
>4. I want methods and variables to be easily obtainable for interaction
>between a nouns interfaces. Edible should be able to call up Poisonous
>to check for special effects of eating a poisonous mushroom.
>
>5. I want the ability to not overwrite existing functions unless I
>declare that it should be done, but instead be able to add to existing
>methods.  Or atleast have a way to determine which method should be used
>depending on action, I guess this is right up the alley for
>interfaces/adapters.
>
>6. features like edible and such will most likely be added to a subclass
>of noun and be used as objects in a library for the use of authors. The
>main purpose of looking into interfaces is the hope that it should make
>life easier for the author.
>
>7. Default error messages when objects don't support certain intefaces.
>or is it the other way around?
>
>I think this pretty much sums it all up. I hope this straightens out
>some questionmarks.

So far, it's sounding to me like what you want is sort of like the San 
Francisco "Extensible Item" pattern, which is like a cross between the 
"Composite" and "Chain of Responsibility" patterns.

To be more specific, it sounds like you need for objects to ask their 
contents to respond to messages from the outside world.  So, you could have 
food contain a Poison object, for example, and then it would respond to the 
IEdible behavior.

Notice, by the way, that this isn't inheritance; you don't inherit from 
Poisonous to make somethign poisonous, you instead make a Poison instance a 
part of the object.  Same thing for the lock on the book or the door.

So, the basic idea is that to perform an operation like "eat", you simply 
go through the object's contents in a post-order traversal, and adapt 
things to IEdible.  Sort of like this:


def chainOfResponsibility(ob,proto,default=None):

     # may not be needed if all Nouns are composites
     whole = adapt(ob,IComposite,None)

     if whole is not None:
         for part in whole.parts:
             for adapted in chainOfResponsibility(part,proto):
                 yield adapted

     adapted = adapt(ob,proto,None)
     if adapted is not None:
         yield adapted

     if default is not None:
         yield default


def invoke(ob,proto,methodName,commandObj):
     chain = chainOfResponsibility(ob,proto,proto)
     first = chain.next()
     getattr(first,methodName)(chain,commandObj)


class IEdible(protocols.Interface):

     def eat(klass, chain, commandObj):
         print "The",commandObj.target,"can't be eaten"

     eat = classmethod(eat)


class Poison(Noun):

     protocols.advise(instancesProvide=[IEdible])

     def eat(self, chain, commandObj):
         if invoke(commandObj.player,IMagical,'preventsPoisoning',commandObj):
             return chain.next().eat(chain,commandObj)
         else:
             print "You're dead, buddy."


So, each object gets an iterator that yields the next object in the 
post-order traversal that implements the desired interface, or if there are 
no candidates remaining, it returns the default implementation provided by 
the interface itself as a class method.  Each invoked method has the 
opportunity to call 'chain.next().whatever(chain,...)' to get the result of 
a sort of "super" call.

So, if you need to implement weight, you could call 
'invoke(ob,ICarriable,"getWeight",commandObj)', and each object's 
implementation would look like:

     def getWeight(self,chain,commandObj):
         return self.weight + chain.next().getWeight(next,commandObj)

and the default implementation in ICarriable would simply return 0.

For openability, you'd have a door implement IOpenable, but if it contains 
a lock, the lock would get the chance to refuse to be opened, unless it was 
unlocked, in which case it would call 
'chain.next().open(chain,commandObj)', thus invoking the door's open() method.

This is of course all very high-level.  I don't know what your 'commandObj' 
looks like, or whether you even have one, or whether you pass other 
arguments to these verb-ish methods.  You might also refactor this in 
various other ways.  For example, you might put the player and the object's 
he's carrying into the chain of responsibility, or maybe even include the 
room as part of the chain, and having verb-ish methods check the target of 
the command object to see if it is them that is being eaten or opened or 
whatever.  This would let you do things like having something be poisonous 
if it's eaten in a particular room, for example.  At that point, IEdible 
would probably be more meaningfully called IEatingListener, since it 
doesn't mean the object is edible, only that it wants to know about 
eating-related events.

Anyway, as a whole, this approach appears to satisfy your main 
criteria.  You do not need lots of multiple inheritance, for example.  You 
can create lots of fine-grained "aspect" objects (like Poison) and 
relatively simple components (like Lock), and then use them as parts to 
assemble more complex objects.  These aspects and subcomponents can then 
override the behavior of the parent components, if/when they desire to do so.

The only drawback to the approach as I've presented it is that you can only 
delegate to one method of the 'chain.next()' object, because iterators 
aren't repeatable.  However, that can be solved with:

nextOb = chain.next()
chain = list(chain)

nextOb.method1(iter(chain),commandObj)
nextOb.method2(iter(chain),commandObj)

# etc.




More information about the PEAK mailing list