An example of introducing symmetry to enable duplication removal

Design Patterns, Implementation Patterns, Katas, Learning, Refactoring, Code Smells


Symmetry is a subtle concept that may seem only related to code aesthetics. However, as Kent Beck states in Implementation Patterns,

"...finding and expressing symmetry is a preliminary step to removing duplication. If a similar thought exists in several places in the code, making them symmetrical to each other is a first good step towards unifying them"

In this post we'll look at an example of expressing symmetry as a way to make duplication more visible. This is the initial code of a version of a subset of the Mars Rover kata that Álvaro García and I used in a refactoring workshop some time ago:

This code is using conditionals to express two consecutive decisions:

  1. Which command to execute depending on a command code.
  2. How to execute the command depending on the direction the rover faces.

These decisions are repeated for different responsibilities, displacing the rover and rotating the rover, so the code presents a Case Statements smell. Since those decisions are completely independent, we could mechanically refactor the code to start using polymorphism instead of conditionals. This way we'll end having two consecutive single dispatches, one for each decision.

If we start applying Replace Type Code with State/Strategy refactoring to the code as it is now, there is a subtle problem, though.

Looking carefully more carefully, we can observe that in the case of the rover's displacement, the sequence of two decisions is clearly there:

Two decisions

However, that sequence is not there in the case of the rover's rotations. In this case there's a third decision (conditional) based on the command type which as, we'll see, is not necessary:

Three decisions

This difference between the two sequences of decisions reveals a lack of symmetry in the code. It's important to remove it before starting to refactor the code to substitute the conditionals with polymorphism.

I've seen many cases in which developers naively start extracting methods and substituting the conditionals with polymorphism starting from asymmetrical code like this one. This often leads them to create entangled and leaking abstractions.

Particularly, in this case, blindly applying Replace Type Code with State/Strategy refactoring to the left and right rotations can very likely lead to a solution containing code like the following:

Notice how the Direction class contains a decision based on the encoding of the command. To be able to take that decision, Direction needs to know about the encoding, which is why it's been made public in the Command class.

This is bad... Some knowledge about the commands encoding has leaked from the Command class into the Direction class. Direction shouldn't be taking that decision in the first place. Neither should it know how commands are encoded. Moreover, this is a decision that was already taken and doesn't need to be taken again.

How can we avoid this trap?

We should go back to the initial code and instead of mechanically applying refactoring, we should start by removing the asymmetry from the initial code.

You can see one way of doing it in this video:

After this refactoring all cases are symmetrical (they present the same sequence of decisions)

Two decisions

We've not only removed the asymmetry but we've also made more explicit the case statements (Case Statements smell) on the command encoding and the duplicated switches on the direction the rover is facing:

Now it's clear that the third decision (third nested conditional) in the original rover's rotations code was unnecessary.

If we start the Replace Type Code with State/Strategy refactoring now, it's more likely that we'll end with a code in which Direction knows nothing about how the commands are encoded:

and the encoding of each command is known in only one place:

As we said at the beginning, symmetry is a very useful concept that can help you guide refactoring. Detecting asymmetries and thinking why they happen, can help you to detect hidden duplication and, as in this case, sometimes entangled dimensions of complexity[1].

Then, by removing those asymmetries, you can make the duplication more visible and disentangle the entangled dimensions of complexity. The only thing you need is to be patient and don't start "obvious" refactorings before thinking a bit about symmetry.

[1] Dimension of Complexity is a term used by Mateu Adsuara in a talk at SocraCan16 to name an orthogonal functionality. In that talk he used dimensions of complexity to group the examples in his test list and help him choose the next test when doing TDD. He talked about it in these three posts: Complexity dimensions - FizzBuzz part I, Complexity dimensions - FizzBuzz part II and Complexity dimensions - FizzBuzz part III. Other names for the same concept that I've heard are axes of change, directions of change or vectors of change.

Originally published in Manuel Rivero's blog.