A collection of articles, ideas, and rambling from a guy who wrote some software that one time.

Monday, February 16, 2009

Explaining Why Interfaces Are Great

Why?

Why use interfaces? Especially with Python's new ABCs, is there really a use for them?

Some of us Zope Interface fans — names withheld to protect the guilty, although you may feel free to unmask yourselves in the comments if you like — have expressed frustration that ABCs beat out interfaces for inclusion of the standard library.  However, I recently explored various mailing lists, Interfaces literature, and blogs, and haven't found a coherent description of why one would prefer interfaces over ABCs.  It's no surprise that Zope's interface package is poorly understood, given that nobody has even explained it!  In fact, PEP 3119 specifically says:
For now, I'll leave it to proponents of Interfaces to explain why Interfaces are better.
It seems that nobody has taken up the challenge.

I remember Jim Fulton trying to explain this to me many years ago, at a PyCon in Washington DC.  I definitely didn't understand it then.  I was reluctant to replace the crappy little interfaces system in Twisted at the time with something big and complicated-looking.   Luckily the other Twisted committers prevailed upon me, and Zope Interface has saved us from maintaining that misdesigned and semi-functional mess.

During that explanation, I remember that Jim kept saying that interfaces provided a model to "reason about intent".  At the time I didn't understand why you'd want to reason about intent in code.  Wouldn't the docstrings and the implementation specify the intent clearly enough?  Now, I can see exactly what he's talking about and I use the features he was referring to all the time.  I don't know how I'd write large Python programs without them.

Caveat

This isn't a rant against ABCs.  I think ABCs are mostly pretty good, certainly an improvement over what was (or rather, wasn't) there before.  ABCs provide things that Interfaces don't, like the new @abstractmethod and @abstractproperty decorators. Plus, one of the irritating things about using zope.interface is that the metadata about standard objects in zope.interface.common is not hooked up to anything: IMapping.providedBy({}) returns False.  ABCs will provide that metadata in the standard library, making zope.interface that much more useful once it has been upgraded to understand the declarations that the collections and numbers modules provide.

So, on to the main event: what do Zope Interfaces provide which makes them so great?

Clarity

Let's say we have an idea of something called a "vehicle".  We can represent it as one of two things: a real base class (Vehicle), an ABC (AVehicle) or an Interface (IVehicle).

There are a set of operations that interfaces and base-classes share.  We can ask, "is this thing I have a vehicle"?  In the base-class case we spell that 'if isinstance(something, Vehicle)'.  In the interfaces case, we say 'if IVehicle.providedBy(something)'.  We can ask, "will instances of this type be a vehicle?".  For an interface, we say 'if IVehicle.implementedBy(Something)', and for a base class we say 'issubclass(Something, Vehicle)'.  With the new hooks provided by the ABCs in 2.6 and 3.0, these are almost equivalent.  With zope.interface, you can subclass InterfaceClass and write your own providedBy method.  With the ABC system, you subclass type and implement __instancecheck__.

However, there are some questions you can't quite cleanly ask of the ABC system.  For one thing, what does it really mean to be a Vehicle?  If you are looking at AVehicle, you can't tell the difference between implementation details and the specification of the interface.  You can use dir() and ignore a few of the usual suspects — __doc__, __module__, __name__, _abc_negative_cache_version — but what about the quirkier bits?  Metaclasses, inherited attributes, and so on?  There's probably some way to do it, but I certainly can't figure it out quickly enough to include in this article.  In other words, types have two jobs: they might be ABCs, or they might be types, or they might be both, and it's impossible to separate those responsibilities.

With an Interface, this question is a lot easier to ask.  For a quick look, list(IVehicle) will give a complete list of all the attributes expected of a vehicle, as strings.  If you want more detail, IVehicle.namesAndDescriptions() and Method.getSignatureInfo() will oblige.

Since the interface encapsulates only what an object is supposed to be, and no functionality of its own, it's possible for frameworks to inspect them and provide much nicer error messages when objects don't match their expectations.  zope.interface.verifyClass and zope.interface.verifyObject can tell you, both for error-reporting and unit-testing purposes, whether an object looks like a proper vehicle or not, without actually trying to drive it around.

Flexibility

