Angular and SOLID principles

Post Editor

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 is where SOLID principles come into play.

8 min read
0 comments
post

Angular and SOLID principles

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 is where SOLID principles come into play.

post
post
8 min read
0 comments
0 comments

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.

Specifications
Link 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:

  • GET /api/authors
  • GET /api/authors/:id/articles
  • GET /api/authors?first=1&last=10&topic=angular&nbArticles=4

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:

  • GET /api/articles
  • GET /api/articles?first=1&last=10&topic=angular
Application MockupApplication Mockup

Naïve Approach
Link to this section

<>Copy
enum ListLevel { AUTHORS = 'AUTHORS', ARTICLES = 'ARTICLES' } interface Pagination { first: number; last: number; } @Component({ selector: 'list', templateUrl: 'list.component.html', providers: [ApiService, StoreService] }) export class ListComponent implements OnInit { public showNbOfArticlesDropdown$: Observable<boolean>; public mainList$: Observable<Authors[] | Articles[]>; public listLevel$: Observable<ListLevel>; public pagination$: Observable<Pagination>; public topic$: Observable<string>; public nbArticles$: Observable<number>; constructor( private apiService: ApiService, private storeService: StoreService ) { this.listLevel$ = this.storeService.select('listLevel'); this.pagination$ = this.storeService.select('pagination'); this.topic$ = this.storeService.select('topic'); this.nbArticles$ = this.storeService.select('nbOfArticles'); } ngOnInit(): void { this.mainList$ = this.getMainList(); this.showNbOfArticlesDropdown$ = this.showNbOfArticles(); } private getMainList(): Observable<Authors[] | Articles[]> { return combineLatest( this.listLevel$, this.pagination$, this.topic$, ).pipe( switchMap(([listLevel, pagination, topic]) => { const { first, last } = pagination; switch (listLevel) { case ListLevel.AUTHORS: return this.nbArticles$.pipe( switchMap(nbArticles => { return this.apiService.fecthAuthors({first, last, topic, nbArticles}) }) ); case ListLevel.ARTICLES: return this.apiService.fecthArticles({ first, last, topic }); default: break; } }) ) } private getArticlesByAuthor(authorId: number): Observable<Articles[]> { return this.apiService.fetchArticlesByAuthor(authorId); } private showNbOfArticles(): Observable<boolean> { return this.listLevel$.pipe( map(listLevel => { switch (listLevel) { case ListLevel.AUTHORS: return true; case ListLevel.ARTICLES: return false; default: break; } }) ) } }

Let’s go through the code above:

  1. We are providing 2 different services:
    • apiService is responsible for doing the API requests to our backend.
    • storeService that is responsible for application state management. We can think of it as a simple RxJS BehaviorSubject store
  2. getMainList returns an Observable stream: anytime the listLevel$, pagination$, or 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
  3. getArticlesByAuthor retrieves all the Articles related to a specific Author
  4. showNbOfArticles checks the current list level and decides if the number of articles button should be shown
  5. 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:

  1. 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.
<>Copy
this.listTitle$ = this.listLevel$.pipe( map(listLevel => { switch (listLevel) { case ListLevel.AUTHORS: return 'Trending authors that published few minutes ago!'; case ListLevel.ARTICLES: return 'Best articles for you!'; default: break; } }) );
  1. 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.
  2. ListComponent will 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:

  1. Open-Closed principle - when we need to add new functionality, we must change existing code.
  2. 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 Injection
Link 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 ListComponent. The abstract keyword indicates what must be defined in the derived class.

<>Copy
import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { ApiService } from '../api.service'; import { StoreService } from '../store.service'; import { PaginationModel } from '../../models/pagination.model'; import { ListLevel } from '../../models/list-level.enum'; import { AuthorModel } from '../../models/author.model'; import { ArticleModel } from '../../models/article.model'; export type ListType = AuthorModel[] | ArticleModel[]; @Injectable() export abstract class ListService { public listLevel$: Observable<ListLevel>; public nbArticles$: Observable<number>; public pagination$: Observable<PaginationModel>; public topic$: Observable<string>; abstract listTitle: string; abstract showNbOfArticlesDropdown: boolean; constructor( protected apiService: ApiService, protected storeService: StoreService ) { this.listLevel$ = this.storeService.select('listLevel'); this.nbArticles$ = this.storeService.select('nbOfArticles'); this.pagination$ = this.storeService.select('pagination'); this.topic$ = this.storeService.select('topic'); } abstract getList(): Observable<ListType>; }

Then we implement that policy using concrete classes. And of course, the implementation is different for AuthorsList and ArticlesList.

<>Copy
import { Injectable } from '@angular/core'; import { combineLatest, Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { AuthorModel } from '../../models/author.model'; import { ListService } from './list.service'; @Injectable() export class AuthorsListService extends ListService { public listTitle = 'Weekly best authors!'; public showNbOfArticlesDropdown = true; public getList(): Observable<AuthorModel[]> { return combineLatest([this.pagination$, this.topic$, this.listLevel$]).pipe( switchMap(([{ first, last }, topic]) => { return this.nbArticles$.pipe( switchMap((nbArticles) => { return this.apiService.fetchAuthors({ first, last, topic, nbArticles, }); }) ); }) ); } }
<>Copy
import { Injectable } from '@angular/core'; import { combineLatest, Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { ArticleModel } from '../../models/article.model'; import { ListService } from './list.service'; @Injectable() export class ArticlesListService extends ListService { public listTitle = 'Interesting articles!'; public showNbOfArticlesDropdown = false; public getList(): Observable<ArticleModel[]> { return combineLatest([this.pagination$, this.topic$, this.listLevel$]).pipe( switchMap(([{ first, last }, topic]) => { return this.apiService.fetchArticles({ first, last, topic }); }) ); } }

Notice that we are not implementing but extending ListService. We do this to inherit shared functionality from StoreService.

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.

Extend Abstract ServiceExtend Abstract Service
<>Copy
import { Component, Injector, OnInit } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; import { tap } from 'rxjs/operators'; import { ListLevel } from './models/list-level.enum'; import { ArticleModel } from './models/article.model'; import { ApiService } from './services/api.service'; import { ListService, ListType } from './services/list/list.service'; import { AuthorsListService } from './services/list/authors-list.service'; import { ArticlesListService } from './services/list/articles-list.service'; @Component({ selector: 'list', templateUrl: './list.component.html', styleUrls: ['./list.component.less'], providers: [AuthorsListService, ArticlesListService], }) export class ListComponent implements OnInit { public listTitle: string; public showNbOfArticlesDropdown: boolean; public list$: Observable<ListType>; private listService: ListService; private subscriptions = new Subscription(); constructor(private injector: Injector, private apiService: ApiService) { this.listService = this.listServiceFactory(ListLevel.AUTHORS); } ngOnInit(): void { this.subscriptions.add( this.listService.listLevel$ .pipe( tap((listLevel: ListLevel) => { this.listService = this.listServiceFactory(listLevel); this.initListData(); }) ) .subscribe() ); } public getArticlesByAuthor(authorId: number): Observable<ArticleModel[]> { return this.apiService.fetchArticlesByAuthor(authorId); } private initListData(): void { this.list$ = this.listService.getList(); this.listTitle = this.listService.listTitle; this.showNbOfArticlesDropdown = this.listService.showNbOfArticlesDropdown; } private listServiceFactory(listLevel: ListLevel): ListService { switch (listLevel) { case ListLevel.AUTHORS: return this.injector.get(AuthorsListService); case ListLevel.ARTICLES: return this.injector.get(ArticlesListService); } } }

The new ListComponent looks cleaner right ?

Some key points to highlight:

  • We use Dependency Injection and provide to ListComponent both, AuhorsListService and ArticlesListService
  • We add a private property listService of type ListService which is the abstract class created above.
  • Then, we define it using the listServiceFactory function. 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 ListService instance. 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 listLevel$ and swap ListService instances every time the listLevel changes.

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.

Conclusion
Link 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.

Comments (0)

Be the first to leave a comment

Share

About the author

author_image

Software engineer - Angular addict @Contentsquare

author_image

About the author

Samuel Teboul

Software engineer - Angular addict @Contentsquare

About the author

author_image

Software engineer - Angular addict @Contentsquare

Featured articles