Deferring all design decitions when defining contracts between software components can easily lead to an actual contract consisting of a hodgepodge of improvised APIs and data formats, informal conventions and opportunistic usage of observed existing behavior — little of it documented.
When designing contracts between software components that are not always upgraded in tandem, it makes sense have a strategy to allow the contract to evolve. A software component is considered backwards compatible if it can successfully use older versions of a contract, and is considered forwards compatible if it can successfully use versions of a contract newer than the version the software component was specifically written to use.
A contract between software components can take many forms, including:
If you don't take care when designing contracts, you can end up with a contract that will be difficult to extend in the future. This seems to be well understood by most programmers. However, how programmers act on this knowledge when designing contracts varies.
There is a particular style of “keeping ones options open” that I've seen lead to a lot of problem. This style involves not being specific about anything, and deferring the design of the actual contract.
The following Python code shows some examples of interfaces implemented in this style.
# This is horrid code. Don't ever write code like this unless you are
# demonstrating the dangers of this style of programming.
class IFile(object):
def doFileOperation(self, operation, arguments):
"""
Perform an action on a the file represented by this object.
Example:
myfile.doFileOperation("rename", ["newname.txt"])
@type operation: str
@param operation: name of file operation to perform
@type arguments: list
@param arguments: list of arguments used in operation
"""
pass
class ICommunicator(object):
"""
Communicates with external components.
Example:
mycomm.set_property('User-Agent', 'Smart fetcher')
mycomm.set_property('url', 'http://www.example.com/page.html')
mycomm.set_property('http_method', 'GET')
mycomm.set_property('connected', 'True')
webpage = mycomm.get_property('content')
"""
def set_property(name, value):
"""
@type name: str
@param name: name of the field to set
@type value: str
@param value: the value to set on the field
"""
pass
class IExecutor(object):
def performActions(self, xmlaction):
"""
@type xmlaction: str
@param xmlaction: XML describing the action to perform
@rtype: str or None
@return: Returns an XML containing the result of performing the
action if the action produces a result, or None otherwise.
"""
pass
With these APIs anything is possible — the possibilities are endless. Much like a blank slate.
On the surface these APIs might even appear properly documented, but its usage is really only described through some flimsy examples. The true contracts provided by APIs like this are often never properly documented. Generic entry points has its uses, but demands a greater discipline in documenting the contract, since the contract isn't explicitly denoted in the code. Unfortunately I've seen people design APIs like this, justifying their reluctance to make any decisions by the need to allow for forward and backwards compatibility.
The actual contract often ends up as hodgepodge of improvised APIs and data formats, informal conventions and opportunistic usage of observed existing behavior — little of it documented. Obvious drawbacks of this are:
So if defining generic APIs are not the solution to backwards and forwards compatibility, then what is?