Book Image

Architecting Angular Applications with Redux, RxJS, and NgRx

Book Image

Architecting Angular Applications with Redux, RxJS, and NgRx

Overview of this book

Managing the state of large-scale web applications is a highly challenging task with the need to align different components, backends, and web workers harmoniously. When it comes to Angular, you can use NgRx, which combines the simplicity of Redux with the reactive programming power of RxJS to build your application architecture, making your code elegant and easy to reason about, debug, and test. In this book, we start by looking at the different ways of architecting Angular applications and some of the patterns that are involved in it. This will be followed by a discussion on one-way data flow, the Flux pattern, and the origin of Redux. The book introduces you to declarative programming or, more precisely, functional programming and talks about its advantages. We then move on to the reactive programming paradigm. Reactive programming is a concept heavily used in Angular and is at the core of NgRx. Later, we look at RxJS, as a library and master it. We thoroughly describe how Redux works and how to implement it from scratch. The two last chapters of the book cover everything NgRx has to offer in terms of core functionality and supporting libraries, including how to build a micro implementation of NgRx. This book will empower you to not only use Redux and NgRx to the fullest, but also feel confident in building your own version, should you need it.
Table of Contents (12 chapters)

Model-View-Controller – the pattern we all know

Regardless of whether you have been a programmer for a year or 20 years, you have almost certainly encountered the MVC pattern in some way, shape, or form. The pattern itself, MVC, consists of three interconnected parts: model, view, and controller. More important than knowing all its parts is knowing what problem it solves. It solves the problem of separation of concerns by decoupling view logic, data logic, and business logic. The MVC pattern has given rise to, among others:

  • Model-View-Adapter (MVA)
  • Model-View-Presenter (MVP)
  • Model-View-ViewModel (MVVM)

Cohesion and coupling – establishing a common language

Without a pattern like MVC, your code could turn out to be hard to maintain as it could have low cohesion and high coupling. Those are fancy words, so what do we mean? Cohesion is about focus and what the class should do. The lower the cohesion, the more different things are performed by a class and therefore it has no clear intention of what it should perform.

The following code shows what happens when a class has low cohesion; it does a lot more than storing data about an invoice, such as being able to log to a file or talk to a database:

Invoice
details
total
date
validate()
print()
log()
saveToDatabase()

Now we have introduced new dedicated classes and moved methods out of the Invoice class to make sure that each and every class now has high cohesion, that is, is more focused on doing one thing well. We therefore now have the classes Invoice, Printer, Logger, and InvoiceRepository:

Invoice
details
total
date
validate()

Printer
print(document)

Logger
log()

InvoiceRepository
saveToDatabase(invoice)

The point I am trying to make here is that a class should only do one thing well. This is illustrated by the unfocused Invoice class being split into four different classes that each do only one focused thing well.

So that deals with cohesion/focus. What about coupling? Coupling is about how strongly connected a software element is to another software element. Ultimately, the higher the coupling, the harder/more tedious it is to change. Let's look at the following example of high coupling written in Java:

// cohesion-and-coupling/invoice-system.java

class Printer {
print(Invoice invoice) {
String total ="";
total += invoice.getTitle();
total += invoice.getDetails();
total += invoice.getDate();
//print 'total'
}
}

class Invoice {
String title;
String details;
int total;
Date date;
public String getTitle() { return this.title; }
public String getDetails() { return this.details; }
public String getDate() { return this.date; }
}

public class Program {
private Printer printer = new Printer();
public void run(ArrayList list) {
for(int i=0; i< list.length; i++) {
Object item = list.getItem(i);
if(item instanceof Invoice) {
Invoice invoice = (Invoice) item;
printer.print(invoice);
}
}
}

public static void main(String [] args) {
ArrayList list = new ArrayList();
list.add(new Invoice());
Program program = new Program();
program.run( list );
}
}

