Chapter 1. Angular Follows Common and Familiar Enterprise Patterns and Conventions
Just a few short years ago, the rhetoric around frameworks was pretty inflammatory, with the general theme being “My framework can beat up your framework.” This negative dialogue was distracting at best, and at the end of the day, it was the end users who suffered.
There has been a distinct shift in thinking from “How do I make my framework win?” to “How do we make the web win?” This has led to some incredible collaborations between major frameworks and technologies that have produced some very powerful tools for everyone to use.
In this chapter, you’ll learn that Angular follows common and familiar enterprise patterns and conventions that support small teams as well as large organizations with several teams. These patterns and conventions include the use of the TypeScript language, a superset of JavaScript, and aspect-oriented programming using dependency injection. The use of TypeScript cannot be overstated; this is what enables the Angular framework to take advantage of tomorrow’s language improvements today. TypeScript is an open source project by Microsoft, and is created in collaboration with other open source projects and maintainers including Google.
TypeScript
The Angular team was originally going to create its own superset of JavaScript called AtScript so that it could have the tools it needed to create the kind of modern framework it envisioned. After researching the amount of effort it would take to support not only a framework but a language, the team realized that TypeScript was doing most of the things they originally set out to accomplish.
Through a joint collaboration, the TypeScript team began to integrate the feature requests that the next version of Angular needed. The most notable feature to come out of this effort was the ability to decorate an ECMAScript 6 (ES6) class with metadata and bridge the gap between the language and the framework in an unobtrusive manner. It is because of this that Angular has a small footprint in the source code of a project with which a developer directly interacts.
It was also from this collaborative effort that they were able to calibrate Angular to maximize the full power of TypeScript. After months of developing Angular applications with TypeScript, it is difficult to imagine not using types, field assignment, constructor assignment, decorators, destructuring, interfaces, and so on.
Metadata
So, if everything is a class, how does Angular know how to interact with it? The answer to that is metadata.
As Figure 1-1 shows, Angular uses metadata that is specified using a TypeScript decorator. We can take an ordinary ES6 class and by applying a decorator to the class, the Angular compiler will know how to convert that class into something the application can use. In most cases, the metadata is used to connect a component class to its template and stylesheets while telling Angular what HTML selector will be used to add it to the Document Object Model (DOM).
In the case of an Angular service, our metadata becomes even smaller because we simply need to annotate it so that it is compiled correctly. We accomplish this by using the injectable decorator. The advantage of using metadata is that Angular is able to compile a much more verbose version of the source code behind the scenes. It is possible to do this by hand in ECMAScript 5 (ES5), but the code volume is significantly higher and tedious to work with.
Note
As of this writing, decorators in TypeScript are currently being proposed to become an official part of the ECMAScript standard.
Constructor Assignment
TypeScript allows us to bypass some manual wiring-up of our code through constructor assignment, as demonstrated in Example 1-1. In ES6, if we wanted a parameter to become a local member, we had to create a local variable and assign that parameter to it. Although this is trivial to write and maintain, this pattern is very common and repetitive in nature. By using an access modifier on the constructor parameter, TypeScript will automatically assign that parameter to a local member. The access modifiers supported by TypeScript are public, private, and protected. The private and protected modifiers enable developers to encapsulate data within a class that is not exposed or available for modification outside of the class. This convenience saves us from having to manually create these assignments and adds up to huge time savings over the course of a project life cycle.
Example 1-1. TypeScript constructor assignment
import
{
Component, OnInit
}
from
'@angular/core'
;
import
{
ActivatedRoute
}
from
'@angular/router'
;
import
{
User
}
from
'../../models/user.model'
;
import
{
UserService
}
from
'../../services/user.service'
;
@Component
({
selector:
'app-users'
,
templateUrl:
'./users.component.html'
,
styleUrls:
[
'./users.component.css'
]
})
export class
UsersComponent
implements
OnInit
{
/** The user to display */
user
:
User
;
constructor
(
private
readonly activatedRoute
:
ActivatedRoute
,
private
readonly userService
:
UserService
) {}
ngOnInit() { const PARAM_ID = 'id'; let id: string; if (this.activatedRoute.snapshot.paramMap.has(PARAM_ID)) { / id = this.activatedRoute.snapshot.paramMap.get(PARAM_ID); }
if
(id
) {this
.user
=this
.userService.getUserById
(id
);} } }
Object-Oriented Programming
Enterprise developers who are familiar with languages such as Java, Kotlin, and C# are often put off by the lack of structure and the asynchronous nature of JavaScript. Attempts at forcing a classical paradigm onto JavaScript have historically led to code bloat, over-engineering, and, ultimately, brittle code. The latest JavaScript standards have addressed this frustration head on by providing class-based object-oriented language constructs, as illustrated in Example 1-2, that look and feel like real classes, such as Kotlin’s class syntax presented in Example 1-3.
Example 1-2. Object-oriented programming principles in TypeScript
import
{
Component
,
OnDestroy
,
OnInit
}
from
'@angular/core'
;
import
{
MatSnackBar
}
from
'@angular/material'
;
import
{
Subject
}
from
'rxjs'
;
import
{
takeUntil
}
from
'rxjs/operators'
;
import
{
Notification
}
from
'./services/notification.interface'
;
import
{
NotificationsService
}
from
'./services/notification.service'
;
@Component
({
selector
:
'app-root'
,
templateUrl
:
'./app.component.html'
,
styleUrls
:
[
'./app.component.css'
]
})
export
class
AppComponent
implements
OnDestroy
,
OnInit
{
/** The application title */
title
=
'Angular REST App'
;
/** Top-level navigation */
links
=
[
{
path
:
'/home'
,
icon
:
'home'
,
label
:
'Home'
},
{
path
:
'/items'
,
icon
:
'list'
,
label
:
'Items'
},
{
path
:
'/widgets'
,
icon
:
'view_quilt'
,
label
:
'Widgets'
},
{
path
:
'/profile'
,
icon
:
'face'
,
label
:
'Profile'
}
];
/** Unsubscribe from observable streams */
private
unsubscribe
=
new
Subject
();
constructor
(
private
snackbar
:MatSnackBar
,
private
notificationService
:NotificationsService
)
{}
ngOnDestroy() {
this
.
unsubscribe
.
next
();
this
.
unsubscribe
.
complete
();
}
ngOnInit() {
this
.
notificationService
.
notifications$
.
pipe
(
takeUntil
(
this
.
unsubscribe
));
.
subscribe
(
notification
=>
this
.
showNotification
(
notification
))
}
showNotification
(
notification
:Notification
)
{
this
.
snackbar
.
open
(
notification
.
body
,
'OK'
,
{
duration
:3000
});
}
}
Example 1-3. Object-oriented programming principles in Kotlin
class
MyViewModel
:
ViewModel
()
{
fun
launchDataLoad
()
{
viewModelScope
.
launch
{
sortList
()
// Modify UI
}
}
suspend
fun
sortList
()
=
withContext
(
Dispatchers
.
Default
)
{
// Heavy work
}
}
Addressing the topic of inheritance and its challenges is beyond the scope of this discussion, but the organizational capabilities of ES6 are compelling. We are now able to cleanly organize our code into standalone units, expose data as properties, and perform actions with methods. Taking this a step further, TypeScript expands upon what is possible by allowing us to take advantage of additional classical techniques in our applications. Finally, because TypeScript is a superset of ES6, it can be safely transpiled to ES5 and executed in all of the latest browsers.
One of the major benefits of the classic object-oriented approach in ES6 and TypeScript is the ability to hire and utilize existing experienced engineers who are familiar with these patterns but might not be familiar with the Angular framework and associated application programming interfaces (APIs). This is a big win for organizations that might be transitioning away from outdated enterprise solutions that often do not provide a highly functional user experience (UX) and are not delivered via the web.
Although the existing engineering workforce in a large organization might not be familiar with Angular, they have retained large amounts of business logic, the details, and structure of existing applications; thus, they are able to put their knowledge of the organization to work immediately. Hiring the best engineers is without a doubt a valuable business advantage when competing nationally or globally. Whether traditionally learned via a computer science degree, a trade learning program, or a coding bootcamp, software engineers are widely accustomed to object-oriented programming principles.
Interfaces
One of the strongest cases for TypeScript is its ability to communicate intent. This enables the use of interfaces to establish code contracts about how things are supposed to behave within an application. For instance, we can use an interface to establish a contract with a plain old JavaScript object (POJO) to classify it as a specific entity within our domain model. As a result, anything interacting with that object can safely make assumptions about that object because it adheres to the interface. If a component class implements an interface, we are required to honor the contract of that interface, which allows us to safely make assumptions about how it is supposed to work, as illustrated in Example 1-4.
Example 1-4. Angular uses interfaces in TypeScript; in this example the class implements both the OnDestroy and OnInit interfaces
import { Component, OnDestroy, OnInit } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] })
export class
AppComponent
implements
OnDestroy, OnInit {
constructor() {}
ngOnDestroy() {
// this method must be implemented
}ngOnInit() {
// this method must be implemented
}}
By programming to an interface, we are now able to use classic design patterns because we no longer have the obligation of programming to concrete implementations.
Field Assignment
In ES6, local members must be defined in the constructor so that they are available when the class is created. TypeScript affords us the convenience of defining these members outside of the constructor, which is familiar to developers who are already using classic languages such as Java or C#, as shown in Example 1-5.
Example 1-5. TypeScript classes include member properties that are easy to access and mutate
...
constructor
(private
readonly activatedRoute: ActivatedRoute,
private
readonly userService: UserService
) {}ngOnInit() { const PARAM_ID = 'id'; let id: string;
if (this
.activatedRoute.snapshot.paramMap.has(PARAM_ID)) { id = this.activatedRoute.snapshot.paramMap.get(PARAM_ID); }
if
(id
) {this
.user
=this
.userService.getUserById(id); }
} }
Dependency Injection
The degree of brittleness of a software system has a direct relationship to how tightly its internal components are coupled. High cohesion makes it very difficult to interact with individual elements in isolation, and it dramatically increases the potential for unintended side effects, as demonstrated in Example 1-6.
Example 1-6. Angular uses aspect-oriented programming dependency injection via the class constructor function
...
constructor
(private
readonly activatedRoute: ActivatedRoute,
private
readonly userService: UserService
) {}ngOnInit() { const PARAM_ID = 'id'; let id: string;
if (this
.activatedRoute.snapshot.paramMap.has(PARAM_ID)) {
id =this
.activatedRoute.snapshot.paramMap.get(PARAM_ID);
}if
(id) {this
.user
=this
.userService.getUserById(id)
; }} }
Composition
Aspect-oriented programming allows us to break a tightly coupled system by providing a class with only the dependencies it requires via dependency injection. Satisfying these dependencies is no longer the responsibility of the class itself; rather, it is delegated up to a higher abstraction. This enables the mechanism responsible for dependency injection to make critical design decisions at runtime depending on the current context of the environment. For instance, depending on the role of a user, the dependency injection system could inject a specific role-based dependency determined by the user’s access level. We can also utilize the power of dependency injection during testing in order to prevent side effects or CPU-intensive calculations.
Mocking
Expanding on this concept of composition, we can also affect how a component is initialized during testing. When we are testing a component, the goal is to test it in isolation, without having to focus on additional factors or side effects in the application. Dependency injection allows us to inject test doubles, or mocks, into our component to stand in for the real thing. This allows us to simplify standing up a component while controlling the environment and the data that it has available to it during testing.
Take note of the line in which the TestBed
’s providers are declared in the code sample shown in Example 1-7 from a test written for Angular. The dependency injection framework that Angular ships with enables the developer to override the class that is provided to the constructor function of the class that is instantiated. Using this technique, we can provide a specific mock, or stub, shown in Example 1-8, in place of the real thing.
Example 1-7. Angular enables developers to test in isolation by using mocks with dependency injection
import
{ComponentFixture, TestBed, async } from
'@angular/core/testing';
import
{RouterTestingModule } from
'@angular/router/testing';
import
{UserService } from
'../../services/user.service';
import
{UserServiceMock } from
'../../services/user.service.mock';
import
{UsersComponent } from
'./users.component';
describe
('UsersComponent'
, () => {let
component: UsersComponent;
let
fixture: ComponentFixture<UsersComponent>; beforeEach
(async
(() => {TestBed.configureTestingModule({ imports: [RouterTestingModule], declarations: [UsersComponent], providers: [{ provide: UserService, useClass: UserServiceMock }] }).compileComponents();
})); beforeEach(() => { fixture = TestBed.createComponent(UsersComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); });
Example 1-8. An example mock service that extends the concrete injectable service
import
{
Observable
,
of
}
from
'rxjs'
;
import
{
User
}
from
'../models/user.model'
;
import
{
UserService
}
from
'./user.service'
;
export
class
MockUserService
extends
UserService
{
constructor
()
{
super
();
}
getUserById
(
id
:string
)
:
Observable
<
User
>
{
return
of
({
firstName
:
'Brian'
,
lastName
:
'Love'
});
}
}
The ability to override providers during runtime by using dependency injection in the Angular framework enables developers to exclusively assert the code under test.
Angular is built with testing at the forefront. Built-in stubs and mocks exist for many of the dependencies that developers rely on, including the router and the HTTP client. Further, developers can quickly and easily create mocks, stubs, and spies by using Jasmine and the TestBed
provided by Angular. This is critical for writing unit tests that ensure the stability of enterprise applications.
Reactive Extensions for JavaScript
Angular was built to be entirely reactive. By that, we mean that when state changes in one place, the rest of the application will predictably react to that change and update itself accordingly. The architecture to support this naturally organizes itself into a tree of components with state flowing from top to bottom. The ability for state to effortlessly flow through the application is facilitated by asynchronous and event-based observable sequences. Angular uses the popular Reactive Extensions for JavaScript (RxJS) library that implements the observer design pattern.
Reactive programming using observables is a powerful concept on its own that provides us with the ability to not only communicate asynchronous events, but transport and manage encapsulated state at the same time. Then we add in a very large set of operators provided by RxJS that allow us to declaratively transform our data as it moves from one place to another, and we have the power to handle complex sequences without even touching Angular. And the amazing part of this entire situation is that Angular is designed to let you use the power of observables without fighting the framework at all.
By working with the RxJS team, we have observables integrated directly into the framework so that we can use them to consume data via an HTTP call, capture form data as an observable stream of asynchronous data, and even bind directly to observables in our templates.
We can even go so far as to turn off change detection within parts of our applications and completely rely on the internal observable event system to communicate state change. This enables massive performance gains for large enterprise applications. Change detection can be resource intensive and result in perceived slowness by the user. It is not uncommon for large enterprise applications to display, and possibly provide the ability to modify, very large sets of data. Displaying and modifying large sets of data in the DOM of a web application can result in long change-detection cycles that hinder the application and, in the end, the efficiency of the user.
Under the hood, Angular uses optimized differ functions to determine when a change to the state of an application has occurred. Although these functions are optimized, when dealing with large datasets, robust computing is required to analyze and determine the changes that are occurring in an application. Solely utilizing the internal observable event system built around RxJS streams, we can instruct the Angular framework to leave the heavy lifting of change detection to the notification of new events and values from the observable stream.
Note
Observables are currently being proposed for adoption into the ECMAScript standard library. At the time of publication, observables are a stage 1 draft before the TC 39 committee within the ECMA organization.
Summary
Angular is indeed a next-generation platform that is purposefully built for enterprises with large development teams building applications that are critical to the success and profitability of the organization. In this chapter, we highlighted the three primary reasons Angular follows common and familiar enterprise patterns and conventions: the TypeScript language, dependency injection, and Reactive Extensions for JavaScript. Together, Angular enables enterprises to build applications at scale, and that scale.
Get Why Angular for Enterprise Development now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.