About me

Michael L Perry

Improving Enterprises

Principal Consultant

@michaellperry

User login

Fixing an unhelpful API

Prove the correctness of a service. Here is how it started:

public void DoWorkWithOrderService()
{
    using (ITransaction currentTransaction = new AcidTransaction(_container))
    {
        OrderService.Transaction = currentTransaction;
        OrderService.DoWork();
        currentTransaction.Commit();
    }
}

It is only possible to prove the correctness of this code based on the caller (seen here), and not the order service itself. If the caller forgets to initialize the transaction property, if the caller forgets to create the transaction in a using statement, or if the caller forgets to create a new instance per thread, then bad things can happen.

Unhelpful_API The order service is an unhelpful API. When you approach it, you have to use caution. If you use it in the wrong way, bad things will happen. If you're lucky, it will throw an exception. If you are unlucky, you won't catch errors until you release to production.

An unhelpful API must be approached with fear.

Parameters are prerequisites
The first step in improving this unhelpful API is to ensure that a transaction exists before a service method is called. We can do this by turning the property into a parameter.

Before:

public class Service : IService
{
    public ITransaction Transaction
    {
        set;
        get;
    }
    
    public void DoWork() { }
}

After:

public class ServiceFactory : IServiceFactory
{
    public IService CreateService(ITransaction transaction)
    {
        return new Service(transaction);
    }
}

public class Service : IService
{
    private ITransaction _transaction;
    
    public Service(ITransaction transaction)
    {
        _transaction = transaction;
    }
    
    public void DoWork() { }
}

You cannot get a service from the factory without providing a transaction. Therefore, you cannot make any service calls before providing a transaction. Q.E.D.

Callbacks are prerequisites
We have proven that a transaction is provided before a service method is called, but we haven't yet proven that the transaction gets disposed. We can take one more step toward making this a helpful, provable API using a callback.

public class ServiceFactory : IServiceFactory
{
    private ITransactionFactory _transactionFactory;

    public void CallService(Action<IService> action)
    {
        using (ITransaction transaction =
            _transactionFactory.CreateTransaction())
        {
            action(new Service(transaction));
        }
    }
}

A transaction factory is injected into the service factory (via constructor not shown here), and that is used to create a transaction as needed. That creation happens within a using statement in the service factory, not in the calling code. There is no way for the caller to forget to do this. Thus we have proven that the transaction gets disposed.

In these two steps, we have transformed an unhelpful API into a helpful one. The unhelpful API offered many options, most of them wrong. The helpful API offers only the options that are correct. It guides the caller in its proper use. It cannot be used incorrectly.

An unhelpful API cannot be proven. It must be approached with fear. A helpful API can be proven. It can be approached with confidence.