There are multiple problems with this code, especially if you aim to change the code in any way. Let's say we wanted to print an email as well. It is tempting to think we would need an Email class and need to add another print() method override to the Printer class. We would also need to add branching logic to the Program class. Furthermore, testing the Program class cannot be achieved without causing a side-effect: calling the run() method would cause an actual call to a printer. The way we tend to work with tests nowadays is to run our tests every time the code changes, which it might do quite a lot as we are developing our program. We might end up with thousands of printed papers just developing our code. For that reason, we need to isolate ourselves from side effects when developing code and tests. What we want to test at the end of the day is that our code behaves correctly, not that the physical printer seems to work.

In the following code, we see an example of high coupling. We add another type, Email. The purpose of doing that is to see the effects of doing so, which is that we need to add code to several places at once. Having to do so is a sign of a code smell. The fewer changes you need to make, the better it usually is:

// cohesion-and-coupling/invoice-systemII.java

class Email {
String from;
String to;
String subject;
String body;
String getSubject() { return this.subject; }
String getFrom() { return this.from; }
String getTo() { return this.to; }
String getBody() { return this.body; }
}


class Invoice {
String title;
String details;
int total;
Date date;
String getTitle(){ return this.title; }
String getDetails() { return this.details; }
Date getDate() { return this.date; }
}

class Printer {
print(Invoice invoice) {
String total ="";
total += invoice.getTitle();
total += invoice.getDetails();
total += invoice.getDate();
//print 'total'
}

print(Email email) {
String total ="";
total += email.getSubject();
total += email.getFrom();
total += email.getTo();
total += email.getBody();
}
}

class Program {
private Printer printer = new Printer();
run(ArrayList list) {
for(int i=0; i< list.length; i++) {
Object item = list.getItem(i);
if(item instanceof Invoice) {
Invoice invoice = (Invoice) item;
printer.print( invoice );
} else if( item instanceof Email ) {
Email email = (Email) item;
printer.print( email );
}
}
}

public static void main(String [] args) {
ArrayList list = new ArrayList();
list.add( new Invoice() );
list.add( new Email() );
Program program = new Program();
program.run( list );
}
}

So let's rearrange the code a bit:

// cohesion-and-coupling/invoice-systemIII.java

class Email implements IPrintable {
String from;
String to;
String subject;
String body;
String getSubject() { return this.subject; }
String getFrom() { return this.from; }
String getTo() { return this.to; }
String getBody() { return this.body; }
public String getContent() {
String total = "";
total += email.getSubject();
total += email.getFrom();
total += email.getFrom();
total += email.getBody();
return total;
}
}

class Invoice implements IPrintable {
String title;
String details;
int total;
Date date;
String getTitle() { return this.title; }
String getDetails() { return this.details; }
String getDate() { return this.date; }
public
String getContent() {
String total = "";
total += invoice.getTitle();
total += invoice.getDetails();
total += invoice.getDate();
return total;
}
}

interface IPrintable {
String getContent();
}


interface IPrinter {
print(IPrintable printable);
}

class Printer implements IPrinter {
print( IPrintable printable ) {
String content = printable.getContent();
// print content
}
}

class Program {
private IPrinter printer;
public
Program(IPrinter printer) {
this.printer = printer;
}

run(ArrayList<IPrintable> list) {
for(int i=0; i< list.length; i++) {
IPrintable item = list.getItem(i);
printer.print(item);
}
}

public static void main(String [] args) {
ArrayList<IPrintable> list = new ArrayList<IPrintable>();
Printer printer = new Printer();
list.add(new Invoice());
list.add(new Email());
Program program = new Program(printer);
}
}

At this point, we have made our program open to extension. How can we say that, you ask? Clearly, we have removed the printer methods from printer. We also removed the switch logic from the method run in the Program class. We have also added the abstraction IPrintable, which makes anything printable responsible for telling a printer what the printable content is.

You can clearly see how we went from high coupling to low coupling when we introduced the types Document and Note. The only change they cause is themselves being added and implementing the IPrintable interface. Nothing else has to change. Success!

// invoice-systemIV.java