At the most basic level, interfaces are more flexible because they are objects.  ABCs aren't objects, at least in the message-passing smalltalk sense; they are a collection of top-level functions and some rules about how those functions apply to types.  If you want to change the answer to isinstance(), you need to register a type by using ABCMeta.register or overriding __instancecheck__ on a real subclass of type.  If you want to change the answer to providedBy, for example for a unit test, all you need is an object with a providedBy method.

Of course, you can do it "for real" with an InterfaceClass, but you don't need to.  In other words, its semantics are those of a normal method call.

Interfaces aren't completely self-contained, of course: there are top-level functions that operate on interfaces, like verifyObject.  However, there's an interface to describe what is expected:

>>> from zope.interface.interfaces import Interface, IInterface
>>> IInterface.providedBy(Interface)
True


There's also the issue of who implements what.  For example, you might have a plug-in system which requires modules to implement some functionality.  Generally speaking, modules are instances of ModuleType, so specifying that all modules implement some type with an ABC is somewhat awkward.  With an interface, however, there is a specific facility for this: you put a moduleProvides(IVehicle) declaration at the top of your module.

In zope.interface, there is a very clear conceptual break between implements and provides.  A module may provide an interface — i.e. be an object which satisfies that interface at runtime — without there being any object that implements that interface — i.e. is a type whose instances automatically provide it.  This distinction comes in handy when avoiding certain things.  This distinction exists with ABCs; either you "are a subclass of" a type or you "are an instance of" a type, but the language around it is more awkward and vague, especially since you can be a "virtual instance" or "virtual subclass" now as well.

There's also the issue of dynamic proxies.  If you have a wrapper which provides security around another object, or transparent remote access to another object, or records method calls (and so on) the wrapper really wants to say that it provides the interfaces provided by the object it is wrapping, but the wrapper type does not implement those interfaces.  In other words, different instances of your wrapper class will actually provide different interfaces.  With zope.interface you can declare this via the directlyProvides declaration.  With ABCs, this is not generally possible because ABCMeta.register will only work on a type.

Adaptation

Let's say I have an object that provides IVehicle.  I want to display it somehow — and in today's web-centric world, that probably means "I want to generate some HTML".  How do I get from here to there?  ABCs don't provide an answer to that question.  Interfaces don't do that directly either, but they do provide a mechanism which allows you to provide an answer: you can adapt from one interface to another.

I'm not going to get into the intricacies of exactly how adaptation works in zope.interface, since it isn't important to understand most of the time.  Suffice it to say you can adapt based on specific hooks that are registered, based on the type an object is, or based on what interfaces it provides.

The gist of it is that you have some thing that you don't know what it is, and you want an object that provides IHTMLRenderer.  The way you express that intent is:

    renderer = IHTMLRenderer(someObject)

If there are no rules for adapting an object like the one you have passed to an IHTMLRenderer, then you will get an exception - which is all that will happen, normally.  However, this point of separation between the contract that your code expects and the concrete type that your code ends up actually talking to can be very useful.

The larger Zope application server has a rich and complex set of tools for defining which adapter is appropriate in which context, but Twisted has a very simple interface to adaptation.  You simply register an adapter, which is a 1-argument callable that takes an object that conforms to some interface or is an instance of some class, and returns an object that provides another interface.  Here's how you do it:

    from twisted.python.components import registerAdapter
    class VehicleRenderer(object):
        "Render a vehicle as HTML"
        implements(IHTMLRenderer)
        def __init__(self, vehicle):
            self.vehicle = vehicle
        def renderHTML(self):
            return "<h1>A Great Vehicle %s (%s)</h1>" % (
                       self.vehicle.make.name,
                       self.vehicle.model.name)
    registerAdapter(VehicleRenderer, IVehicle, IHTMLRenderer)


Now, whenever you do IHTMLRenderer(someVehicle), you'll get a VehicleRenderer(someVehicle).

Your code for rendering now doesn't need any special-case knowledge about particular types.  It is written to an interface, and it's very easy to figure out which one; it says "IHTMLRenderer" right there.  It's also easy to find implementors of that interface; just grep for "implements.*IHTMLRenderer" or similar.  Or, use pydoctor and look at the "known implementations" section for the interface in question.

Conclusion

In a super-dynamic language like Python, you don't need a system for explicit abstract interfaces.  No compiler is going to shoot you for calling a 'foo' method.  But, formal interface definitions serve many purposes.  They can function as documentation, as a touchstone for code which wants to clearly report programming errors ("warning:  MyWidget claims to implement IWidget, but doesn't implement a 'doWidgetStuff' method"), and a mechanism for indirection when you know what contract your code wants but you don't know what implementation will necessarily satisfy it (adaptation).

