Refactoring and S.O.L.I.D Principles
6 min read
Another day another Lint-School session.
We learned about refactoring and what to do before you start refactoring things we talked about SOLID principles as well when we were refactoring and saw the OCP and SRP in action
So I took some notes about refactoring and what to do before and when refactoring and when writing code so you write better and cleaner code
so, let’s get started!
You got a task on a new project, a legacy project, where you're required to add a new feature.
you open the code, the networking code is in the view controller, he caches the response in the user defaults as well as writing the caching code in the view controller, there are a lot of random magic numbers thrown around doing god knows what calculations...
What is the first thing to do?
DON'T TOUCH THE CODE
Understand what’s going on first, then start writing tests, If you can’t just writing prints on the functions. verifying those prints is enough
After you have done that, start refactoring.
When Refactoring, identify problems first by…
Start reading the class functions, see if the function has more than 1 responsibility then start breaking them down
Start analyzing class responsibilities from the broken functions, see if something is off or shouldn’t be here, then start taking it out to another component and make sure you pick an appropriate name to it, after all, software design is about two things, naming things and putting them in their correct place
Start writing documentation to the classes you are refactoring stating their responsibilities so you don’t forget
Start analyzing dependencies (variables) and see how the class is coupled with each other
start changing these dependencies into protocols to make the class depend on protocols rather than concert Implementations which is what the DIP states
If a function contains magic numbers or calculations start taking those numbers to scope-local variables with descriptive names and use them, these add great readability with too little changes
Always remember SOLID Principles, and SOLID is about Discipline, about being consistent
When writing components, adding functions, ask yourself, is this component fulfilling more than 1 responsibility?
Two reasons to change, two responsibilities, start refactoring
Now we talk a bit about the SOLID principles...
Dive deep into SOLID Principles
Violations of SRP
Let’s say you have a cache layer that all it does is caching, in-memory cache, realm or core data, and we want to cache user token, then a new requirement is added and now we have to renew the token if it expires.
We have to add validations if the token is expired we don’t cache it, we start adding it to the cache layer… now, is that the right decision? from the above list we check does this adds to the responsibilities of the cache layer?
Well no it is not the right decision, because it now manages the cache and also validates if the token is valid for caching, now we start to separate the validation in a validator class, and move this into the business layer.
Always ask yourself when adding components, does this add to the responsibilities of the same class? of the function? And from there, you will know if you violated the SRP or not
OCP (Open - Closed Principle)
So what is the open-closed principle? In plain English, it means that when you have a feature and you want to add code to it, you don’t go into it and start modifying it, like when adding a new feature you don’t start modifying an existing feature, instead you start extending this feature, by maybe add new functionality by adding another class.
For example, if I have a validation suite,
Bad Example: Build one big class that do validations for the whole project, and when I need to add new validations, I add to that class
Good Example: Break down my validations into a couple of classes each doing one thing, so that when I need to add new functionality, I create a new class to validate the phone number format for example
Perks of OCP is you have less work to do, and less risk of breaking things, it’s additive.
LSP (Liskov Substitution Principle)
The LSP states that the objects should be substituted with their subtype without affecting the correctness of the system.
Example:- If you have a client that depends on a cache protocol, (the cache protocol is the type it depends on), it shouldn’t matter if the client interacts with a subtype of the protocol, an in-memory cache, realm or core data cache If it does matter, you're violating the Liskov principle and thus you created more coupling between components, and thus, you have a rigid components
ISP (Interface Segregation Principle)
The ISP is about dividing your protocols into small protocols to ensure that clients don’t submit to unwanted requirements by the protocol Doing so enables you more freedom in cofronting to what you want, and leave what is unneccessary In Swift, you can extend that protocol and do a default implementation to those functions or variables which is basically the the same thing.
DIP (Dependency Inversion Principle)
Dependency Inversion States that High-level components should not depend on low-level components or modules, they should depend on Abstractions
Protocols rather than concrete implementations
Example:- Instead of letting your high-level components like UseCases, Interactors depend directly on frameworks like Realm, for example, they should depend on some kind of abstraction, then the Realm implementation conforms to that abstraction.
Because if you do, you can develop, test, maintain, extend your high-level modules, your policies, your business rules in isolations and in parallel, you can have someone working on the high-level rules and someone working on the low-level details, like Realm implementation
But make sure you don’t leak your low-level details in the abstraction, like passing parameters in your abstraction like
Core Data class), then you're leaking the implementation details to the abstraction, then you cannot test it in isolation, you can not develop it in isolation, u can not replace the framework with another framework easily, during tests or because you have a new framework that you want to use.
If you find that you're importing a module in the file of the abstraction like a protocol, and those import modules from the details side, then that’s probably not good, because think about it, you want to be decoupled from the implementation details, you want the framework depending on the business rule, not the opposite.
Violations of DIP
Making high-level components depending on the low-level ones, like importing core data on your high-level business rules
Why do we need SOLID Principles?
Good software, build projects that are a pleasure to work with, collaborate better, build flexible, modular systems, that are easier to maintain, test, develop and extend, replace even These things lead to exceptional salaries, more fulfillment, happiness
What happens if you don’t apply SOLID Principles?
The code is going to rot, coupling, code smells everywhere, no clean abstractions, everything gets in each other way, thousand of code lines in the same file, debug hell. Not following the SOLID principles is a perfect recipe for Spaghetti code
Why is it so hard to apply the SOLID Principles?
Because it depends on your case, there is no one silver bullet that’s going to solve all your dependency issues, your modularity issues Most of these things are counter-intuitive