Thursday, July 16, 2009

Scenario in terms of abstraction levels

Framework Design Guidelines promotes several principles but one of them is worth special attention:

Frameworks must be designed starting from a set of usage scenarios and code samples implementing these scenarios.

It allows framework designers to see their API from consumer’s perspective and the value the sight produces should not be underestimated. The framework designers are experts in subject area and thus they are cursed by this knowledge. That is why it is highly important to express mainline usage scenarios in terms that consumers are familiar with or can get easily to know (of course advanced scenarios may require much deeper knowledge in the subject area).

Unfortunately designers quite often rely on standard design methodologies (including object-oriented design methodologies) that are oriented more on maintainability than on usability of the API.

Recently I came across an API (or basically an approach promoted over the application) that is responsible for data persistence. Basically in common scenarios data persistence is used in both directions (read/write). From consumer’s perspective it looks like a single scenario. Suggested approach assumes that data is loaded through loaders and data changes are propagated back to data source using Unit of Work pattern:

// Represents some company information
internal class Company
{
    public int Id { get; set; }
    public int TrustLevel { get; set; }
}

// Loaders are responsible for data reading.
internal interface ICompanyLoader
{
    // Loads limitted set of data about companies that has highest 
    // profits.
    IEnumerable<Company> LoadTopPerformingCompanies(int count);
}

// Here goes Unit of Work pattern.
internal interface IUnitOfWork
{
    // Methods that are responsible for state changes tracking and writing out changes 
    // coordination.

    void RegisterNew(object obj);
    void RegisterDirty(object obj);
    void RegisterClean(object obj);
    void RegisterDeleted(object obj);
    void Commit();
    void Rollback();
}

Used like this:

// Loading data which is expressed in terms of business specific
var companies = companyLoader.LoadTopPerformingCompanies(10);

// Updating data which is expressed in terms of a selected 
// persistence layer implementation
foreach (var company in companies)
{
    company.TrustLevel++;
    unitOfWork.RegisterDirty(company);
}
unitOfWork.Commit();

So where is the problem? It seems working…

As mentioned from consumer’s perspective this is a single scenario. However the API expresses single scenario in terms of different abstraction levels:

  • Data loading is expressed in business specific terms (load top performing companies – companies that has profits for the last year more than the others)
  • Data updating is expressed in terms of state management (register object as dirty - updated)

This is the catch. In order for consumer to implement the scenario one needs to switch between abstractions which dramatically reduces productivity and on the other hand readability of the code.

In order to avoid mentioned problems a single level of abstraction must be used:

// It may be Table Data Gateway pattern 
interface ICompanyGateway
{
    // Loads limitted set of data about companies that has highest 
    // profits.
    IEnumerable<Company> LoadTopPerformingCompanies(int count);

    // Updates companies trust
    void IncreaseCompaniesTrust(IEnumerable<Company> companies);
}

...

// Loading and updating data which is expressed in terms of business specific
var companies = companyGateway.LoadTopPerformingCompanies(10);
companyGateway.IncreaseCompaniesTrust(companies);

Summary:

  • DO NOT mix different abstraction levels within single scenario
  • DO provide expected by the consumer terms required to implement the scenario

No comments: