Halfway on Main: Thoughts on Clean Architecture

Uncle Bob ignores his own advice when considering the "Main" component, but we can improve on his thoughts and learn from them.

Chapter 26 is a short section of Uncle Bob Martin's classic, Clean Architecture. It discusses the necessary evil of creating a Main component which handles the dirty work and initializes the rest of the program. This component necessarily breaks rules to get the show running and provide an interface with the non-clean world which is our reality. The Main component takes care of setting up globals and may enter the program into an infinite loop to keep it running forever.

Uncle Bob gives a lengthy yet incomplete example of a Main class component for a hypothetical game of "Hunt the Wumpus". The game is a text-based somewhat-roguelite dungeon-crawler in which you seek out the Wumpus and avoid traps. A simple game concept, well within the wheelhouse of a first-year computer science student, and Uncle Bob's code looks the part. For an otherwise insightful book about how to separate concerns, Martin seems to give up when it comes to this component, relegating it to be "the dirtiest of all the dirty components" without any effort to find a better way. The example class he presents is needlessly brittle and repetitive.

Here is that entire Main class, as presented in the book. Note that the book contains the comment at the end (much code removed...), it wasn't added here.

public class Main implements HtwMessageReceiver {
  private static HuntTheWumpus game;
  private static int hitPoints = 10;
  private static final List<String> caverns = new ArrayList<>();
  private static final String[] environments = new String[]{
    "bright",
    "humid",
    "dry",
    "creepy",
    "ugly",
    "foggy",
    "hot",
    "cold",
    "drafty",
    "dreadful"
  };

  private static final String[] shapes = new String[] {
    "round",
    "square",
    "oval",
    "irregular",
    "long",
    "craggy",
    "rough",
    "tall",
    "narrow"
  };

  private static final String[] cavernTypes = new String[] {
    "cavern",
    "room",
    "chamber",
    "catacomb",
    "crevasse",
    "cell",
    "tunnel",
    "passageway",
    "hall",
    "expanse"
  };

  private static final String[] adornments = new String[] {
    "smelling of sulfur",
    "with engravings on the walls",
    "with a bumpy floor",
    "",
    "littered with garbage",
    "spattered with guano",
    "with piles of Wumpus droppings",
    "with bones scattered around",
    "with a corpse on the floor",
    "that seems to vibrate",
    "that feels stuffy",
    "that fills you with dread"
  };

  public static void main(String[] args) throws IOException {
    game = HtwFactory.makeGame("htw.game.HuntTheWumpusFacade", new Main());
    createMap();
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    game.makeRestCommand().execute();
    while (true) {
      System.out.println(game.getPlayerCavern());
      System.out.println("Health: " + hitPoints + " arrows: " + game.getQuiver());
      HuntTheWumpus.Command c = game.makeRestCommand();
      System.out.println(">");
      String command = br.readLine();
      if (command.equalsIgnoreCase("e"))
        c = game.makeMoveCommand(EAST);
      else if (command.equalsIgnoreCase("w"))
        c = game.makeMoveCommand(WEST);
      else if (command.equalsIgnoreCase("n"))
        c = game.makeMoveCommand(NORTH);
      else if (command.equalsIgnoreCase("s"))
        c = game.makeMoveCommand(SOUTH);
      else if (command.equalsIgnoreCase("r"))
        c = game.makeRestCommand();
      else if (command.equalsIgnoreCase("sw"))
        c = game.makeShootCommand(WEST);
      else if (command.equalsIgnoreCase("se"))
        c = game.makeShootCommand(EAST);
      else if (command.equalsIgnoreCase("sn"))
        c = game.makeShootCommand(NORTH);
      else if (command.equalsIgnoreCase("ss"))
        c = game.makeShootCommand(SOUTH);
      else if (command.equalsIgnoreCase("q"))
        return;
      c.execute();
    }
  }

  private static void createMap() {
    int nCaverns = (int) (Math.random() * 30.0 + 10.0);
    while (nCaverns-- > 0)
      caverns.add(makeName());

    for (String cavern : caverns) {
      maybeConnectCavern(cavern, NORTH);
      maybeConnectCavern(cavern, SOUTH);
      maybeConnectCavern(cavern, EAST);
      maybeConnectCavern(cavern, WEST);
    }

    String playerCavern = anyCavern();
    game.setPlayerCavern(playerCavern);
    game.setWumpusCavern(anyOther(playerCavern));
    game.addBatCavern(anyOther(playerCavern));
    game.addBatCavern(anyOther(playerCavern));
    game.addBatCavern(anyOther(playerCavern));
    game.addPitCavern(anyOther(playerCavern));
    game.addPitCavern(anyOther(playerCavern));
    game.addPitCavern(anyOther(playerCavern));
    game.setQuiver(5);
  }

  // much code removed...
}

User command parsing

Let's start with the low-hanging fruit: repetitive statements. The main method contains the primary game loop, which runs forever until the user enters "q". Most of this is fine, but the long block of else if's are not only difficult to read, they're needlessly inefficient. We must test input against every statement in sequence until one turns up true or there are no statements left to test. Further, if the user enters a command which matches none of the conditions (necessitating a complete run-through of them all), the game executes a "rest" command, declared outside the set of conditions, which could easily come back to bite an unsuspecting developer in the future.

Whenever there is a set of three or more distinct conditions to test, it's almost always a better bet to use a switch, case or best of all (if the language supports it), pattern matching. The game loop cleans up a bit if we use this advice. We can also take advantage of the fact that when comparing strings, the switch statement acts as if we're calling the String.equals method, so as long as we convert the command to lower case, it'll act identically to calling String.equalsIgnoreCase repeatedly.

while (true) {
  System.out.println(game.getPlayerCavern());
  System.out.println("Health: " + hitPoints + " arrows: " + game.getQuiver());
  System.out.println(">");

  String command = br.readLine();
  HuntTheWumpus.Command c;
  switch (command.toLowerCase()) {
    case "e": c = game.makeMoveCommand(EAST);
      break;
    case "w": c = game.makeMoveCommand(WEST);
      break;
    case "n": c = game.makeMoveCommand(NORTH);
      break;
    case "s": c = game.makeMoveCommand(SOUTH);
      break;
    case "se": c = game.makeShootCommand(EAST);
      break;
    case "sw": c = game.makeShootCommand(WEST);
      break;
    case "sn": c = game.makeShootCommand(NORTH);
      break;
    case "ss": c = game.makeShootCommand(SOUTH);
      break;
    case "q": return;
    default: c = game.makeRestCommand();
  }
  c.execute();
}

These conditions are now, in my opinion, more readable, and operate in O(1) time. Additionally, the command parsing is visually separated from the user output (which is a sort of UI).

Rather than making the command matches here, the architecture would benefit even more from taking Martin's advice from his own book, and moving the UI elements to their own component. If we wish to implement a more complex UI in the future, even a GUI, only the dedicated component will need to change considerably. In the shorter-term, perhaps we'll want to add an explicit "i" command which prints this out. It would be nice to separate this concern from the "dirty" Main component.

The main game loop could still live in a clean main method, but we should restrict it to only getting the command and executing it. This leaves the looping action at this, the lowest "dirtiest" level, while abstracting away the complications of interpreting user input. Here's how well we clean it up by separating the concerns via abstraction:

while (true) {
  GameUI.displayUserStatus();
  HuntTheWumpus.Command c = GameUI.getUserCommand();
  c.execute();
}

Map generation

The createMap method may indeed be at home in the Main class, but surely we can clean it up. Uncle Bob leaves needless repetition in the same method where he used a loop to avoid it.

First, let's look at the cavern connection block, which uses some not-printed method to dynamically generate connections between caverns. This isn't so bad, but a nested loop could abstract it a little better, especially if we wanted to change the cavern geometry in the future (I'm thinking hexagons, which are the bestagons). We'll make the further improvement of moving our directions into an enum, which we'll simply call Direction.

for (String cavern : caverns) {
  for (Direction direction : Direction.values()) {
    maybeConnectCavern(cavern, direction);
  }
}

Next, we have a block which spawns the characters, some bats and some pits into presumably random unique caverns. Placing the player and the Wumpus are single statements and probably always will be, but we don't need to repeat ourselves thrice for bats and pits each. Let's also rename the anyOther method to anyOtherCavern to reduce ambiguity.

Along with populating caverns, our map generation block gives the player a quiver of arrows? This has nothing to do with creating the map! Let's move that to a method called createPlayer, which we'll relegate to the "much code removed..." section.

String playerCavern = anyCavern();
game.setPlayerCavern(playerCavern);
game.setWumpusCavern(anyOther(playerCavern));
IntStream.range(0, 3).forEach(() -> game.addBatCavern(anyOtherCavern(playerCavern)));
IntStream.range(0, 3).forEach(() -> game.addPitCavern(anyOtherCavern(playerCavern)));

Using IntStream.range is about the closest we can get to a proper range loop like Python's for x in range(i, j), and I much prefer it to for loops.

Why did I leave the bat and pit cavern population loops separated? Because they're not inherently linked, and we may reasonably wish to change the frequency of one without changing the other.

Hard-coded values

When an application hard-codes values as severely as this example, it's hard to avoid cringing at the looming technical debt. The severity we see here would be perfectly acceptable in an early computer science course, but a real-world system would struggle to keep up with changing requirements. A simple typo in a string, an additional witty cavern description or the substitution of localized languages should not require code changes.

Similarly, values such as the initial player HP, the number of arrows in the player's quiver, the seed for the randomly generated number of caverns, and the number of bat and pit caverns, all should be configurable with ease. Perhaps we wish to introduce difficulty levels which change the balance of these values. Perhaps we find we've given the player too much HP for a fair fight. We'll undoubtedly need to balance these values, and so we'll be better off storing them in an configurable but immutable data structure.

As detailed in previous chapters of Clean Architecture, the data structure to house these values shouldn't matter to our Main component. They could reside in a key-value store, a database of any kind, a CSV or TSV, or even a well formatted plain text file. As far as this component knows, they're all just an interface. For our purposes we'll call the interface GameConfiguration, which is responsible for loading and providing the configured values.

Putting all our changes together with the interface-provided configuration, we arrive at a much cleaner architecture than Uncle Bob presents.

public class Main implements HtwMessageReceiver {
  private static HuntTheWumpus game;
  private static final List<String> caverns = new ArrayList<>();

  private static int hitPoints;
  private static int quiver;
  private static String[] environments, shapes, cavernTypes, adornments;

  public static void main(String[] args) throws IOException {
    environments = GameConfiguration.get("environments");
    shapes = GameConfiguration.get("shapes");
    cavernTypes = GameConfiguration.get("cavernTypes");
    adornments = GameConfiguration.get("adornments");
    hitPoints = GameConfiguration.get("hitPoints");
    quiver = GameConfiguration.get("quiver");

    game = HtwFactory.makeGame("htw.game.HuntTheWumpusFacade", new Main());
    createMap();
    createPlayer();
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    game.makeRestCommand().execute();

    while (true) {
      GameUI.displayUserStatus();
      HuntTheWumpus.Command c = GameUI.getUserCommand();
      c.execute();
    }
  }

  private static void createMap() {
    int nCaverns = (int) (Math.random()
                          * GameConfiguration.get("cavernSeed")
                          + GameConfiguration.get("cavernMinimum"));
    while (nCaverns-- > 0)
      caverns.add(makeName());

    for (String cavern : caverns) {
      for (Direction direction : Direction.values()) {
        maybeConnectCavern(cavern, direction);
      }
    }

    String playerCavern = anyCavern();
    game.setPlayerCavern(playerCavern);
    game.setWumpusCavern(anyOther(playerCavern));
    IntStream.range(0, GameConfiguration.get("batCaverns"))
      .forEach(() -> game.addBatCavern(anyOtherCavern(playerCavern)));
    IntStream.range(0, GameConfiguration.get("pitCaverns"))
      .forEach(() -> game.addPitCavern(anyOtherCavern(playerCavern)));
  }

  // much code removed...
}

The resulting code is more terse, hardy and generally cleaner. Is some of this overkill for a small pet or student project? It probably is, but Uncle Bob presents this as a contrived but real-world example in a printed book about code design, and should have taken the time to apply his own principles to his examples.