Refactoring: The missing step
Have you ever wondered why your code just doesn’t look as clean as some of your more senior coworkers? More than likely the biggest difference is refactoring! Refactoring is one of the more important steps when you are developing new features, or writing any code for that matter. A lot of developers will go about it the usual way - think of the solution, work on a feature and get it working, then call it a day. If it works, it works.
However, it’s very rare to get it right the first time, unless you’re writing something really simple and straightforward. Looking at the code you wrote yesterday will practically always bring some new ideas and options how you could have done it better. Perhaps you could make something modular or at least re-usable with minimum effort, or more readable. This only comes through reflection and with some distance between writing and reading the code.
Some of the benefits of refactoring include more modular, testable code, cleaner and more understandable code, and code that in general more closely follows the SOLID principles.
So, how do we actually refactor our code?
There are a few things we can do to clean up our code.
1. Use dependency injection to decouple your code from frameworks.
This step has a few benefits. Decoupling your code from your frameworks makes it so your code is actually less dependent, especially if you use an interface or protocol for your injection. Instead of relying on the framework, you use an interface you define for your app’s needs of that framework.
This allows you to replace this framework with another framework more easily at some point in the future. For example, if you wanted to move from the 1st party URLSession for your network calls to a 3rd party framework, such as Alamofire. If you didn’t use dependency injection with a protocol, this migration could very easily take weeks, maybe even months depending on the size and complexity of the application. However, with dependency injection and protocols, that migration takes maybe an afternoon, if that.
Since you are using an interface to work from instead of working with the framework directly, your code can define everything in terms of the domain you are working in, instead of having to work with the framework APIs and forcing your code to change based on how the specific framework works, which is a major strain on both the codebase, resulting in hacks and making it harder to understand, and the developer. In addition to all of these benefits, dependency injection also allows us to easily mock the frameworks. This allows us to be able to go faster in our tests, making it less time consuming to run the test suite, and not reliant on other things that could make tests become more inconsistent, such as backend servers. Mocks also allow us to more thoroughly test our code, especially with code that uses network calls. The reason it allows us to more thoroughly test our code is because we can not only test the golden happy path, we can test for all the different error states the app could be, and make sure that the application handles errors as appropriate.
2. Naming
Naming things appropriately allows us humans to reason about what is happening and how things interact with each other. There are a couple things to be aware of when naming things. There are 2 main things we will be naming: Objects and functions. Objects will include classes, structs, enums, and protocols, as well as type aliases, if you choose to use them. In general, we will be mostly using classes and structs in our codebase. Since structs are value types, they should represent more typically regular objects, i.e. Chair or Table. Classes have a lot more utility than the other types due to being a reference type and being able to utilize both inheritance and protocol implementation.
3. Readability
Very often you’ll find it’s difficult to read someone’s code, not because of the code complexity, but simply because of the layout. Even with comments things sometimes just don’t make sense when you’re jumping between files and up and down the code to find some poorly named and even worse placed function.
Refactoring gives you an opportunity not just to improve your code in terms of stability and dependencies, but also to make it easier for others to understand it as if they’re literally reading it. It’ll never be as simple as natural language, after all it’s not a novel, but just as we mentioned how proper naming conventions help, same applies to the flow of the code.
Grouping of methods that perform similar roles, or are related is a great practice, but usually can only be done with some delay and when you’re finished adding new ones, as you’ll very often write the code as needed for a particular feature you’re developing, fixing a bug you just discovered, or just plainly following your own train of thought.