The 2 Values of Software

I first encountered the idea that software has 2 values while reading Robert C Martin’s “Clean Architecture” back in 2022: software is valued for being “soft” and as a “ware”. Uncle Bob had a way of saying this like this was in front of us the whole time — waving even — and he was just pointing it out.

I’ve read the book a few more times since then and I’m fairly certain I must have been so shocked at the idea of softness as a value that my succeeding read-throughs felt like I was reading the rest of the book for the first time. Except, it didn’t feel like a new idea. It felt like it was, in fact, the idea behind all the principles, practices, and patterns. The why of software design and architecture itself!

Now that may seem over-dramatic but imagine a decade of context-less information and inarticulable intuition all falling into place. I’ve finally obtained a piece of foundational knowledge in software development! Or so I believe.

I now go with the more professional-sounding duo of structure and behavior. A behavior’s value is in the present while a structure’s value is in the future — when behavior needs change or addition. Reading “Tidy First?” by Kent Beck has further reinforced this idea and why little tidyings matter.

Now while I’m still clumsy, this knowledge has helped immensely in my ability to articulate design decisions, considerations, and trade-offs. This has been indescribably valuable as a basis and filter (which is why I seriously mean foundational)  for the various advice, practices, and opinions on software development floating about nowadays.

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.


Dependency Injection

Dependency injection has become a relatively common concept to ask during technical interviews. While it is the generally accepted best practice in modern PHP development, it seems terribly little understood beyond the provisions exposed by 3rd party libraries and frameworks.

Perhaps the most common explanation of DI (dependency injection) is a literal description such as passing objects into a constructor. This is, however, neither sufficient nor a satisfying explanation to an interview question or an introduction to the uninitiated.

While many things are said about DI, it's foremost purpose is the decoupling an object's creation from its usage. In practice, this is usually achieved by having a class define and require its dependencies rather than instantiating the dependencies by itself. This forces the "passing objects into the constructor" during object construction which is often cited during discussions of DI.

Not to be pedantic but I think it is important to know the advantages of DI despite already knowing the "motions". Primarily because, the knowledge can help maximize the returns of DI and justify the "overhead" of routing individual dependencies through the constructor rather than just using them outright wherever (as with Laravel facades, static methods, or instantiating dependencies on the spot).

Perhaps the most underrated advantage of DI is a strong decoupling of implementation – that is, dependencies can be of any implementation so long as the interface required is met. This is only possibly since we remove the responsibility of object creation which needs an exact concretion. It is then a matter of expressing the object's dependencies as abstractions such an interface or class to make them "swappable" with other objects implementing the same interface or inheriting the class.

Another major advantage of DI is it naturally leads to the use of a DI container which takes over the responsibility of object creation, dependency resolution, and allows instance re-use. Best of all, there are already several excellent open source DI container implementations which means a huge effort saved and massive gains in code quality.

I can personally recommend Symfony's DI component and PHP-DI which I've both enjoyed using.

Another great benefit to dependency injected services is the possibility of employing an open-closed approach (think Open-Closed principle) when introducing new behavior or updating an existing one. Rather than modifying a class, we can simply create another one or extend the original and implement the new behavior. The new class can then be swapped with the original class wherever the new behavior is needed.


interface Logger
{
    public function log(string $message);
}

class NoisyLogger implements Logger
{
    public function log(string $message)
    {
    	echo "$message\n";
    }
}

class MutedLogger implements Logger
{
    public function log(string $message)
    {
        // no-op
    }
}

class Router
{
    private $logger;
    
    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }
    
    public function route(string $path): void
    {
    	$this->logger->log("Routing $path...");
        return $controller;
    }
}

if ($isSilent)
    return new Router($pathToClassMap, new MutedLogger());

return new Router($pathToClassMap, new NoisyLogger());


To illustrate, say we have a Router class which logs information on what it is doing. Some need arises such that we need to be able to conditionally disable the Router from logging anything. Without modifying the class, we can achieve this simply by instantiating a Router with a different Logger.

Despite being a best practice, there are some situations where DI is unsuitable and it usually means there are bigger design issues to address.

1. God classes — classes that are responsible for and do several things — are terrible candidates for dependency injection. Some objects are expensive to create such as wrappers for database connections. With God classes, DI would result in all dependencies of the God class to be instantiated regardless of whether it will be used. And as God classes are typically used in a lot of places, this could result in a systemic performance hit.

2. Another unsuitable scenario are classes with circular dependencies. Formally defining the dependencies of such classes in order to enable DI will actually make it impossible to instantiate the classes.

Knowing the why of DI as well as its strengths and weaknesses is helpful in reasoning about and communicating software design choices. It also enables fully taking advantage of DI when available and avoiding some potentially nasty pitfalls from blindly following best practices — especially when dealing with legacy code.


Writing a Bug Report

Despite being at the receiving end of bug reports, programmers are generally poor authors of bug reports as well. This came as a surprise to me as I worked with programmers of varying experiences. How could programmers themselves not know how to author an effective bug report? If anyone, they should best know what a good bug report looks like. This is kind of true and not true at the same time.

It does seem like most programmers can deliberately tell apart a good bug report from a bad one. When authoring, however, they seem to be at a loss. They sometimes put in unnecessary information or leave out important ones. Oftentimes, they get lazy and just put in whatever first comes to mind and sticks. What you get is an empty ticket with the title: “Cannot upload”. A dozen of these and a few weeks later, you get a substantial backlog of tickets no one understands.

This seeming paradox of a programmer’s inability to author good bug reports can be attributed to a problem solving mindset. Programmers typically care far more about procuring the solution than the problem needing solving. It is not quite uncommon to see programmers (especially juniors or immature ones) take pride in being able to decipher completely unhelpful bug reports. It probably seems like piecing together a puzzle. You get to pat yourself on the back when you get it right. But get it wrong and you waste not just your time and resources.

In a company, a team, or any worthwhile group endeavor, it is less about individual prowess than it is about teamwork and collective productivity. Deciphering a poorly written bug report and realizing you got it wrong 3 days later is a huge waste of everyone’s time.

As those who have studied the computer sciences may have learned, how a problem is framed also influences its solutions. That is, a well framed problem can also make the optimal solution obvious. Similarly, a well written bug report makes it easier to correctly understand and address a bug.

For a while now, I have followed a simple 3-step template for reporting bugs. I, honestly, only got it somewhere on the internet a few years back but it has been so helpful that all the teams I have shared it with have decided to adopt it. I privately call it the Reality-Expectation-Reproduction (RER) template and it basically ensures a bug report has at minimum the 3 following information:

  1. What happened - Reality
  2. What was expected - Expectation
  3. and How to reproduce - Reproduction

Together, these three convey to the reader all the actionable information they need to verify a bug. Even better, this template is also useful for user testing and verifying the bug actually no longer exists.

I have found, however, that even better than the RER template are bug reports with annotated screenshots or screencasts. When applicable, I believe screenshots and screen casts are the most effective way to convey information.

In practice however, leveraging both the RER template and well picked screenshots or screen casts seem to result in the most helpful tickets. They are simple to write, easier to estimate during sprint planning, easier to understand, and easier to verify when testing.