Pages

Wednesday, 2 February 2011

Conceptual Abstraction, and Technical

Dear Junior

I think there is an important distinction between solving a technical problem and solving the conceptual problem. And this distinction has a profound effect on what the code will look like. 

Let me take an example. Imagine that you have some class that keeps track of the score during a football game. It might look something like this.

class FootballGame() {
  int home = 0;
  int guest  = 0;
  void homeGoal() { home++; }
  // void guestGoal()
  // String score () , e g "4-3"
  // etc
}

Now you want to add goal logging, that logs each goal to stdout. This might change homeGoal to 

void homeGoal() {
  home ++;
  System.out.println("home goal");
}

Next reasonable change can be that you want the logging to be configurable so that you can log to a file instead of standard out.

This can be done from a technical perspective or a conceptual perspective. Let's have a look at each to see why I prefer doing it the conceptual route.

Technical abstraction

The ambition of technical abstraction is to make yourself independent of the particular implementation you have at the moment. The "how" is here "to log goals to standard out", the "what" is "to log goals somewhere". We want the solution less dependent on the "how".

We refactor homeGoal to use a configurable output stream instead of hard-coding. The code inside FootballGame might turn out something along 

FootballGame(PrintStream out) {
  this.out = out;
}
void homeGoal() {
  home++;
  out.println("home goal");
}

This is a perfectly acceptable solution from a technical perspective. It uses dependency injection, so we can easily configure it with stdout, a stream to a log file or sending the data remote through a socket. For testing we can use a PrintStream backed with a ByteArrayOutputStream or we can mock the PrintString when doing TDD. The unit test for driving that step might look like 

    @Test
    public void shouldLogHomeGoal() {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        FootballGame game = new FootballGame(new PrintStream(out));
        game.homeGoal();
        Assert.assertEquals("home goal", out.toString());
    }

(Arguably we also test some of the functionality of ByteArrayOutputStream - but as that is a standard API class I am pretty comfortable with that.)

Typically technical abstraction can be done by using the standard APIs given by the platform (OutputStream, String, List, File etc).

The weakness of this approach is that the code/solution/architecture gets riddled with technical constructs, and after a while it is hard to remember exactly what purpose that particular OutputStream was for in that particular place.

Conceptual abstraction

The ambition of the conceptual abstraction is also to make yourself independent of the particular implementation. However, we look at the situation a little bit different and thus set of in a slightly different directions. The "how" is here "to log goals to standard out", but the "what" becomes "to log goals".

We refactor homeGoal to use a GoalLogger.

FootballGame(GoalLogger goallogger) {
    this.goallogger = goallogger;
}
void homeGoal() {
    home ++;
    goallogger.log("home goal");
}
interface GoalLogger {
    void log(String msg);
}
This solution does also use dependency injection, it can also be configured to log to standard out, to a log file, or cross network. For TDD we will probably using mocking in the driving testcase - which is really easy as we have an explicit interface.

    @Test
    public void shouldLogHomeGoal() {
        GameLogger gameLogger = Mockito.mock(GameLogger.class);
        FootballGame game = new FootballGame(gameLogger);
        game.homeGoal();
        verify(gameLogger).log("home goal");
    }
The difference is that we will need explicit classes. E g for stdout-logging we will use StdoutGoalLogger.
class StdoutGoalLogger implements GoalLogger {
    void log(String s) { System.out.println(s); }
}
For other purposes we will use a FileGoalLogger or a NetworkGoalLogger.

Obviousness is the Difference

The main difference to me is that it is totally obvious what the intent of the GoalLogger is, as apart from the generic PrintStream in the example of technical abstraction. 

Granted, there are more code in the conceptual abstraction than in the technical abstraction - but I would still prefer the former if it came to maintenance or further development.

But the real difference comes in the next step. We will probably notice that there are two calls that are repeated:
goallogger.log("home goal"); 
goallogger.log("guest goal");
Now that we have the GoalLogger abstraction we can let it evolve.
interface GoalLogger {
    void homeGoal();
    void guestGoal();
}
Note that we have also removed the now obsolete "void log(String msg)", and this is what makes this GoalLogger distinct from some or any general-purpose logger.


Had we done the technical abstraction of working with an PrintStream, it would not be as obvious what could be done next. What I have seen most of the times is some kind of util class emerging containing static "helper methods" like GoalUtil.logHomeGoal(PrintStream), helper methods that are sometimes used, sometimes not. This in turn leads to similar (or duplicated) code in several location and an increase in code line count.

In my experience the small increase in line count for the conceptual abstraction is far better than the "ripple" increase in line count that the technical abstraction cause over time. 

The bottom line. Focusing on the conceptual problem at hand gives a better codebase in the long run than focusing on solving the technical problem does. It might give more code but it need not to take much more effort.

Yours




   Dan