Published on

How TDD enables a Simple Design

In “Clean Craftsmanship”, Robert C. Martin explains that the best design for a system is one that can afford to introduce new changes in a flexible way and can ensure that previous features will continue to work. He also says that simple in this context does not necessarily mean simplicity, but rather untangled/decoupled.

A good design is driven by four simple rules:

  1. High code coverage

The first and most important rule of simple design is to have high code coverage (Uncle Bob, 2021, p231 states that the desired code coverage is 100%). This enables a good design because of one of the main characteristics of testable code, which is decoupling/untangling, the code developed using TDD already tends to be decoupled. Also by having a good test suite (good coverage, decoupled, good expression) we can make changes to the design without the fear of breaking the current implementations. This allows us to improve the design/architecture of a system more safely/easily because our only worry will be to find a better approach when making changes to the system.

  1. Maximize Expression

By making the code more expressive with good variable/methods naming, and good types, for example, the designer’s intent will be clear. “The structure of the algorithm is easy to see. This code is expressive. This code is simple.” (Robert C. Martin, 2021, p234).

In conjunction with having a well-expressed production code, it’s also essential to have a well-expressed test suite. This enables the use of test cases as guides in order to learn the features of the system. It also helps newcomers to get to know the system.

An example of how we can improve test code expression by using a DSL:

Instead of accessing the table view directly from the test code.


func test_displayCells() {
    let sut = ViewController()
    sut.loadViewIfNeeded()

    let indexPath = IndexPath(row: row, section: businessSection)
    let cell = tableView.dataSource.tableView(tableView, cellForRowAt: indexPath)

    /* ... assert against cell properties ... */
}

We can extract the cell creation to a helper method.


func test_displayCells() {
    let sut = ViewController()
    sut.loadViewIfNeeded()

    let cell = sut.simulateCellIsVisible()

    /* ... assert against cell properties ... */
}

func simulateCellIsVisible(at row: Int = 0) -> UITableViewCell? {
    let indexPath = IndexPath(row: row, section: businessSection)
    let ds = tableView.dataSource

    return ds.tableView(tableView, cellForRowAt: indexPath)
}
  1. Minimize duplication

Copying and pasting code can lead to a lot of duplication, and then to a tangled/coupled design because now there are two stretches of code that will have to be modified to accommodate changes. Finding these duplications and modifying them based on their contexts is not trivial and can make the system more fragile.

  1. Minimize size

The last rule is this simple (in the common meaning of simplicity 😆). It states that, after you’ve applied all previous rules, you can reduce the code size by extracting methods - or others refactoring techniques-. The objective is to have smaller and more expressive functions.

Conclusion

We can see that the most important rule of a simple design is High code coverage because it actually enables and facilitates the execution of the other rules (which are basically refactorings). This is because a well-designed test suite removes the fear of change, as we get more confident refactoring, we strive to make the code more expressive, with less duplication and with a smaller size.

References

Clean Craftsmanship, Robert C. Martin, 2021