Using the Cognitive Dimensions – Penetrability


Penetrability refers to the extent to which a developer must understand the underlying implementation details of an API and the extent to which the developer is able to understand those details. For example, an API that is basically a very thin wrapper over some lower level API probably requires that anyone using that API understands the low level implementation details before being able to use the API successfully.


 


Sometimes problems that users experience in an API are due to the extent to which they need to understand lower level details and the support that the API provides for gaining that understanding. Depending on the user, the necessary changes to an API in these cases may require changing the abstraction level by adding new higher level abstractions on top of the implementation details so that there is absolutely no need to understand those details. Or, at the other extreme, it may require reducing the abstraction level so that developers can more easily understand the implementation details.


 


In this posting, I’d like to try to explain how you might evaluate your own API to determine how much of the underlying implementation your users will need to know. Refer to the first article in this series for a description of how to do a task analysis of your API and then read on…


 


For each goal that the user might attempt to accomplish with the API, describe the extent to which the user needs to understand the implementation details of the API and the extent to which these details are exposed to the user.


 


For example, in the System.IO namespace, one user goal is to write code to append text to a file. In order to write code that uses the StreamWriter class effectively, the user needs to know:


 



  • If the class is thread safe or not by default and the extent to which this can be configured.

  • The character encoding that the class supports by default, and the extent to which this can be configured.

Users can discover from the documentation that the class is not thread safe and can find more details about the character encoding that the class supports. To create a thread safe wrapper around a StreamWriter instance the base class of StreamWriter, TextWriter is used. TextWriter exposes a Synchronize method that creates a thread safe wrapper around any class that derives from TextWriter. Users thus need to find out whether or not StreamWriter derives from TextWriter to determine whether or not they can use TextWriter.Synchronize to create a thread safe wrapper around StreamWriter.


 


Thus in this example, users need to know the context in which the class is defined and some of the working details of the class, particularly those details that are set by default.


 


There are other ways to expose API details to the user. For example, the API source code provides most of the detail that a user might need, but at the cost of having to parse and understand the code. Naming conventions used in the API can also help expose some of the details. For example, class, parameter, property, method and variable names might indicate to the user their purpose, scope, visibility etc. When inspecting these members in a debugger, such naming conventions can help users identify those members that they have direct access and control over from the internal working details of the classes they are inspecting in the debugger. It is important to figure out the details of the API that users need to know in order to be able to design the API in such a way that communicates these details clearly.


 


Penetrability can be defined in the following terms.


 



  • If the API exposes only enough information to allow the user to distinguish between different methods and classes and if this is the only information that the user needs to attend to, the API provides and requires only minimal penetrability, or a snapshot view into the details of the API.

  • If the API exposes enough information to allow the user to understand the context or scope of the particular part of the API that the user is working with, and if this is the only information that the user needs to attend to, the API provides and requires a context driven view of the details of the API.

  • If the API exposes enough information to allow the user to understand the intricate working details of the API and if the user must attend to this information, the API provides and requires an expansive view of the details of the API.

Note that in the above definition penetrability is defined both in terms of the requirements from the perspective of the user working with the API and the extent to which the API provides support for gaining that perspective. In other words, describing penetrability from the perspective of the API describes the extent to which the developer must understand implementation details. Describing penetrability from the perspective of the developer describes the extent to which the developer demands to be able to understand the underlying implementation details. The developer might not require that amount of information in order to be able to use the API effectively, but they might want that amount of information in order to be able to feel comfortable using that API.


 

Having defined penetrability in the above terms, the System.IO namespace supports and requires a context driven view of the API.

Comments (6)

  1. So the Avalon API would then require an "expansive" view, correct? The most unusable aspect of that API is the unnecessary exposure of low-level implementation details, imho.

    Usually we just bag these problems all under the title "unnecessary complexity". Sometimes people break it down, and say something works at the "wrong level of abstraction." Your distinctions are very useful. However, you do not seem to have a term strictly categorizing complexity that is unnecessary.

    For example, a typical design mistake is to expose several low-level details of an API implementation, simply to avoid the much harder task of figuring out an appropriate set of abstractions for the user. The usual argument is "the user needs that because of this one scenario," or more often, "performance."

    But there is more than one way to skin a cat. Exposing a low-level detail may fix one problem, or solve a performance problem. But now the user is permanantly exposed to a lower level of abstraction, and the supplier must work that much harder to support upward compatability of the low-level detail. Changing the internal implementaion is no longer easy.

    The better solution is often to reconsider the API at a higher level, to enable the scenario, or solve the performance problem, without exposing a low-level detail. This is much harder and developers generally prefer to skip this step, in part because it implicitly invalidates the original "architecture" (ego may be involved).

    So Steve, are there terms to describe this very typical design problem? I believe it causes much unnecessary complexity in modern APIs.

  2. Steven Clarke says:

    You’re right Frank, in saying that the cognitive dimensions don’t describe the ‘unnecessary complexity’ that you talk about. Measuring the penetrability of an API on it’s own doesn’t really tell you much with regards to whether or not the snapshot or expansive view required by the API is reasonable.

    Determining what works well is done by comparing the measurement of an API with the cognitive dimensions profile of the API user. When you look at the penetrability required to be able to use an API with that expected by a developer, you can use the cognitive dimensions framework to describe why the API is unnecessarily complex or too simple (or just right).

  3. Frank Hileman says:

    This logic doesn’t sit well with me. Simply because a user can handle the complexity, does not mean the complexity is necessary. I am talking about another type of "correctness," which does not seem to be quantified or recognized in your model.

    An API may provide types A and B, with numerous members on each. Or, after redesign, may provide only type C, with less members and less complexity. With C the user can accomplish everything needed; A and B were unnecessarily complex.

    In API design, simplicity is generally considered one of the most important factors. Perhaps only experience in API design and the specific domain can guide one toward simplicity.

    But to quantify simplicity, we must measure both functionality and complexity, and look at the ratio between them. A design with a high ratio is considered superior to a design with a low ratio. To consider a design by itself, without looking at alternatives, does not help us determine a reasonable ratio.

    Any consideration of the design must include all the types in the domain area — looking at a class in isolation does not help.

    A design with a good ratio, as measured above, will tend to be more usable, because there is less mental burden, and less to learn. The books are not as thick.

    Any developer can tell you their number one problem today is learning new APIs: they are proliferating rapidly, and a developer typically must be familiar with many at the same time. Since MS is responsible for much of this proliferation, API simplicity should be a top priority.

  4. Steven Clarke says:

    I should have been clearer in my response. Complexity of an API is a combination of the different factors that the cognitive dimensions framework measures. I have seen many APIs that are considered too complex by users because of the abstractions they expose (your example above), or the learning styles required to understand the API, or the working framework the API imposes, or because of any one of the other dimensions and combinations thereof.

    What the cognitive dimensions framework helps expose is the reason for the complexity in the API. It helps the API designers understand in more detail why the API was too complex and what they can do to reduce the complexity. For example, one answer might be to change the abstractions so that instead of exposing a collection of primitives, a collection of factored components is exposed instead. Or, it might be that the work-step unit for many tasks is too long. In this case, the API could take care of some of the programming work required, for example automating more of the initialization required to use the API instead of forcing all that work on the user.

    Additionally, all measurements have to be taken with respect to the particular user being targeted by the developer. Different users have different requirements and what might be considered complex for one user could be just right for another.

    I absolutely agree with your comment about looking at alternative design ideas. It’s a great point. It should be part of the design process to decribe the scenarios that the API should support and then to show multiple ways in which the API could support these scenarios. This would be a great way to identify the way that diferent design decisions impact the complexity of the API.