[Zope3-dev] Interface declaration API
Phillip J. Eby
pje@telecommunity.com
Tue, 11 Mar 2003 15:41:28 -0500
At 02:46 PM 3/11/03 -0500, Jim Fulton wrote:
>Subclassing is done to reuse implementation. If interfaces were
>inherited, then you would be unable to subclass a class without
>also promising to implement the base class interfaces. I've always
>viewed this as an undesireable requirement, but maybe I'm wrong,
>since so many people desire it, ;) So I'm willing to give on this.
Ironically, you then go on to say that 'doesNotImplement()' would be a
YAGNI. :) I actually suggested 'doesNotImplement()' as a way to deal with
the situation above. If one wishes to reject base class interfaces, one
might then do so explicitly.
>Soooooooo ....
>
>Let's suppose we decided that in:
>
> class C(A, B):
>
> alsoImplements(IFoo)
>
>The outcome would be that C's instances would implement
>IFoo and whatever A's and B's instances implement.
>
>This is implementable, but with a hack. The hack is that the
>descriptor needs to update itself the first time that it's used. :(
>
>In::
>
>
> class C(A, B):
>
> pass
>
>The outcome would be that C's instances would implement
>whatever A's and B's instances implement.
>
>This too requires a pretty heavy hack. The first time you tried to
>get the instance interface spec for a C, you'd inherit a spec from
>A or B. The inherited descriptor would detect that it was inherited
>and create a new descriptor for C, merging the interfaces from A and
>B.
>
>Finally, with:
>
> class C(A, B):
>
> implements(IFoo)
>
>The outcome would be that C's instances would implement
>only IFoo.
I've previously implemented late-binding class descriptors of this kind,
and I agree that they aren't appropriate for Zope 3. One potential issue,
for example, is thread-safety of the object that does the late binding
(because non-persistent classes are normally shared between threads).
I do notice, however, that your description seems to imply a
descriptor-based implementation. If the intention is to encapsulate the
implementation, it's not clear to me that putting the behavior in
a descriptor is the most useful way to go. For example, 'implementedBy()'
might look something like this:
def implementedBy(klass):
spec = klass.__dict__.get('__implements__')
if spec is None:
spec = klass.__implements__ = InterfaceSpecification(
*tuple([implementedBy(c) for c in klass.__bases__])
)
return spec
Notice this also has the same thread-safety issue. Also, you have to have
*something* that does this, if you want 'implementedBy()' to return a
mutable object!
So, if you really want to simplify this, I'd suggest going back to
immutable interface specs. This would then require an explicit rebinding
(e.g. klass.__implements__ += IFoo) and wouldn't need a check for whether
the specs were inherited or not. You also wouldn't need a descriptor,
which means that "classic" classes might be better supported. (I'm not
positive that any of classic classes' limitations on descriptors are an
issue here, though.)
In order to preserve encapsulation, you would need to have functions
like 'addImplements()', 'addProvides()', 'setImplements()', etc. While
this expands the number of API functions, it also increases the
explicitness of such operations (one may grep for 'setImplements()' more
easily than for 'foo=implementedBy(Bar) ... multiple lines of code ...
foo.add(IBaz)'. That is, one is now required to put the operation in the
same place with the thing being operated upon.
Doing this would actually give you an implementation benefit to sticking
with "first in MRO" inheritance semantics for interface declarations, er,
specifications.
(By the way... I always stumble on calling them "interface
specifications". To me, the interface specification is the "class
IFoo(Interface):" block, while the "__implements__ = IFoo" is an *interface
declaration*. But I may be alone in this.)
Anyway... what I'm saying is, if you stick with mutable specifications,
you might as well implement "merged __bases__" inheritance, because there
is no less implementation complexity. AFAICT, to reduce the implementation
complexity from what you described, you have to also make specifications
immutable, so that it never matters if the specification is inherited or
not. Otherwise, you are right back in the same boat of needing to either
have a lazily-binding descriptor, or a function that acts like one "from
the outside".