Even with a standard library mechanism for doing some of these things, Zope Interface remains a highly useful library, and if you are working on a large Python system you should consider augmenting its organization and documentation with this excellent tool.

12 comments:

Cory said...

Although I understood everything in the post, and I understand the usefulness of interfaces in general, I don't use them and one of the sticking points is:

I can't figure out when something should just be a class and when something should implement an interface. What's the heuristic to say "this feels like a thing that implements an interface, not just a thing."

The extra effort to make something an interface always ends up being wasted because I misunderstood the purpose of the class I was creating, or I can't get adaptation to work the way I wanted, or something. If I knew how to recognize code that would benefit from interfaces, I would put the effort into those classes more surgically and get some benefit from the system.

glyph said...

@Cory,

An interesting point. Probably worth its own post, or maybe even its own book :).

Briefly, my own rule is that, there should be an interface if there's more than one implementation of that interface and its commonalities need to be documented. This is especially true if the interface is something that "application code" is supposed to implement.

You don't want to reason about the interface from the perspective of what you're implementing; look at it from the perspective of a caller. Once you've got multiple implementations of something, you can look at what the callers want and examine that. You might want to have methods on one implementation which are not part of that interface.

Wheat said...

Amen! Interfaces are great.

When writing Interfaces, it's easier to focus on the purpose than it is with ABCs. Interfaces are specification and documentation only.

With ABCs their purpose is more muddied. When writing an ABC for the method bodies should you write: 1) a default, generic implementation that is intended to be usable 2) the method body is intended only for code-as-documentation but the implementation is not suitable for real use 3) raise a redundant NotImplemented error 4) raise some other type of error to specify the type of error the overridden methods should throw 5) write a real abstract method - where abstract means "method signature and doc string only" 6) some mix of all of the above.

Separating interfaces from implementations makes the code more readable as well, I find. With Interfaces you have the choice of either following base classes hierarchy to see how the implementation works, or following the interfaces hierarchy to see how the intent is structured. With ABCs you might find these two tasks interlaced, first an implementation class, then an ABC, then another implementation, then another ABC ...

ABCs don't currently allow for Adapation, but I'm guessing it would be feasible to write an abc.adaptation package that provided the same thing?

Another big benefit of adapation, especially in larger programs or frameworks, is that they make object composition much more sane. When a Class heavily depends upon some external object, it's very nice to spell out what methods you are relying on that external object providing without needing to scrutinize the entire implementation of the class for all instances of where that external object has calls delegated to. Explicit is better than Implicit!

Ali said...

Thanks, this is a really good article. Are you probably suggesting "ABC and Interfaces are separately useful", and so probably shouldn't we be using both at the same time?

Al said...

Wouldn't an ABC be equivalently expressed as an interface if all methods are defined in ABC as abstract methods (using @abstractmethod)?

I am trying to grasp the concept of abstraction in python easily as I did in Java but I don't think I'm 100% there yet.

BTW, great post. I'll be sure to keep an eye out for more related posts.

-Alen

Justus said...

With an Interface, this question is a lot easier to ask. For a quick look, list(IVehicle) will give a complete list of all the attributes expected of a vehicle, as strings

This is what people always say about interfaces and in every non-trivial system I've ever seen it is a lie.

Just taking a random example from twisted that took me only a few seconds to find: FileDescriptor says it implements IReadWriteDescriptor. But if you actually call "doRead" you get a NotImplementedError.

That's a funny definition of "implements" and not one in usage outside of people who use interfaces.

You see the same thing throughout the Java I/O hierarchy: methods like "mark" appear near the top of the hierarchy but they don't make sense for all things so things that "implement the interface" actually throw exceptions when you try to call the method.

What does it mean to "implement the interface" if you have to read the code to know that it'll throw an exception every time you call the method?

How does that help me reason about intent?

Even in a strongly typed language like Java the interface doesn't help me reason very much about intent. Okay, MethodX takes a String and returns a List of Strings. Is the List supposed to be ordered? Is it mutable? Does the method block? Does it require the database to be configured prior to calling it? Is the String expected to be html escaped or is it not yet trustworthy? What are the preconditions, postconditions, and invariants?

In my experience, interfaces provide little in the way of helping me understand the intent of the designer.

