OriginsLink to this section
My experience as a Software engineer taught me that a Startup product grows and develops every day. This means that the codebase as well, it’s inevitable.
If you don’t architect your codebase in a way that can allow changes, later on, you will pay an expensive price!
In software engineering, making things work the first time is always easy. But, what if you want to add new functionalities to an existing code? Making iterations on an existing basis can be difficult to do without introducing bugs. This becomes even more of an issue when many developers are working on the same project. If the project team doesn’t agree on strong software architecture to start with, your codebase can and will become messy. If you don’t have a set of predefined rules, your team will reach a point of no return. A point where maintaining the functionality of the whole application becomes difficult, not to say impossible. Especially, if you don't have the practice to write tests... but this is another topic.
Turns out, I got tired of being afraid to change or add a piece of code without breaking everything. I realized something needed to change and I started to learn software design, particularly SOLID principles.
SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible, and maintainable. The principles are a subset of many principles promoted by American software engineer and instructor Robert C. Martin (Uncle Bob) - Wikipedia
The primary benefits of a SOLID architecture are that you will write code:
- that is testable
- that is easy to understand
- that can be adjusted and extended quickly without producing bugs
- that separates the policy (rules) from the details (implementation)
- that allows for implementations to be swapped out
- where business logic is where it is expected to be
- where classes do what they are intended to do
S: Single Responsibility Principle - A class or function should only have one reason to change
O: Open-Closed Principle - A software artifact should be open for extension but closed for modification
L: Liskov-Substitution Principle - Introduced by Barbara Liskov in the 1980s. This principle defines that objects of a superclass shall be replaceable with objects of its subclasses without breaking the application. That requires the objects of the subclasses to behave in the same way as the objects of the superclass.
I: Interface Segregation Principle - Prevent classes from relying on things that they don’t need
D: Dependency Inversion Principle - Abstractions should not depend on details. Details should depend on abstractions
In this article, I will focus on the Open-Closed, Liskov-substitution, and Dependency Inversion principles and show you how you can leverage them in your Angular application. First thing first, we need a concrete example.
SpecificationsLink to this section
Let’s suppose that we are working on a brand new feature on a blog application. We want to add a page where we can:
- See the list of authors.
- When clicking on a specific author, it should make an API call to fetch the articles related to the author, and show them to the user.
- Filter this list of authors by topic and, by the total number of articles. For example, if one user wants to see authors that have written at least 4 articles, but at least 1 article related to the Angular topic.
- The list of authors can be huge, with thousands of results. We are experienced software engineers and we don’t want to overcharge the DOM. So, we add pagination to the list.
The endpoints used for the list of authors are:
Nothing complicated right? Let’s make it harder ?
On the same page, we want to be able to switch the displayed list, it can be a list of authors or, a list of articles. We still have the pagination and only the filter by topic should be available.
The endpoints used for the list of articles are:
Naive ApproachLink to this section
Let’s go through the code above:
- We are providing 2 different services:
apiServiceis responsible for doing the API requests to our backend.
storeServicethat is responsible for application state management. We can think of it as a simple RxJS BehaviorSubject store
getMainListreturns an Observable stream: anytime the
topic$emits a new value, it checks the current list level and performs the corresponding API request. Notice that, in the case of Authors level, we must subscribe first to
nbArticles$before sending the request
getArticlesByAuthorretrieves all the Articles related to a specific Author
showNbOfArticleschecks the current list level and decides if the number of articles button should be shown
- We can assume that all the subscriptions are made from the template using the async pipe
This code is fully reactive and works well, however, there are few points to be considered:
- The current list level is checked twice but, if more logic that depends on the list level is added, we will have to increase the number of checks. For example, let’s assume that we want to add a title to the list, we have no choice but to add a third check.
- What if we want to add another level to the “show by” dropdown, like grouping the authors by countries, or languages? We will have to check for this new value in all places where we are already checking for the current list level.
ListComponentwill end up with a lot of business logic. As a consequence, the code will produce more bugs, become less understandable, and less maintainable.
In one word,
ListComponent violates SOLID principles:
- Open-Closed principle - when we need to add new functionality, we must change existing code.
- Dependency Inversion principle - the code doesn’t depend on abstractions but implementation details.
We can do better so let’s fix this!
Abstraction and Dependency InjectionLink to this section
Abstract classes are mainly for inheritance where other classes may derive from them. We cannot create an instance of an abstract class. An abstract class typically includes one or more abstract methods or property declarations. The class which extends the abstract class must define all the abstract methods - TypeScript
The main idea behind Abstraction is to keep the policy separate from the implementation details to enable loose coupling.
In our example, we can create a
ListService abstract class that will define the policy and what is needed by
abstract keyword indicates what must be defined in the derived class.
Then we implement that policy using concrete classes. And of course, the implementation is different for
Notice that we are not implementing but extending
ListService. We do this to inherit shared functionality from
From now on, if some feature requires us to add another level, we just create a new service that extends the abstract
ListService. No more modification, only extension.
ListComponent looks cleaner right ?
Some key points to highlight:
- We use Dependency Injection and provide to
- We add a private property
ListServicewhich is the abstract class created above.
- Then, we define it using the
injector.get()helps us retrieving and returning an instance from the injector based on the provided token.
- Now, we check only once what is the current list level and get the right
ListServiceinstance. In plain English it is like saying to our component: “Hey, I want you to behave and configure yourself as a list of type A. So go and get the list configuration of type A from your node injector.”
- For that, we subscribe to
ListServiceinstances every time the
Thanks to this new implementation,
ListComponent now depends on
ListService abstraction and the implementation logic can be swapped out, whenever the current list level changes. We have just applied Dependency Inversion and Liskov Substitution Principles.
ConclusionLink to this section
Every software engineer should know and understand SOLID principles. It is a concept that helps to get a strong architecture foundation for any application.
Writing clean and SOLID code does increase the number of files introduced in your project because of abstractions. But, it enables any large team of developers to scale quickly, without producing bugs. Particularly in large enterprise applications where product requirements evolve every time.
You don't want to miss my next article? Follow me on Twitter.