Promises, Promises

Promises are like babies: easy to make, hard to deliver. ~Anonymous

Every .NET interface, every COM interface, and every API is a promise. A contract of sorts. We all know that — it’s the point of an API. But, have we really thought about what that implies (especially in the context of object oriented code)?

Consider this interface (assume we’re using namespace System.Collections.Generic):

public interface IAlarm
{
    List<AlarmInfo> GetList(
              HostMachineInfo hostMachine);

    List<AlarmInfo> GetListByEnvironment(
               HostMachineInfo hostMachine,
               EnvironmentInfo environment);
}

What are some of the promises being made here?

The obvious ones:

  1. You can get the collection of AlarmInfo’s associated with a hostMachine,
  2. you can get the collection of AlarmInfo’s that are associated with a hostMachine in an environment.

Maybe a bit less obvious, but still promised by the interface:

  1. You can modify the set returned by those methods (e.g., you can Add, AddRange, Clear, and Remove to and from the list. You can also Reverse and Sort the lists in place)
  2. The return value says you can access individual elements by index.

More? How about something a bit more problematic, but required by the implementation of List<T>:

  1. The list is always in your your process space.
  2. The list is always completely loaded prior to it being returned to you.

In other words, this API promises that every implementation of this interface will satisfy all of those requirements (as well as the promises made by any base interfaces). It says that no matter how the outside world changes, no matter how big the data sets get, or how constrained the memory, no matter how distributed this might be, all of those promises will be satisfied.

Absolutely no future implementation of GetList(HostMachineInfo) can lazy-load AlarmInfo’s — no matter how big that result set gets. Furthermore, all future implementations must gracefully handle a user changing the contents of the list — even when an implementation might return a cached list, shared across other consumers who might be surprised by such a change.

With the current design, the only way we can add any of that new behavior in a later release is to find and modify all consuming code to use a new abstraction, instead of simply supplying a single new implementation of the interface.

In short, it makes the mistaken assumption that software doesn’t change but, by actively resisting change, makes the design brittle. Which all leads to big, painful, late and over-budget rewrites (or, read another way, we burn cash that could be spent on raises, bonuses and new gee-whiz tools).

So, how do we fix it? In existing/shipped code, it’s probably too late. More likely than not there is other code depending on the promises already made by the offending code. But in new designs, think about the minimum we can promise and still satisfy client code’s needs. If, for example, all consumers really need to do is iterate over the list of alarms, then consider an interface that returns IEnumerable<AlarmInfo> instead — iteration is the only thing IEnumerable<T> promises.

Careful what you promise — you’ll have to live with it a long time

Powered by WordPress with GimpStyle Theme design by Horacio Bella.
Entries and comments feeds. Valid XHTML and CSS.