A few weeks ago, I posted some details about the cognitive dimensions framework that we use at Microsoft when considering API usability. Up to now, we've been using it primarily to describe the results of studies done on APIs in our labs. What I'd really like to see though is different API teams being able to use the framework as a means to design an API, rather than evaluate it. Over the last couple of weeks I've been writing up a document that guides API teams in evaluating whether or not their API design is progressing in the direction they would like it to, with respect to the framework. I thought I would share this with you. I'm hoping that you'll be able to provide good feedback on this approach so that I know whether or not this really is useful for teams designing an API.

The basic idea is to start the design of an API by describing the type of developer that the API is designed for in terms of the cognitive dimensions framework. For example, your customer might be a developer that prefers APIs that expose a set of aggregate components, that prefers an API that supports a bottom up learning style etc etc. Then, for each scenario that the API is designed to support, write the code that the developer will have to write using the API to implement that scenario. Note that you can do this before you're anywhere near ready to start implementing your API. Just write out the code in a text editor - what you are doing is saying 'When the API is ready, here is the code that we would like developers to write when tackling this scenario'. It's sort of like designing a UI for a product before implementing the product. Designing an API and a GUI in this manner (from the perspective of the user) means that it is less likely that the implementation details of the product will surface in either the GUI or the API.

Once you have a set of code samples, you can then start to evaluate them in terms of the framework.

Let's begin by looking at how we might evaluate the abstraction level dimension. We start by doing a task analysis of each of the scenarios.

For each user goal that the API supports, describe the tasks that the user has to implement to accomplish that goal. For example, in the System.IO namespace one goal might be to append a line of text to a file. For such a goal, the list of tasks might be:

 

  • Open the file with a given name
    • Create the file if it does not already exist
  • Write a line of text to the file
  • Close the file 

Note that a task analysis for an API describes the different actions that a developer would expect to have to accomplish. It does not describe how the API supports these goals. For example, one API might create a file automatically if you attempt to open a file that does not exist, another API might force the developer to explicitly create the file. They both support the same action from the developer's perspective, they just support it differently.

 

For each task that you describe in each goal, list the different API components that are involved in accomplishing that task. For example, to open the file in append mode, you might create an instance of the StreamWriter class and set the append parameter to true in the StreamWriter constructor. The StreamWriter constructor will create the file if it does not exist so there is no extra action to be taken to create the file. To write to the file, you would then call StreamWriter.Write or StreamWriter.WriteLine using the instance of the System.IO.StreamWriter class returned by the constructor. You would then finish off by calling StreamWriter.Close.

 

Do a similar task analysis for each goal that the API supports. For example, an additional goal for the System.IO classes might be to read a line of text from a file. In this case, the tasks would require the use of the StreamReader class in order to open, read from and close the text file.

 

Having described the different tasks that the user has to accomplish to achieve each goal and the different components that the user has to use to implement each task, you can now describe the nature of the components in the following terms.

 

  • If individual tasks require two or more components to be used in conjunction, the components are considered primitives. In other words, individual components exposed by the API do not map on to unique user tasks.
  • If each individual goal requires the use of only one component, but the set of goals requires different components, the components are described as factored components.
  • If the set of goals could all be accomplished with the same set of components, the components are described as aggregate components.

Given the above definition, the System.IO classes are best described as factored components since each goal requires the use of different components (StreamWriter to write to a file and StreamReader to read from a file). Each class is factored to a particular goal, but different components are required to achieve different goals.

Do the above analysis for each scenario that your API supports. If each scenario that your API supports is likely to be accomplished by the same type of user, you should attempt to ensure that each scenario exposes the same type of abstractions. If each scenario that your API supports is likely to be accomplished by different users, you should attempt to ensure that each scenario exposes the type of abstractions that those users are most likely to be successful and comfortable with.

Given the above analysis, if the target customer for System.IO prefers to work with factored components, this indicates that the design will satisfy them with respect to this dimension.

We'd complete the analysis by looking at the other dimensions in turn. I'll describe how to analyze the remaining dimensions in later postings.