Friday, June 19, 2009

Commonality that is worth abstractions

Unfortunately desire to achieve reusability not always leads to expected results. It can bring you development effort savings through reusing existent components or it can lead to negative consequences (such as proliferation of unnecessary classes, decreased discoverability and readability).

Reusability is often based on abstractions. Abstraction captures some commonality between its concrete implementations. But abstractions being very powerful are dangerous as well.

Consider situation where a piece of work is generalized and represented through Command pattern. Basically task (commonality) resulted in command (abstraction).

interface ICommand
{
  // Performs command's task
  void Do(object parameter);
}

Is this commonality worth the abstraction? It depends on the rationale behind.

It may result from desire to factor out functionality that belongs to a particular category (for example, business rules) and through implementing common interface outline membership in a category (and of course it may be expected that through abstraction they will be reusable in some other scenarios). However with this rationale at hand it is most likely that code that creates command also executes it (because it is the only scenario for now – more other scenarios are coming :-)).

// Holds arguments for SomeCommand
class Arguments
{
  public string Arg1 { get; set; }
  public int Arg2 { get; set; }
}

class SomeCommand : ICommand
{
  public void Do(object parameter)
  {
      var args = (Arguments) parameter;
      // Do something with arguments
  }
}
var command = new SomeCommand();
var args = new Arguments {Arg1 = "value", Arg2 = 10};
command.Do(args);

Unfortunately this rationale doesn’t justify introduction of command abstraction. It forces calling code to consume general purpose opaque contract despite the fact that calling code knows exactly what must be done. It decreases readability in comparison with direct call to a method with comprehensive name and may result in proliferation of unnecessary classes which are basically arguments containers. Thus the abstraction introduced drawbacks with no benefits.

It is as important to resist premature abstractions as the abstraction itself. Every abstraction even not used once is not free - it has maintenance costs attached since abstractions are considered to be a contract between the author and consumers that must be fulfiled.

Anytime you introduce a new abstraction a concrete implementation of the abstraction and consuming API must be provided as well. It is hard to see whether abstraction designed correctly unless there is a use case implemented that uses the abstraction.

Consider when calling code cannot execute commands itself but rather commands must executed in order of appearance (through a queue).

// Executes commands in order
interface CommandQueue
{
  // Enqueues command with parameter that is known
  // at the time of enqueue
  void Enqueue(ICommand command, object parameter);

  // Enqueues command with function that will provide
  // parameter on demand
  void Enqueue(ICommand command, Func<object> parameter);
}

In this case abstraction introduction is justified. The command queue conforms to Open/Closed Principle - it is easily extensible with new types of commands because it relies on Command abstraction.

If classes share only a common interface (not behavior), then they should inherit from a common base class or interface only if they will be used polymorphically.

Summary:

  • AVOID premature abstractions
  • DO carefully justify introduction of new abstractions
  • DO provide concrete implementation and consuming API for every abstraction introduced

No comments: