Limit your abstractions

While trying to find a better alternative to our “pass-the-ball” architecture (webform ->code behind-> presenter->controller) for my app, I have stumbled upon interesting bite-sized series Limit your abstractions by Ayende.

It basically starts with a code from ndddsample and shows what is wrong (in his opinion) with it (events, too much useless abstraction).

Series

  1. Analyzing a DDD application – The abstration is non-abstracted abstraction. Basically only extracted interfaces.
  2. Application Events–the wrong way
  3. Application Events–what about change? – What if we have new state, e.g. lost cargo
  4. Application Events–Proposed Solution #1
  5. Reflections on the Interface Segregation Principle
  6. Application Events–Proposed Solution #2–Cohesion
  7. Application Events–event processing and RX
  8. You only get six to a dozen in the entire app
  9. Commands vs. Tasks, did you forget the workflow?
  10. All cookies looks the same to the cookie cutter
  11. So what is the whole big deal about?
  12. Refactoring toward reduced abstractions
  13. The key is in the infrastructure…
  14. And how do you handle testing?

Events

public override void InspectCargo(TrackingId trackingId)
{
  Validate.NotNull(trackingId, "Tracking ID is required");

  Cargo cargo = cargoRepository.Find(trackingId);
  if (cargo == null)
  {
    logger.Warn("Can't inspect non-existing cargo " + trackingId);
    return;
  }

  HandlingHistory handlingHistory = handlingEventRepository.LookupHandlingHistoryOfCargo(trackingId);

  cargo.DeriveDeliveryProgress(handlingHistory);

  if (cargo.Delivery.Misdirected)
  {
    applicationEvents.CargoWasMisdirected(cargo);
  }

  if (cargo.Delivery.UnloadedAtDestination)
  {
    applicationEvents.CargoHasArrived(cargo);
  }
  cargoRepository.Store(cargo);
}

This is actual business method that does business logic method. It violates Single Responsiblity Principle (it looks up the delivery history and dispatches events) and Open Closed Principle (if we add or change cargo state, e.g. cargo is lost, we have to modify the class).

There are of course many possible solutions to event handling and dispatching, some are discussed. I didn’t know about Reactive Extensions, rather nice.

Non-abstracted abstraction

According to Ayende, the code should have a very limited amount (<10) of abstractions, he proposes following abstractions are good enough for most projects.

  1. Controllers
  2. Views
  3. Entities
  4. Commands
  5. Tasks
  6. Events
  7. Queries

Creating an abstraction always has a cost, sometimes small, sometimes large, see Abstract Factory Factory Façade Factory. Use your abstractions carefully.

My notes

Definitely worth reading, but I wonder how does proposed reduced solution work in a real project with more complex operations and larger teams.

Basically, he puts the code into a self-container Command class that contains all the logic and calls it from the MVC action. The queries are also self contained classes that get their result using Query method of the Command.

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Register(string originUnlocode, string destinationUnlocode, DateTime arrivalDeadline)
{
    var trackingId = ExecuteCommand(new RegisterCargo
    {
        OriginCode = originUnlocode,
        DestinationCode = destinationUnlocode,
        ArrivalDeadline = arrivalDeadline
    });

    return RedirectToAction(ShowActionName, new RouteValueDictionary(new { trackingId }));
}
public abstract class Command
{
    public IDocumentSession Session { get; set; }
    public abstract void Execute();

    protected TResult Query<TResult>(Query<TResult> query);
}

public abstract class Command<T> : Command
{
    public T Result { get; protected set; }
}

public class RegisterCargo : Command<string>
{
    public override void Execute()
    {
        var origin = Session.Load<Location>(OriginCode);
        var destination = Session.Load<Location>(DestinationCode);

        var trackingId = Query(new NextTrackingIdQuery());

        var routeSpecification = new RouteSpecification(origin, destination, ArrivalDeadline);
        var cargo = new Cargo(trackingId, routeSpecification);
        Session.Save(cargo);

        Result = trackingId;
    }

    public string OriginCode { get; set; }
    public string DestinationCode { get; set; }
    public DateTime ArrivalDeadline { get; set; }
}

In the end, he uses hand-coded mocking which I find rather distasteful

public void ExecuteCommand(Command cmd)
{
  if (AlternativeExecuteCommand!= null)
    AlternativeExecuteCommand(cmd);
  else
    Default_ExecuteCommand(cmd);
}

It seems much easier and maintainable just to create ICommandExecutor or even virtual method that can be overriden.