How OOP is mistreated in Angular

Post Editor

Object oriented programming - a word combination instantly recognized by almost every software developer in the world; even the ones who don't use (or even hate) it.

7 min read
3 comments
post

How OOP is mistreated in Angular

Object oriented programming - a word combination instantly recognized by almost every software developer in the world; even the ones who don't use (or even hate) it.

post
post
7 min read
3 comments
3 comments

Object oriented programming - a word combination instantly recognized by almost every  software developer in the world; even the ones who don't use (or even hate) it. OOP is the most popular and widely used programming paradigm in the world; and the web frontend is no exception to this: React (still) has class-based components; Vue components are objects themselves; but the framework that makes the most use of OOP is, beyond doubt, Angular. Angular uses OOP for lots of its stuff; most of its features are based on classes (components, directives, pipes...), it uses approaches like dependency injection and such, and so on. So naturally, Angular is also the place where OOP gets mistreated the most. In this article, we are going to explore how sometimes people misuse OOP practices in Angular projects, and how we can do better.

Please think about mixins
Link to this section

Mixins are a feature of JavaScript (an, by extension, TypeScript) that allow us to write functions that receive a class (not an instance of a class, but the class itself), extend from it, and return the resulting class. This resulting class then can be used to extend another one of our classes, say a component. Here is an example of a mixin that creates a destroy$ Subject which then can be used to dispose Observable streams upon ngOnDestroy, so we don't have to write it manually every time:

<>Copy
function WithDestroy(Base) { return class extends Base implements OnDestroy { destroy$ = new Subject(); ngOnDestroy() { super.ngOnDestroy(); this.destroy(); } }; }

If we then extend some of our components from this mixin, we can use the destroy$ Subject with the takeUntil operator to unsubscribe automatically, and we won;t even have to implement the ngOnDestroy method.

If we then extend some of our components from this mixin, we can use the destroy$ Subject with the takeUntil operator to unsubscribe automatically, and we won’t even have to implement the ngOnDestroy method.

This works in a way as if we have a Base class that implements the ngOnDestroy method, and we extend our class that needs unsubscription logic from it. But because it is a function that accepts a class as an argument and extends from it, we can still extend our class from another one while using WithDestroy, creating some sort of multiple inheritance.

