avatarNicklas Millard

Summary

The article provides guidance on improving code quality by applying SOLID principles through practical refactoring examples.

Abstract

The article "Refactoring From Trash to SOLID" by Nicklas Millard emphasizes the importance of writing high-quality code by adhering to SOLID principles. It offers practical examples to demonstrate how to refactor complex and poorly designed code into simpler, more maintainable, and testable classes. The author illustrates how to apply the Single Responsibility Principle by separating concerns, the Open/Closed Principle by extending functionality without modifying existing classes, the Liskov Substitution Principle by using polymorphism, the Interface Segregation Principle by creating focused interfaces, and the Dependency Inversion Principle by relying on abstractions. The article acknowledges that refactoring may lead to an increase in the number of classes but argues that this is necessary to match the complexity of the application and improve the overall workability of the code.

Opinions

  • The author believes that refactoring to SOLID principles is not only about minimizing code but also about making it more understandable and easier to maintain.
  • The article suggests that while some may view the proliferation of classes as a negative consequence of refactoring, it is a necessary step to align the codebase with the application's complexity.
  • The author expresses a preference for the Open/Closed Principle, highlighting the benefit of adding new functionality without altering existing code.
  • The author criticizes the use of "swiss army knife" classes and advocates for the creation of single-purpose classes and interfaces to enhance code clarity and testability.
  • The article promotes the use of code reflection for creating simple and highly extensible code, despite potential performance trade-offs.
  • The author asserts that adhering to the Dependency Inversion Principle simplifies testing and the creation of custom implementations, which outweighs the complexity introduced by additional classes.

BETTER SOFTWARE DESIGN

Refactoring From Trash to SOLID

Amp up your code quality, the easy way.

I’m not going to preach theory. Since you decided to click this cryptic title, I assume you’re already familiar with the SOLID principles abbreviations.

Chances are you’ve already looked at a bunch of other articles, tutorials, and videos about SOLID. No shame in having to get more perspectives on the same topic.

I’ll show you how easy it is to refactor garbage code, and hopefully, you’ll have a foundation to improve your code quality.

Luckily, it’s incredibly easy to write SOLID code.

Let’s get one thing straight before diving into code

So, I have no chance of demonstrating every aspect of each principle. I’ll merely be demonstrating the principles using a few practical examples. You have to do your own thinking from thereon. Incorporate what you think works. Discard what doesn’t.

Make your classes do one thing well

If you have to modify a class when a new feature comes along, you might be looking at a single responsibility violation. Typically, this happens whenever you need to implement cross-cutting concerns, such as logging, caching, etc.

So, this example snippet clearly shows a class having too many responsibilities. It’s both handling repository related operations as well as logging.

Class doing logging and database operations

We refactor this by creating two separate classes. One is used to decorate the other. Note I’ve also extracted an interface and the decorator class takes an instance of the interface as a dependency. That’s how easy you can decorate a class.

Extremely simple and effective.

Split one class into two for better separation

Do you see how the UserRepository got a whole lot simpler, and now only has one responsibility? Logging is no longer its concern.

We’ve created a separate UserRepositoryLoggingDecorator class that takes care of logging.

Tip: you can also refactor to commands and queries. It’s a near perfect pattern to use when making classes simpler.

Don’t Modify Existing Classes

This is my absolute favorite principle. Being able to just add new classes and not touch existing ones is d*mn awesome.

You quickly spot OC violations. Characterized by massive if-else or switch branching, or, every new feature requires a new if. Just like this product formatter, taking a product type and returns a string formatted in some desired way.

Poorly written format method

Upfront it doesn’t look too bad. It’s quite simple.

However, for every new formatting type, you have to violate OCP by modifying the method. Also, if there’s a bug and you need to fix formatting logic, you need to do so at a place surrounded by unrelated code.

Using polymorphism to remove insane, unnecessary if-else is one of the best, and easiest, ways. In this way, you also touch upon the Liskov’s Substitution Principle.

Now, we’ve refactored the above, inflexible code. Take whatever time you need to read thru this code. You and I will walk thru it after.

Refactored to strategy classes

So, you have obviously noticed how much more code there’s now. Refactoring is not about minimizing code, it’s about making code easier to deal with and reason about.

First off, we take each logic chunk inside the if and else-if and place it inside its own formatter class. Each class now has a single purpose. When you need to fix a bug, you know all the surrounding code is related to it.

Secondly, each formatter type is registered with the ProductFormatter by calling its RegisterFormatter().

The Format() implementation has been changed to use lookup which formatter to use, as opposed to handling the formatting itself.

Also, notice there now is a JSON formatter. Simply by adding a class for it. It was that simple.

Here’s an example of how to use the product formatter.

Example of using a formatter class with registered formatters

Beginner programmers and computer science majors will no doubt raise their eye-brows and immediately say this is overly complicated or not good for performance. Well, there’s a trade-off with everything. That’s life.

Tip: look into using code reflection to create simple, highly extensible code.

Only use what’s relevant to your class

You’ve probably used or even made your own swiss army knife classes. This is for instance service and manager classes. Generic enough to container whatever. They’re basically logic dumpsters.

Say we have a command class used to update a user. This class takes a dependency on a UserManager containing the nitty-gritty user stuff.

No interface segregation

It’s all mighty fine. But, we can do better.

As of now, the UpdateUser class can freely access any method exposed by the user manager. Not exactly what we want, right?

We’ll replace the swiss army knife dependency with a single-purpose tool like in the snippet below.

Notice how the UpdateUserCommand and UserManager has almost stayed the same, with only a few changes.

That’s how easy it is to clean up messy code.

Extracted interfaces from the UserManager

Take a second to think of the implications in regards to testing, and creating custom implementations of IUserUpdater. It all became so. much. easier.

Decouple your classes

You’ve already been exposed to the dependency inversion principle thru out this article. It’s all about relying on abstractions over concrete classes. It’s that simple.

So, the class below is violating multiple principles. It’s ‘newing’ up its dependency on AppDatabase. A big no-no.

Class violating DIP and explicit dependencies principle

Instead of ‘newing’ up concrete instances, you’d take them as explicit dependencies, and, you’d want to rely on abstraction instead of a concrete class. Simply put, you’d want to take an interface as a constructor argument.

Take a look at this refactored GetUsersCommand class.

Stop newing up classes and take interfaces as constructor arguments. It’s honestly that simple.

The refactored class that adheres to DIP

Again, this incredibly simple approach has just relieved us from pain-inducing testing.

“The number of classes explodes with every refactor your make!”

This might be one objection you have to my refactoring approach.

Remember refactoring is not about minimizing code or even necessarily about making things “simpler”. It’s about changing your code to match your application’s complexity and making your code easier to work with.

Simple != easy.

Resources for the curious
-------------------------
What to look for in Code Review - JetBrains Blogpost
Dependency Inversion Principle by OODesign

Nicklas Millard is a software development engineer in one of the fastest-growing banks, building mission-critical financial services infrastructure.

Previously, he was a Big4 Senior Tech Consultant developing software for commercial clients and government institutions.

New YouTube Channel (@Nicklas Millard)

Connect on LinkedIn

Programming
Software Development
Software Engineering
Solid
Object Oriented
Recommended from ReadMedium