This distinction between abstract and concrete interfaces is important when we talk about the base interface of a program module. The concrete interface, again, is what really makes things happen: cycles get used, memory gets allocated, electromagnetic radiation gets produced, an answer may even come back. The abstract interface is some abstraction that abstracts away most properties of the concrete interface, leaving a few, hopefully salient, ones.
But there are lots of abstractions over a module's concrete interface. Which one is "*the* abstract interface"? The traditional answer is "the documented abstraction". This answer comes from the black box abstraction game. You pick an abstraction, apply it to a module's concrete interface, and document that particular abstraction. This abstract interface is dubbed the "semantics" of the module.
But this assumption from the black box world, that one abstraction of the base interface will be able to capture everything that you should care about a module, is not going to hold up in the world of open implementations. We are going to want to talk about several different abstractions of the base interface of a module. There is still only one concrete base interface, but we will make use of several abstractions over it. Depending on how a module is used, different abstractions may be more important.
I believe, therefore, that there is a black box presumption behind one question we have been asking: Is this open implementation is powerful enough to change the "semantics"? The more direct, and interesting, question is: Which abstractions will not be affected by any changes made with an open implementation, and which are preserved?
For example, the CLOS MOP allows customization of multiple inheritance, but it always makes sure that a class inherits from all its super-classes, with nearer superclasses having precedence. If we treat this description of multiple inheritance as an abstraction of the full multiple inheritance details, then the CLOS MOP preserves how multiple inheritance works as seen through that abstraction. An advantage here is that the large number of programs that only depend on what the abstract description of multiple inheritance says will work correctly, no matter what is done with the MOP. Meanwhile, those programs that do depend on the details of multiple inheritance can use the MOP to request the version they need.
This illustrates that an open implementation ought to come with at least two abstractions of its base interface. One will be preserved across all changes made with the meta interface. This will lead to reuse, as discussed in a minute. The second will expose the consequences of "mapping dilemmas" but not of "mapping details". The first abstraction characterizes what is common across the space of implementations that an open implementation supports, while the second characterizes how good a job the open implementation has done of separating the mapping dilemmas from mere implementation details, and of presenting them. The meta interface can be naturally described in terms of how its affects are manifested in terms of the second abstraction. For layered protocols, we will probably want several abstractions of the base interface.
Reusing an open implementation of a module requires "only" that it be possible to adjust the module to meet the requirements of the new client. It doesn't require there to be anything in common between the two uses of the module; it doesn't require "separation of concerns"; it only requires that the module be flexible enough. If a module came with a switch on the side that took it from being a numerical analysis package to being a window system, it would be a reusable open implementation in this sense.
What is interesting about open implementations is that they facilitate client reuse. An open implementation allows this when the first abstraction of its base interface, the one that is preserved across changes to the implementation, is informative enough to allow client software to be written to it. Since the client software is then valid across variations of the open implementation, it can be reused with different variations of the underlying implementation. This, in turn, is what allows performance issues to be dealt with separately; once the client code is written, the open implementation can be adjusted to get the desired performance, and the client code "reused" across the variations of the open implementation.
What has happened here is that the meta inferface of the original open implementation can now be used to adjust the performance of the combination of the client and the open implementation. In other words, the combination of the client code and the original open implementation has become a new, larger, open implementation, with the meta inferface of the original open implementation also being the meta interface of the new open implementation. This is how imperative open implementations get built in the first place; some of the code of the open implementation is a client of the replaceable pieces.
In summary, the client code of an open implementation can depend on a particular setting of the open implementation, or can work with a range of settings, or, more usually, somewhere in between. The wider range of implementation settings the client code can work with, the more open the resulting, larger system, and, presumably, the more reusable. This, in turn, depends on there being two abstractions of the base interface of the original open system, one abstraction which is preserved across changes to the open implementation so that the client code can remain valid, and one abstraction which captures variations across changes to the open implementation, so that properties of the combined system can be adjusted.
Back to Alphabetical List of Responses
(Last Revised October 1994)