Does Clean Code Matter?

In his book Clean Code, Uncle Bob quotes Kent Beck about how fragile a premise it is that good code at all matters. Both authors write their respective books on the faith that it is although I suspect it is perhaps more accurate to call it conviction than faith.

It is not uncommon to come across code bases that are undoubtably dirty, messy, or unclean. It is also not entirely uncommon to come across successful projects with a terribly unclean code base. You can’t help but think whether clean code is plain vanity after all.

However, I think clean code ultimately matters because messy code is hard to read and hard to change. When necessary care is not taken writing code, you get messy code and mess tends to multiply and escalate. Messiness can initially be cosmetic but over time (and not a long time is required at all) more material aspects become messy including design and architecture. Small messes don’t stay small for long.

I think an excellent analogy is the Broken Window Theory. The idea is that visible signs of disorder encourages more of it. One story talks about a building which was well maintained for many years. It never got intruded or vandalized. Then one day, an accident happened and one window was broken. The rest of the building was still pristine but, for some reason, the broken window was not immediately repaired. In the following weeks, the building, that for years never got intruded or vandalized, got intruded and vandalized resulting in more broken windows.

Code that looks neglected tends to encourage neglect. Messy code tends to become the foundation for even more messy code. The mess bleeds into design and even escalates into architecture. The mess snowballs.

Between clean code and messy code, we know clean code is better. Yet, we also know projects succeed despite having messy code. Perhaps projects even initially succeed precisely because of quick messy coding. Clean code is practiced on the conviction that it is the right way to succeed. While it may ruffle some feathers, clean code reflects our professionalism and a low barrier to entry is not an excuse for low professional standards.

Dependency Inversion

One of the most useful techniques I’ve found when building software is dependency inversion. It seems to be largely misunderstood by most programmers and confused with dependency injection. Yet, it is a cornerstone of Clean Architecture and is the main principle behind decoupling a system from its lower level dependencies.

The central idea in DI is that high level policies must not depend upon lower level details. Typically, the flow of dependency follows the same direction as the flow of control as a program executes. The inversion in DI refers to the inversion of dependency flow against control flow.

The inversion of dependency flow happens on the source code and not on runtime. This seems to be a common point of confusion when trying to grok DI. In the source code, dependency is expressed by the presence/utilization of a module (dependency) in another (dependent). To perform the inversion, an indirection of the dependency must be done and this is typically done using interfaces. Although, having written applications in JS which does not natively support interfaces, I think the interface language construct is not strictly necessary to achieve DI.

Rather than depending on a specific module, we depend on an interface. An interface consist of public functions, arguments, returns, and exceptions thrown. An interface must express the purpose of a module using these elements. Put differently, an interface is a precise and formal specification of purpose and structure.

To be usable for its purpose, modules must fulfill i.e. implement the interface. Without interfaces, the higher level module must “know how to use” the dependency. With interfaces, the lower level modules must “know how they will be used”. This is a big shift of responsibilities. Now, the dependencies must “look up” at the interface and they must be implemented to its specification. In essence, they now depend on the interface.

In a very fundamental way, an interface is a boundary. It is the point where dependency flow reverses and it is a boundary that separates higher level modules from lower level ones. Crossing an interface is crossing an abstraction layer. On one side, we have high level policies embodying the high value business logic while, on the other, we have lower level details serving as a business-agnostic foundation of the system.

When dependency flow is reversed, lower level modules can now be freely changed without affecting the structure of high level ones. Hence, we refer to lower modules as details. We expect them to change and vary in implementation while still providing the same “service” e.g. calculation, storage, retrieval, etc. This gives us a more flexible system, highly decoupled, and robust to changes in lower level dependencies.


TDD

On a strange turn of things, I am beginning to appreciate the practicality and utility of TDD. It has been over a decade since I first learned about it but only recently do I seem actually be learning it. Of all things, I seem to be making progress with it by unit testing in JS. As I said, strange.

A few things seem to have crystalized for me which has helped in this recent break though. They seem so banal in retrospect that it surprises me.

(1) TDD is not exactly test first. Rather, test drives. I used to agonize over making sure I never write code before tests but this seems to have been taken too literally. Code that is written to figure out what needs to be done, organize thoughts, or visualize an idea might as well be notes. Production code is what gets written after a test. This is a slight but helpful distinction.

(2) Tests won’t protect you from incompetence. I used to have a bunch of silly takes around this point. For example, thinking: You can still pass the test with a bad design, you can still write insecure code, you can still produce certain side effects, you can still do this or that. Of course you can. But why would anyone competent do that? This thinking is a race to the bottom. Tests won’t make a bad programmer good. If you don’t write good code, you probably won’t write good tests either. TDD is a better way to write good code.

(3) The preparation of the test requires a different “mode” of development/thinking. To do TDD, you need to switch between “set an expectation” mode and “fulfill the expectation” mode. This was helpful to understand why TDD can initially feel the way it does — jarring and unnatural. However, this does not seem unnatural at all when you think about how you automatically do the same thing (manually) when coding.

A common argument against TDD that still eludes me is exploratory work/coding. I suspect though this is similar to point 1. Exploratory work simply does not produce production code and is not a good area to apply TDD. Perhaps we shall see what more I learn in the next decade and whether this is the case or not.

Aside from guiding development, an interesting thing I have been seeing a few times are experiments with test driven design. However, I won’t try and integrate that with my mental model for now but definitely looks interesting. While I think in practice it would require a maybe impractical level of test granularity, the mere idea that design can be discovered rather than created seems, to me, quite full of groundbreaking implications.