We have all been there. The app that once was this little cute baby has grown to this huge monster no one wants to get near. Every day you fight this monster but it only seems to get bigger, because your project keeps feeding it with short-term optimization by focusing on the next feature or deadline. The day you realize that you are spending all this time working with this confusing codebase, you go tell your boss why new features should be postponed and instead the team should spend time on refactoring instead. Unless you have a tech-savvy boss, they normally don’t like to have features postponed in favor of refactoring, and they might tell you to just keep focusing on creating new features.
What should your team do to keep the codebase clean?
First of all, asking your boss for permission to refactor the codebase is normally not something you should do because:
- Refactoring should be part of your craft just like a dentist clean their equipment after use, they shouldn’t ask you if you want clean equipment
- Prevention is better and cheaper than fixing, so you should always opt to keep the code clean and not let dirty code slip into the master branch
- Refactoring should be a done-criteria of every feature. By incorporating refactoring into the feature development, you don’t need to ask your boss/client for permission. You are simply practicing your craft like a professional
Of course, things don’t always go like this, so you would want to know what to do when shit has hit the fan. In this post, I will go through how to refactor Angular apps, so they become easy to work with.
How to Refactor Angular Apps
To understand how to refactor Angular Apps we are gonna look at two levels of abstraction; one is the architecture of the app (what belong to what file) and the other is how to keep the code clean inside the files.
This video is demonstrating my process of how to Refactor Angular Apps.
Refactoring for a Clean Architecture of an Angular App
We want to ensure a nice foundation for our app before we look at the details in the same way that you would want to have a nice foundation for a house before you fix the walls.
How should the App be structured?
For the best overview and scalability, you should structure the app in feature folders. This ensures proximity of files that belong to a feature. When you are working on a given feature it is neat to have these files located in the same folder. I even like to do this for Redux apps where I like to put the Redux files in a folder called
Smart and dumb components
You should opt for keeping components stateless and dumb and instead use services to encapsulate state and business logic as your application grows.
There are two kinds of component smart components and dumb components.
Smart components or controller components are the parent components controlling the child (dumb) components. On the other hand, dumb components or presntation components only interact using input/output and has the sole purpose of presenting something. Smart components are delegating logic to services and presentation to dumb components. Smart components should be small and simple because they delegate all the logic and presentation. If smart components start growing in complexity, the proper refactoring is to push the logic into a service, dedicated for that smart component to manage all the state and logic. Dumb components or presentation components are the components that only contain input and outputs and are used for presenting something in the dom.
Let’s look at an example of a to-do app structured this way:
The todo-list component is the smart component and the add-todo folder contains a dumb component for adding TODOs. The todo-list component has a todo-list service for business logic and HTTP calls.
Knowing how the ideal architecture of an Angular app is, how are we transforming a legacy application to this?
The Flow of Refactoring the Architecture of Angular Apps
To understand how we are gonna transform a legacy application into a clean architecture we can follow the below flow:
By looking at the flow we see that the first step is to make our app distinguish between smart and dumb component. We are gonna do this by starting at an outer leave (dumb components) of the component tree and ensure that these components are only getting data from input and emitting events through output. The exception here is if you are using redux, then you might be using selectors and actions for this instead. After this step, your dumb component should be more maintainable because… they are dumb = simple. But your smart components are still too huge to easily grasp. We are gonna fix that by moving the logic and state from the smart components to a dedicated service for that component. I really like this pattern because the components become very readable and the use cases become very clear without all the business logic. The last part is when your smart component service becomes too big, you would want to break this into smaller services. Usually, if prefer all files to be no longer than 500 lines. It’s up to you to decide the right services to create, but the key point is to find to group/theme of methods and put them in the same service so you obtain good cohesion.
Refactoring Code For Readable Methods and the three-second rule
Now that we have an architecture in place that easily conveys the intent of the components and has pushed logic into services we are gonna look at actually making the methods in the components and services more readable. This part is based on knowledge gained from the books Code Complete and Clean Architecture, which I can recommend for more information about writing clean code.
Making the code clean
We should aim to make every method in the codebase so readable that they follow the three-second rule: You should be able to understand what a method does within three seconds of looking at it.
How do we make the code obey the three-second rule?
To make the code obey the three-second rule we should focus on self-explaining method names and variables, a consistent level of abstraction and separating query and command.
Self-explaining method names
A good method name explains what it does without you having to dig into the code of the method. It should explain the consequence of calling the method and what the method returns.
Example: Bad method name
Example: Good method name:
Self-explaining variable names
Similar to method names, variable names should explain the value the variable holds. A well-named variable name can be very good to eg. simplify boolean conditionals.
Example: Bad variable name
Example: Good variable name:
Consistent level of abstraction
To obey the three-second rule we need to keep the method small and at a consistent level of abstraction. That means that you should not mix high-level methods with low-level implementation code in the same method. Instead, the method should consistently work at the same level of abstraction.
An example of this would be to do http request in a method that was also calling high abstraction use case methods.
Separating query and command
For making the control flow of the code easy to grasp, it is recommended to separate querying of data by modifying or creating data in separate method calls. That way you can easily see what part of the code has side-effects and which part is simply querying data.
The process of writing clean code
- Writing the pseudo code as comments in the code before writing the actual code
- Replace the comments with well-named methods
- Implement the methods using the above guidelines
Following this process will ensure well-named methods, consistent level of abstraction and separation of query and command. All this will ensure that that is code is readable to the point of obeying the three-second rule.
In this post, we went through how to keep the code base clean. It is preferable to prevent unclean code by making it part of the development process as a contrast to needing to convince your boss/client why you should spend time refactoring the code instead of creating new features.
We looked at to levels of refactoring: the architecture level and on the code level. On the architecture level, we saw that we should keep components stateless and dumb and instead use services to contain logic as your application grows. That way, we made our architecture convey the uses cases clearly.
On the code level we saw how to make the code obey the three-second rule by making the method and variable self-describing, keeping methods small and on a consistent level of abstraction and ensuring a clear control flow by separate side-effect free query operations from commands, which modify or create data. We saw a simple three-step process to writing clean code, that will help you ensure that your code obeys the three-second rule.