views:

139

answers:

1

Updated question, see below

I'm starting a new project and I would like to experiment with components based architecture (I chose PyProtocols). It's a little program to display and interract with realtime graphics.

I started by designing the user input components:

  • IInputDevice - e.g. a mouse, keyboard, etc... An InputDevice may have one or more output channels:
    • IOutput - an output channel containing a single value (e.g. the value of a MIDI slider)
    • ISequenceOutput - an output channel containing a sequence of values (e.g. 2 integers representing mouse position)
    • IDictOutput - an output channel containing named values (e.g. the state of each key of the keyboard, indexed by keyboard symbols)

Now I would like to define interfaces to filter those outputs (smooth, jitter, invert, etc...).

My first approach was to create an InputFilter interface, that had different filter methods for each kind of output channel it was connected to... But the introduction in PyProtocols documentation clearly says that the whole interface and adapters thing is about avoiding type checking !

So my guess is that my InputFilter interfaces should look like this:

  • IInputFilter - filters IOutput
  • ISequenceInputFilter - filters ISequenceOutput
  • IDictInputFilter - filters IDictOutput

Then I could have a connect() method in the I*Ouptut interfaces, that could magically adapt my filters and use the one appropriate for the type of output.

I tried to implement that, and it kind of works:

class InputFilter(object):
    """
    Basic InputFilter implementation. 
    """

    advise(
            instancesProvide=[IInputFilter],
        )

    def __init__(self):
        self.parameters = {}

    def connect(self, src):
        self.src = src

    def read(self):
        return self.src.read()


class InvertInputFilter(InputFilter):
    """
    A filter inverting single values.
    """

    def read(self):
        return -self.src.read()


class InvertSequenceInputFilter(InputFilter):
    """
    A filter inverting sequences of values.
    """

    advise(
            instancesProvide=[ISequenceInputFilter],
            asAdapterForProtocols=[IInputFilter],
        )

    def __init__(self, ob):
        self.ob = ob

    def read(self):
        res = [] 
        for value in self.src.read():
            res.append(-value)
        return res

Now I can adapt my filters to the type of output:

filter = InvertInputFilter()
single_filter = IInputFilter(filter)           # noop
sequence_filter = ISequenceInputFilter(filter) # creates an InvertSequenceInputFilter instance

single_filter and sequence_filter have the correct behaviors and produce single and sequence data types. Now if I define a new InputFilter type on the same model, I get errors like this:

TypeError: ('Ambiguous adapter choice', <class 'InvertSequenceInputFilter'>, <class 'SomeOtherSequenceInputFilter'>, 1, 1)

I must be doing something terribly wrong, is my design even correct ? Or maybe am I missing the point on how to implement my InputFilterS ?

Update 2

I understand I was expecting a little too much magic here, adapters don't type check the objects they are adapting and just look at the interface they provide, which now sounds normal to me (remember I'm new to these concepts !).

So I came up with a new design (stripped to the bare minimum and omitted the dict interfaces):

class IInputFilter(Interface):

    def read():
        pass

    def connect(src):
        pass


class ISingleInputFilter(Interface):        

    def read_single():
        pass


class ISequenceInputFilter(Interface):

    def read_sequence():
        pass

So IInputFilter is now a sort of generic component, the one that is actually used, ISingleInputFilter and ISequenceInputFilter provide the specialized implementations. Now I can write adapters from the specialized to the generic interfaces:

class SingleInputFilterAsInputFilter(object):

    advise(
            instancesProvide=[IInputFilter],
            asAdapterForProtocols=[ISingleInputFilter],
        )

    def __init__(self, ob):
        self.read = ob.read_single


class SequenceInputFilterAsInputFilter(object):

    advise(
            instancesProvide=[IInputFilter],
            asAdapterForProtocols=[ISequenceInputFilter],
        )

    def __init__(self, ob):
        self.read = ob.read_sequence

Now I write my InvertInputFilter like this:

class InvertInputFilter(object):

    advise(
            instancesProvide=[
                    ISingleInputFilter, 
                    ISequenceInputFilter
                ]
        )

    def read_single(self):
        # Return single value inverted

    def read_sequence(self):
        # Return sequence of inverted values

And to use it with the various output types I would do:

filter = InvertInputFilter()
single_filter = SingleInputFilterAsInputFilter(filter)
sequence_filter = SequenceInputFilterAsInputFilter(filter)

But, again, this fails miserably with the same kind of error, and this time it's triggered directly by the InvertInputFilter definition:

TypeError: ('Ambiguous adapter choice', <class 'SingleInputFilterAsInputFilter'>, <class 'SequenceInputFilterAsInputFilter'>, 2, 2)

(the error disapears as soon as I put exactly one interface in the class' instancesProvide clause)

Update 3

After some discussion on the PEAK mailing list, it seems that this last error is due to a design flaw in PyProtocols, that does some extra checks at declaration time. I rewrote everything with zope.interface and it works perfectly.

+1  A: 

I haven't used PyProtocols, only the Zope Component Architecture, but they are similar enough for these principles to be the same.

Your error is that you have two adapters that can adapt the same thing. You both have an averaging filter and an inversion filter. When you then ask for the filter, both are found, and you get the "ambigous adapter" error.

You can handle this by having different interfaces for averaging filters and inverting filters, but it's getting silly. In the Zope component architecture you would typically handle this case with named adapters. Each adapter gets a name, by default ''. In this case you would give the adapter names like "averaging" and "inverting", and you'd look them up with that name, so you know if you get the averaging or the inverting filter.

For the more general question, if the design makes sense or not, it's hard to tell. You having three different kinds of outputs and three different kinds of filters doesn't seem like a good idea. Perhaps you could make the sequence and dict outputs into composites of the single value output, so that each output value gets it's own object, so it can be filtered independently. That would make more sense to me.

Lennart Regebro
Thanks now I better understand how adapters work.Named adapters are not available in PyProtocols, and if I understand well they are just syntactic sugar and it would be basically the same situation as having different interface for each of my filter (I want to be able to create filters in separate plugins, without bothering to create new interfaces or unique names).I don't want to do composition because it yields to type checking, which is what I wanted to avoid in the first place.
Luper Rouch
I don't agree that they are syntactic sugar. In this case you can have one interface type instead of a name, but in many cases you end up having many components which are exactly the same except for a name, and having one interface per each component gets unruly. Also, with named adapters you can have component factories create all the components from a list of names, which gets very tricky without named components. Having multiple plugins without differentiating either on interface or name is impossible.I don't see why composition must lead to type checking...
Lennart Regebro
My second design (see update above) is correct, my problem was in fact due to PyProtocols: I tried to rewrite the whole thing with zope.interface and it works. If I did composition, the InputFilter read() method would require branching depending on the data type it receives from its output channel (if type(data) is dict [...] elif type(data) is list [...] else [...])
Luper Rouch
No, the point of composition is that you don't have to do that, by making the interface be the same in all cases. Basically, you could use the dict interface always.
Lennart Regebro
Oh sorry I misunderstood what composition meant... I think I'm going to redesign it that way, this 3 kinds of everything design adds complexity and offers no advantages... Thanks !
Luper Rouch