Writing Maintainable and Testable Code: The “Why” Behind Design Principles

İbrahim Gündüz
4 min readJan 30, 2025

--

Generally speaking, programming languages and libraries are tools that enable us, as developers, to build software applications efficiently. However, when it comes to maintainability and testability, we still need to understand certain principles — and, more importantly, why they exist. Of course, we won’t attempt to explain all design patterns or principles in this article, but we will explore why some of these principles exist by examining real-world problems.

Let’s take the following examples and see what the issues are.

What are the issues here? I’m pretty sure the majority of us will come and say, “Oh… why didn’t you use DI?” — and they’ll be right. But what is the actual problem? Can’t we test this code?

The answer is YES. We still can test this code with some reflection hacks. But when we come to the point, of why we should inject the dependencies instead of initializing them in place:

  • Our code must be open for extension and closed for modification. (O from SOLID — Open-Close Principle ) So we can avoid any potential bugs which are not directly related to the implementation. For instance, based on the example above, any bugs happening while configuring the dependencies while instantiating.
  • If we leave the code as it is, we would need to disable the class constructor and manually set the mocked dependencies in the private properties by using some reflection hacks each time when we need to instantiate EmailNotifier during testing. So, ideally, we should be able to test our code with minimal effort spent on mocking. To achieve this, we should handle the instantiation of dependencies externally and inject them into the class being tested.

So, the final shape of the code should be like this:

Finally, we can test the code easily.

Let’s make more changes to this code example.

Someday, we decided to replace the email service with an internal SMTP service. So, we need to replace the HttpClient with an SmtpClient. However, as we’ll lose some reporting capabilities previously we could use from the email service, we need to log some data about the email notification.

As the SmtpClient is a dependency installed from another repository, it’s not a piece of code we can modify. So, we need to add this logging feature to the client library somehow.

Although we achieved our goal of logging the email data, we still need some hacks to mock the SmtpClient communication while testing. Considering the effort we need to spend to struggle with all hacks, I guess, it is clear that we need to use the functionality from the SmtpClient without using inheritance.

Although we can apply the composition-over-inheritance principle by simply injecting SmtpClient, as we did in the previous example, we still need to address logging the email data. Since we cannot modify the SmtpClient class, we need an intermediary solution—and this is where the Decorator design pattern comes into play.

Decorator design pattern is a pattern that allows adding a new functionality to an existing object without modifying it.

The decorator class shown above takes NotificationStorage and SmtpClass as constructor dependencies and logs the notification details in the desired storage after sending the email. This allows us to inject the class as shown below and easily test both components individually by mocking their dependencies.

As SmptClient and NotificationStorage are dependencies of SmtpClientNotificationStorageDecorator, you can just test the functionality of the class by mocking its dependencies. You don't need to worry about SmtpClient since it’s an external dependency.

— -

Let’s keep it brief to maintain the article’s readability for now. I hope you enjoyed reading it and gained some key takeaways about the “why” behind the principles explained.

Thanks for reading!

Credits

--

--

No responses yet