Wheat said...

Al,

Yes, an ABC with only abstractmethods is equivalent to an interface - conceptually at least. Abstract Base Class and Interfaces are both have the same purpose - to provide specification and documentation, a way of formally and semi-formally describing a set of method names and signatures. There are differences though between how Python std lib ABCs and zope.interface Interfaces are implemented and how they are used. But it is for example possible to bridge between the two, for example by declaring that an ABC also provides an equivalent Interface.

>>> import collections
>>> from zope.interface import Interface, classImplements
>>> class ICollection(Interface):
... "Eqvuivalent to ABC Container"
... def __contains__(x):
... "Return True if contained, otherwise return False"
>>> classImplements(collections.Container, ICollection)
>>> class ConcreteContainer(collections.Container):
... def __contains__(self):
... pass
...
>>> c = ConcreteContainer()
>>> ICollection.providedBy(c)
True

glyph said...

@Justus:

This is what people always say about interfaces and in every non-trivial system I've ever seen it is a lie.

Erm... just regarding the particular point you're responding to:

>>> from zope.interface import Interface
>>> class IVehicle(Interface):
...  def drive(self):
...   "Drive the vehicle."
...  def stop(self):
...   "Stop the vehicle."
... 
>>> list(IVehicle)
['stop', 'drive']

I'm not making any claims about how useful this list of strings is, just that you can easily get it without junk in it such as "__dict__", "__module__", "__name__", "_abc_foometa_implementation_detail", etc.

Just taking a random example from twisted that took me only a few seconds to find: FileDescriptor says it implements IReadWriteDescriptor. But if you actually call "doRead" you get a NotImplementedError.

FileDescriptor is very old code. It predates the use of interfaces of any kind in Twisted, let alone the use of Zope Interface. IReadWriteDescriptor was an attempt to formalize what is expected of FileDescriptor subclasses.

Given that the interface does not describe such an exception, you can feel free to report a bug. It has most likely persisted because there is no convenient declaration like "subclassesShouldImplement"; you'd have to replace FileDescriptor's one implements() declaration with 15 or so on all of its subclasses. Nevertheless, this is the right thing to do.

I didn't say that interfaces were perfect, I just said they were useful :). It's helpful to know that instances of FileDescriptor provide IReadWriteDescriptor. (You won't find any instances of FileDescriptor in the wild, only its subclasses, and they won't raise NotImplementedError from doRead.)

djfroofy said...

@Justus

This is what people always say about interfaces and in every non-trivial system I've ever seen it is a lie.

I think this point could be applied to Python (and other dynamically typed languages) in general rather than just interfaces. In Java you cannot implement an interface and lie.

Nonetheless zope.interface provides machinery to ensure you don't accidentally lie in your unit test, etc., with verifyClass for implementors and verifyObject for providers of an interface.

Just taking a random example from twisted that took me only a few seconds to find: FileDescriptor says it implements IReadWriteDescriptor. But if you actually call "doRead" you get a NotImplementedError.

You might have a good point here if the exception were AttributeError. It's common practice in base classes that specify a method but don't implement them to raise NotImplementedError - which has nothing to do with zope.interface.

bigdog said...

glyph,
Good post. The catalysis folks used the term "precise abstraction" to define interfaces. That phrase has always helped me. FYI. Their book is a tough read, and could use some editing help, but it is free online. http://www.catalysis.org/books/ocf/index.htm

Justus said...

@djfroofy

I work in Java every day and so when I said it is a lie, I was most certainly including Java and its interfaces.

Anyone who thinks "you can implement an interface" where "implementing" is defined as "raising a Not Implemented exception" has immersed themselves in some kind of Orwellian mindset that I can't rationally argue with.

I call MethodX and it raises an exception every single time. Whether that is an AttributeError or a NotImplementedError is an implementation detail that I don't believe in distinguishing between.

Even worse, in languages like Java the try to make this kinds of things compile-time errors, you've turned it into a run-time one.

Jonathan said...

@glyph, @Justus, chiming in late in this, but isn't FileDescriptor exactly a good candidate for an ABC with an abstract doRead method?

It still implements the interface as it can't be instantiated directly and each non-abstract subclass would be required to implement the doRead method.

I guess my point here is that interfaces and ABCs are both useful and serve both for different purposes. Interfaces define the, well, interface, while ABC provide parts of the implementation while requiring subclasses to implement the missing (specific) part.