class Document implements IPrintable {
String title;
String body;

String getContent() {
return this.title + this.body;
}
}

class Note implements IPrintable {
String message;

String getContent() {
return this.message;
}
}

// everything else stays the same

// adding the new types to the list
class Program {
public static void main(String[] args) {
list.add(new Note());
list.add(new Document());
}
}

OK, so to sum up our changes:

  • We added the IPrintable interface
  • We simplified/removed the branching logic in the Program.run() method
  • We made each printable class implement IPrintable
  • We added some code at the end of the previous snippet to demonstrate how easy it would be to add new types
  • We injected an IPrinter through the Program class constructor to ensure that we can easily test the Program class

In particular note that we did not need to change any logic in either Printer or Program, when adding the Document and Note types. The only thing we needed to do was add Document and Notes as classes and ensure they implemented the IPrintable interface. To put emphasis on this, any addition to a program should not lead to an overall system change in the code.

Let's reiterate the last bullet of adding IPrinter. Testability is a very good measurement to see whether your code has low coupling. If you depend on abstractions rather than actual classes, you are able to easily switch out one concrete class for another, while maintaining high-level behavior.

Another reason for switching Printer to IPrinter is so that we remove side effects from the program when we test our code. Side effects are when we talk to files, mutate states, or talk over the network for example. Testing the Program class means we want to get rid of a side effect such as actual printing and have it call something fake, or we would have a large stack of papers every time we run our tests. So to instantiate our Program class for the purposes of testing, we would write something like this instead:

// cohesion-and-coupling/invoice-systemV.java

class FakePrinter implements IPrinter {
print(IPrintable printable) { System.out.println("printing"); }
}

class Program {
FakePrinter fakePrinter;
Program(FakePrinter fakePrinter) {
this.fakePrinter = fakePrinter;
}

public static void main(String[] args) {
ArrayList<IPrintable> list = new ArrayList<IPrintable>();
Printer printer = new FakePrinter();
list.add(new Invoice());
list.add(new Email());
Program program = new Program(printer);
}
}

What we see from this code is how we shift from instantiating the Printer class (which prints to a real printer) to the Program class using an instance of FakePrinter. In a testing scenario, this is exactly what you would do, if wanting to test the Program class. What you most likely care about is the print() method being called with the correct arguments.

OK, so this was a pretty long way of expressing what low coupling is about. It is, however, important to establish what crucial terms such as coupling and cohesion are, especially when talking about patterns.

Explaining the components of MVC

Back to the MVC pattern. Using said pattern means we get high cohesion and low coupling; this is due to code being split into different layers with different responsibilities. View logic belongs in views, controller logic in controllers, and model logic in models.

The model

This is the crucial part of the application. This does not rely on any specific user interface but more defines the domain in which you operate. Rules, logic, and data live here.

The view

This can be anything from a native app view to a bar chart, or even a web page. The point is that it ultimately displays data from the model. There can be different views displaying the same thing, but depending on for whom they are designed, they might look different. An admin might see a totally different view than a user for the same information.

The controller

This is really the spider in the web. It is able to take input from the view or from the data and turn it into commands.

Interactions – the behavior between the components

All these three mentioned components act in different ways when talking to each other. A model stores data it is being given from the controller based on commands. A view changes its appearance based on changes happening in the model. A controller can send a command to the model based on a user interaction. One such example is a user deciding to browse between page-based records. A new set of data will need to be retrieved based on the new visual position.

These two basic flows are what mostly happens in an application-based on MVC:

  • User interaction: Controller sends command to Model => Model changes => View is updated
  • View asks for data: Controller sends command to Model => Model is created/changed => View is updated

MVC summary

A lot can be said about MVC and its many variants, but let's be content with what we have for now by summarizing the properties of the pattern that we identified:

  • Low coupling
  • High cohesion, separating presentation concerns from the model
  • Simultaneous development is possible; due to the existence of many layers, people can work in parallel on a task
  • Ease of change; because of how things are separated, adding future concepts or making alterations becomes easier