<>Copy
export class MyComponent extends WithDestroy(SomeOtherComponent) { constructor(private service: DataService) { super(); } ngOnInit() { this.subscription = this.service.selectSomeData().pipe( takeUntil(this.destroy$), // we can use this from the mixin class ).subscribe( // handle data ); } }


Mixins are a nice way to share functionality between classes without having to resort to classic inheritance or breaking inheritance chains. You can read more about mixins in my article Harnessing the power of Mixins in Angular.

Inheritance is not a toy
Link to this section

Inheritance, or the practice of extending one class from another, is the most well known (and probably the simplest) practice in OOP. But it gets some notoriety because of how it is being used in places it is not needed or applicable.

Inheritance is an is-a relationship, meaning if we extend class A from class B, we should be able to say A is a B in a way that would make sense. For example, if we extend class Car from class Vehicle, we can safely say Car is a Vehicle.

Content imageContent image
Simple class extension: a Car is a Vehicle

But sometimes relationships exist between classes which can be best described as a has-a relationship. For example, a Car has an Engine, but a Car is not an Engine, so it won't make sense to extend class Car from class Engine, despite the possibility that Car might access some methods and properties of an Engine.

Content imageContent image

Now, this does not make sense, because a Car merely has an engine, but does not have the properties of an Engine. Instead, we should use object composition and incorporate Engine as a nested property of a Car.

Content imageContent image

What gets messed up a lot is the fact that some developers might think "well, I need method  M from class B in class A, so I should extend A from B"; this, in 99 percent of cases, is a trap.

In those cases, it is useful to reevaluate the relationship and find out if the relationship is an is-like-a relationship. In our previously defined example the relationship was clearly not like that (a Car is not like an Engine). But in case of Angular services there might be cases when this can be applicable. For example, imagine a wrapper around HttpClient, and a data fetching service like ProductService. Clearly a data handling service is not a generic wrapper around HttpClient, it behaves like one, so inheritance can be considered in this scenario. It is important to be careful, and remember about DI and where it is more applicable than inheritance.

The main purpose of inheritance is not sharing functionality, but for representing relationships between data structures. But inheritance can be used in an is-like-a relationship scenario.

If we want to share functionality of class A in class B without inheritance, Angular has a remedy for that: dependency injection. We should inject class B in class A and use it through a provided reference.

Using classes everywhere
Link to this section

TypeScript has both classes and interfaces; usually we cannot live without classes, and if we have to choose, lots of developers often opt for using only classes, and not using interfaces at all. But this is a path to some nasty pitfalls. Imagine the scenario: we have a service that make a data call to get some Product data. HttpClient's methods are generic, so we can specify what the response data is. Naturally, we will write something like this:

<>Copy
@Injectable() export class ProductService { constructor( private readonly http: HttpClient, ) { } getProducts(): Observable<Product[]> { return this.http.get<Product[]>('/api/products'); } }

Now what is Product? Let's suppose it is a class:

<>Copy
export class Product { name: string; price: number; amountSold: number; }

Seems nice. So where is the problem? The backend does for sure return this precise object, so this should be working, right? Well, it will, until someone decides to interfere:

<>Copy
export class Product { name: string; price: number; amountSold: number; get total() { return this.price * this.amountSold; } }

As you can see, someone just added a getter method to our class, and this will prove to be a problem. Let's look at a component that uses our service and class:

<>Copy
@Component({ selector: 'app-product-list', template: ` <div *ngFor="let product of products"> <span>{{ product.name }} - {{ product.price }}</span> <span>{{ product.total }}</span> </div> `, }) export class ProductListComponent { products: Product[]; constructor( private productService: ProductService, ) {} ngOnInit() { this.productService.getProducts().subscribe(products => { this.products = products; }); } }

Now we can spot the problem: our service loads the products, but it does not ensure the data is 100 percent corresponding with what we have in our class, and the total getter method is not being added; so TypeScript won't catch an error, but we will end up with an empty slot in our UI where the total price should be.

With interfaces, though we can add method declarations too, developers are not tempted to do that unless the interfaces are then implemented by classes. Another approach is to use type definitions instead of interfaces:

<>Copy
export type Product = { name: string; price: number; amountSold: number; }

You can read more about differences between types and interfaces here.

Not using classes where necessary
Link to this section

Sometimes we need some utility functions in our applications to perform routing actions with data:

<>Copy
export function isObject(obj: any): obj is object { return obj !== null && typeof obj === 'object'; } export function isArray(obj: any): obj is any[] { return Array.isArray(obj); } export function copy<T>(obj: T): T { if (isObject(obj)) { return JSON.parse(JSON.stringify(obj)); } else { return obj; } } // and so on...

Of course, we might not know where exactly to put those, and just put them in some file named functions.ts for example, and be done with it.

This is something that should better be decided by you (and your team), as this comes both with tradeoffs and benefits. Using functions instead of classes allows for better tree-shaking, on the other hand, they might become harder to mock in unit testing.

Insanely large classes
Link to this section

This has been said over and over, but even the most experienced developers fall into this trap: sometimes our components contain large amounts of logic, all of which is necessary. Huge classes are harder to read, reason about and test. Angular codebases are especially guilty of this; mainly because components are very often pages (as in routing), and modern web pages can contain ridiculous amounts of logic, checks and stuff that will make our classes bloated. So it is a good practice to break down our components into smaller classes, each with a specific responsibility (single responsibility principle). But we should remember to start breaking down only when an existing class grows larger, and only start with a broken down approach right away only if we know that the very first implementation of the class is going to be large by design. Usually a good approach to this is to watch when a class becomes difficult to work with and refactor.

Don’t optimize prematurely

Having services for specific components
Link to this section

What we often encounter in Angular projects is when specific components have kind of dedicated services for themselves, for example, a HomePageComponent might have a HomePageService that specifically loads all the data required by the home page. While this sounds like a nice separation of concerns, it still tightly couples the HomePageComponent with the service. In the future, it is possible we might need some methods from that service in, say, SideBarComponent, which will be strange at the very least - home age might not even be loaded, but its dedicated services will already be working because of the sidebar. Also, it kind of obscures the purpose of the service: it is not readily apparent what  sort of data the HomePageService is working with. So, a better approach would be creating different services working with specific sorts of data and business logic; for example, we might have a UserService, a PermissionsService, a ProductService, and so on, and use them in appropriate components when necessary.

In Conclusion
Link to this section

In this article, we have explored some common malpractices with OOP in Angular codebases. While there are lots of other programming paradigms (some also used by Angular projects like function programming, reactive programming and so on), OOP has a central place in the Angular ecosystem, so using it correctly is paramount if we want to achieve flexibility and maintainability.

Comments (3)

authorvdlindk
11 August 2021

Hello Armen,

First of all thanks form the great article. Ik do have one remark regarding 'a specific service for a component'. Ik use this (anti)-pattern a lot. I use it for every container , but I make sure it is provided at the container itself. With this approach my container-component has a clear specification, while the implementation of this specification is nicely encapsulated in a service. I agree that from an OOP point of vue that this service and component are strongly coupled (I place them in the same folder), but this pattern introduces in my opinion some nice advantages regarding the maintenance and testability of your application.

authorArmenvardanyan95
11 August 2021

Thanks for appreciation!

About your remark: but what if you need the data from service A in component B? Do you replicate the code (violating DRY) or do you move that logic to another Service C and inject C in A and B? And if the latter is true, does it not even furthr worsen the strong coupling now introducing coupling between A component and B component too?

authorezzabuzaid
11 August 2021

Nice article!

Regarding Using classes everywhere, I believe this is more of a mapping issue rather than interface vs class. the developer should map the returned data to the appropriate type before using it. and sometimes the returned data fields cannot be used as they are, for instance, due to naming convention, so the developer has to map them.

authortalamaska
27 September 2021

I tend to use mostly interfaces, not classes for data mapping. If I need to massage the data, then probably I might consider it. But i don't really like to see too much new Data() all over the place.

Share

About the author

author_image

Senior Angular developer from Armenia. Passionate about WebDev, Football, Chess and Music

author_image

About the author

Armen Vardanyan

Senior Angular developer from Armenia. Passionate about WebDev, Football, Chess and Music

About the author

author_image

Senior Angular developer from Armenia. Passionate about WebDev, Football, Chess and Music

Featured articles