[Zope3-dev] Re: Interface declaration API

Jim Fulton jim@zope.com
Wed, 12 Mar 2003 10:22:26 -0500


Phillip J. Eby wrote:
> At 04:35 PM 3/11/03 -0500, Jim Fulton wrote:
> 


...

>> The way you would reject base-class interfaces is to use implements. 
>> That is,
>> if you use implements, you implement *just* what you say you implement,
>> so I still think 'doesNotImplement()' is a yagni.
> 
> 
> This is probably a matter of design principles.  In general, I go for 
> the Liskov substitutability principle; that is, I should be able to use 
> a subclass where the superclass is acceptable.  This implies, in other 
> words, that failing to implement a superclass' interfaces is a no-no.  

I think this is a reasonable principal that is rarely followed.
It would certainly discourage the use of inheritence purely
for implementation sharing.

OTOH, if you create a base class purely to abstract common implementation,
it should really not make any interface declarations.


> In the rare case where I want to reject the legacy of my ancestors, I 
> want to explicitly identify what contracts I'm rejecting, rather than 
> try to reiterate every contract I might still implicitly be supporting.  
> I only want to use 'implements()' if I want to reject *all* ancestral 
> contracts, which is something I've never wanted to do.  For that matter, 
> because of the LSP, I'm not sure I've ever actually rejected even one 
> ancestral contract, but I can easily conceive of situations where I 
> might want to reject some subset of them.
> 
> I will concede that:
> 
> class A(B,C,D,E):
> 
>    implements( implementedBy(B,C,D), IE1, IE2 )
> 
> is almost as usable as:
> 
> class A(B,C,D,E):
>     doesNotImplement(IE3)
> 
> where IE1, IE2, and IE3 are contracts of E, and I want class A to reject 
> IE3.  I find it annoying on principle though, since I want the rejection 
> to be more visible.  Violation of the LSP may indicate a "suspicious 
> smell" in the inheriting class.  (For example, that it is using 
> inheritance when delegation may be a more appropriate technique.)  Not 
> only does the explicit 'implements()' give no clue about this "smell", 
> it also creates a possible super-to-subclass maintenance dependency that 
> may be bad for application frameworks where user code inherits from 
> framework-supplied base classes.

I think that there is always a superclass-subclass dependency.
I think we tend to try to ignore this or sweep it under the rug at
our peril. This is why I've become alergic to frameworks that require
specific base classes.  Sometimes it's an necessary evil, but it's still
evil. :)


...

>>> 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.
>>
>>
>> That's what I was thinking of. Hm. I'll have to ponder a non-descriptor
>> implementation. It might make things cleaner if we decide change the 
>> inheritence
>> semantics.  I was thinking in terms of descriptors because they avoid 
>> having
>> to sniff to decide if something is a class, which is *really really* 
>> painful
>> in the presense of security proxies.
> 
> 
> I'm not sure why you need this.  Consider:
> 
> def directlyProvidedBy(ob):
>     if '__provides__' in ob.__dict__:
>         return ob.__provides__
>     spec = ob.__provides__ = InterfaceSpecification()
>     return spec
> 
> def implementedBy(klass):
>     if '__implements__' in klass.__dict__:
>         return klass.__implements__
>     spec = klass.__implements__ = InterfaceSpecification() # XXX copy or 
> merge from __bases__
>     return spec
> 
> def providedBy(ob):
>     return directlyProvidedBy(ob) + implementedBy(ob.__class__)
> 
> This should work fine for "plain" objects, classes, metaclasses, and 
> "turtles all the way up".  :)  The only sticky bit is the presence of 
> '__dict__'; but if something's __class__ doesn't have a __dict__, it's 
> not going to be something whose ultimate metatype is 'type'.  So the 
> above does need some try/except handling for a missing __dict__ 
> attribute.  The except block would need to check if the non-dicted 
> thing's __class__ has a "data descriptor" for that attribute name, and 
> if so, use it.  This would allow __slots__ to be usable for the 
> __provides__ and __implements__ attributes.
> 
> AFAICT, no part of this approach either requires or prohibits the use of 
> descriptors.  But the __dict__ check *has* to take place (no matter how 
> you implement this, with descriptors or not!) because you otherwise 
> can't distinguish between an explicit and an implicit attribute value, 
> when you don't know if what you're dealing with is a "class" in some sense.

The problem is that, in the presense of security proxies, you don't have access
to the __dict__ attribute. All you have access to is attributes.

Descriptors have a major benefit, in the presense of security proxies, which is
that, within the descriptor, you get access to an unproxied object.
But, I realized since yesterday that the descriptor need not actually store all
of the information. It can walk class bases if it wan't to.  So, I may actually
be able to avoid some of the weird hacks, unless I can't.

It's time for me to start prototyping things.


> 
>>   I guess we shouldn't get too hung up
>> on implementation difficulties at this point and focus on what we want.
> 
> 
> Okay, if we're not worrying about implementation, then what I suggest we 
> want is to inherit the union of the interfaces declared by a class' 
> __bases__.  :)

Unless we say otherwise.

...

> Threadsafety is still at issue. 

With a little effort, this is pretty easy to deal with.

 > Complexity and performance
> too.  You now have to return a new object from the descriptor's return 
> value, that knows where you were retrieving it from (like a context 
> wrapper) so that it can mutate the correct thing.  That seems like a 
> cure worse than the disease.  It would be much simpler to 
> mutate-on-query and be done with it.

Maybe. I think that this framework is important enough that we
shouldn't get too hung up on complexity. Performance is important, *but*,
I've found that to get reasonable performance, you really need to employ
caching in ways that make the actual algorithm performance less important.
For we'll need to cache isImplementedBy calls, making the efficiencey
of the uncached computation less important.

I think we should put the main focus on a usable API.

> Anyway, I'm not objecting to the complexity, per se, just saying that 
> __mro__-first inheritance of declaration doesn't actually buy you any 
> real simplification.

I've decided to change that.

...

> In short: Since I've offered to back up my desires with code, I could go 
> for either implementing immutable specs and __mro__-first inheritance 
> (simple way), or adding my fancy bits (__bases__, alsoImplements, 
> doesNotImplement) to a mutable specs approach.  If you guys are going to 
> do the hard work to make mutable specs workable (and again, I don't see 
> why they're valuable), then I don't see why I shouldn't benefit by 
> contributing a few extra bells and whistles.  :)  Conversely, if you 
> don't want me to contribute the bells and whistles, I don't see why you 
> shouldn't just do the "simplest thing that could work" and do immutable 
> specs + __mro__-first inheritance.
> 
> I could be happy with either approach, speaking both as a consumer and 
> potential co-implementer.

Let's put the implementation issues aside. I'm sorry I brought them
up. That's a bad habit of mine. I'd love to get code contributions, but
for now I want to focus on getting a good API and I'd like to do some
prototyping myself.

I really appreciate the great input. Thanks much.

Jim