[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