Blog

Reactive Encapsulation Pattern in Angular

Category
Software development
Reactive Encapsulation Pattern in Angular

To explore an alternative approach to encapsulating data models in Angular using TypeScript, we will focus on separating two crucial components: the Layout and the Model Data Hub. We’ll see how to create simple yet highly decoupled code, showcasing its effectiveness in minimizing code while maximizing functionality.

We want to have a simple way of storing data so that its details are completely hidden from layout components. In other words, Layout should talk only with the Model. We’ll enable easy application extension with new data models by defining a base model superclass and interfaces. All we need to create is a Model, while everything else is covered by abstractions.

Our good friends, inheritence, polymorphism, and encapsulation will attribute to the resulting code being:

  • easy to use
  • adapt to changes quickly
  • enable adding new features with minimal effort

Reactive Encapsulation Design Pattern Overview

Design Pattern Overview

This pattern enables significant simplicity to the Layout, which is only coupled with the Model, thereby achieving greater decoupling. Consequently, we have achieved increased code reusability through abstractions, which we’ll discuss shortly.

Let’s delve into the “Layout,” establishing the flow in which the Angular component should render and display data, such as a list of products.

  1. Layout: Component initiating data display request.
  2. Model: Receives a request from the Layout and sends it to the API service.
  3. API service: Receives a request from the Model and sends it to the Backend API.
  4. Backend API: Returns data to the API Service.
  5. API service: After receiving a response from the Backend API, sends a request to the Store to update the state.
  6. Store: Receives a request to update the state from the API Service –>Stores the data list to the application state. Returns reactive state to API service. 
  7. API service: Sends the stored data list to the Model.
  8. Model: Sends the stored data list to the Layout.
  9. Layout: Renders the data according to the desired client design.

Every entity object in Layout is also an instance of a model. So, in addition to requesting a list, it can also send requests to fetch individual items, edit and delete items, use methods on any object from the list, and subscribe reactively to the local state of the Store. 

It’s important to mention that all three elements, Model, API Service, and Store, have an inherited abstraction that provides the same interface for each Model and its capabilities, facilitating and speeding up development with reusability and adding new models. A slightly more detailed diagram looks like this:

Model_API_Store_Reactive Encapsulation Pattern

Practical Application of Reactive Encapsulation Pattern

Now we can take a look how our design pattern looks in the real world.

The Product Model provides a straightforward internal API used by the Layout component, ProductListComponent.

@for (product of (products | async); track $index) {
  <div>
    <h3>{{ product.name }}</h3>
    <p>{{ product.description }}</p>
  </div>
}Code language: HTML, XML (xml)
@Component({
   …
  })
export class ProductListComponent {
  products: Observable<IProduct[]> = new Product().fetchAllSelect$();
…Code language: JavaScript (javascript)

By creating a new instance, `new Product()`, we can access encapsulated methods inherited from the abstract superclass `BaseModel`. It allows us to use methods that will handle the ‘dirty work’ in the background and deliver the requested data to the Layout through a subscription to changes.

The implementation of the `fetchAllSelect$()` method looks like this: fetching a list of Product items from the server and returning the list, propagating changes of items from the Store through an Observable:

As part of the BaseModel abstraction:

fetchAllSelect$(): Observable<T[]> {
  return this.#apiService.list();
}Code language: PHP (php)

We can notice that the Model calls the `list()` method from the API service, whose implementation simplifies it:

As part of the ProductApiService abstraction:

// The method for fetching the list of entities.
list(): Observable<T[]> {
  return this.http.get<T[]>(this.#buildUri()).pipe(
    tap((resp) => {
      this.store.replaceList(resp);
    }),
    switchMap(() => this.store.selectAll$),
  );
}Code language: PHP (php)

After successfully fetching, it sends a request to the Store to update the state and reactively subscribes to the select observable from the Store. This observable contains the list of entities and will return all entities once they are populated in the Store.

Common Data Control

Let’s dive a bit deeper and see other advantages. The diagram below has an additional set of utilities available for every model instance. Every Model encapsulates Common Data Control that implements methods for comparison, value copying, difference extraction, and serialized object cloning. 

Common Data Control

The interface with Common Data Control serves as the encapsulation hub for handy utilities, showcasing a great example of polymorphism. It’s like your Swiss Army knife for working with models. Here are a few cool things it can do:

/**
* Compares property names and replaces values for matched property names.
* @param {object} data - Another object with shared properties.
*/
copyValuesFrom(data: any): void;
/**
* Extracts different values from another object and returns the property keys in a * string array, suitable for extracting properties for the patch.
* @param object - Another object to compare values with.
* @returns string[] - An array of property keys with different values.
*/
extractDifferentValuesFrom(object: any): string[];
/**
* @param object - object to compare to.
* @param escapeProps, list of properties which will be skipped.
* @returns true if objects are equal, otherwise false.
*/
compareWith(object: any, escapeProps?: string[]): boolean;
/**
* Clones the current object into a new object reference in serialized form.
* @returns serialized object.
*/
cloneSerialized(): Partial<T>;Code language: PHP (php)

Effortless Comparisons

It helps you compare certain Base Model instances with another object effortlessly. It even allows you to ignore some properties. It’s handy with forms when you want to quickly and easily check if the Model has changed and, if so, save it to the server.

productFormOutput(product: Product): void {
 if(!product.compareWith(this.clonedProduct)){
   // objects have different values 
   // The form has changed, save the product to server.
  }
}Code language: JavaScript (javascript)

We want to compare the object from a form with a cloned object for differences to ensure the object has changed before taking further action:

Efficient Value Copying

Any object literal that shares the same property names can be easily taken, extracted, and copied, retaining only matching member values. It handles everything from dates to deep copies of those complex objects. One use case could be an untyped form responding to an object literally. Another use case could be a layer of security that validates JSON data from the API to ensure we receive only the members we need for specific models. Imagine a serialized object that comes from an unreliable source. Here, we can use the copyValuesFrom method to ensure the object adheres to its interface.

const serializedData = {
 name: 'item',
 description: 'item description',
 alienProperty: false // will not be copied, because the Product does not have that property
};
const product = new Product().copyValuesFrom(serializedData);
// product will copy only properties that match.Code language: JavaScript (javascript)

Spot-On Difference Detection

This method is beneficial when we have models with many members, i.e., large objects. For example, if only a small data set is changed, we don’t want to use PUT, but prefer PATCH and send only the modified values. This method reduces traffic, speeds up the application, and enhances security.

productFormOutput2(product: Product): void {
 if(!product.compareWith(this.clonedProduct)){
   // objects have different values so let's see properties with changes
	
  const diff = product.extractDifferentValuesFrom(this.clonedProduct);
  // use diff to create PATCH, if you have large objects
  }
}Code language: JavaScript (javascript)

Clone Crafting

This method is useful when we want to quickly and painlessly create a clone of an object and break the memory reference, i.e., convert an instance of the Model into an object literal. For example, we can make a copy of an object that we send to a form for later comparison to determine which values have changed. It works excellently in synergy with the `compareWith` and `extractDifferentValuesFrom` methods.

this.clonedProduct = this.product.cloneSerialized();
// clonedProduct could sit and wait for further comparison.Code language: JavaScript (javascript)

So, think of Common Data Control as your coding sidekick, making your life easier when dealing with models.

Comparison with Other Patterns

Reactive Encapsulation Pattern approach and its efficiency compared to other well-known patterns:

Service Layer Pattern:

  • Pros: Clear separation of responsibilities between Layout components, Model, API service, and Store, following the Service Layer Pattern. This organization facilitates maintenance and testing.
  • Cons: The Service Layer Pattern can become complex as functionality grows, while the REP approach aims to maintain simplicity.

Model-View-Controller (MVC):

  • Pros: Offers a simpler code organization than a clear division of responsibilities between Model, API service, and Store.
  • Cons: MVC can become complicated in large projects, while the Reactive Encapsulation Pattern brings a more transparent structure and reduces complexity.

Model-View-ViewModel (MVVM):

  • Pros: The REP shares similarities with the MVVM approach, especially in state management and one-way data flow.
  • Cons: In this case, the REP can be more flexible and simpler to implement in certain situations. Also, MVVM may have more layers that can increase complexity.

Flux Architecture:

  • Pros: The REP shares one-way data flow with Flux architecture but may be simpler to implement.
  • Cons: Flux can provide more robust tools for managing complex states, but with the REP, you can achieve similar benefits with fewer additional components.

Key Components Structure

Repository File Structure:

src/
  app/
    layout/
      ~components
    model-data-hub/
      _dependencies/ // abstractions, helpers, etc
      product/ // example of a model
        product.model.ts
        product.store.ts
        product.api-service.ts
  assets/
  environment/Code language: JavaScript (javascript)

Model diagram structure

If we envision a product that includes only essential members. We aim to avoid the complexity of creating duplicate code every time, emphasizing the importance of defining just the Model. In this case, the product will encapsulate only fundamental properties.

Model Diagram Structure

Layout data fetch structure

Envisioning the perspective from the Layout, we can observe that it utilizes, or rather creates, a new instance of the Product class. This resolves the issue of the Layout being coupled. 

Imagine a scenario where we need to add an extra method to retrieve the top 5 products, requiring a new approach to connect to the API. In this situation, we will add that method to the ProductApiService, seamlessly extending functionality without disrupting the ApiAbstractionService and all the CRUD operations it provides. This ensures the ability to expand modularly and harmoniously without compromising the existing system’s integrity.

Layout Data Fetch Structure Angular

Repository

Explore the REP in action by checking out the code repository. The repository includes comments and explanations within the code to provide clarity on how each part supports the design pattern.

Reactive Encapsulation Pattern Conclusion

Accelerated development after the initial setup means that developers can focus their time and resources on implementing business logic and innovation rather than reconfiguring basic parts of the system. 

Keep in mind that this pattern is not a universal solution and not suitable for all types of projects. We need to carefully consider the needs of each individual project. Project’s specifics, team size and application requirement all are important in finding the best approach.

Reactive Encapsulation design pattern is best applied in situations where the application is focused on consuming a backend REST API that provides most CRUD endpoints. In such scenarios, the pattern brings simplicity, code cleanliness, and efficiency in state management, making the development of Angular-based applications easier.

Inspiration for this pattern comes from studying and combining best practices in OOP, clean code, SOLID principles, security layers, separation of concerns, best practices in modularity, and data reactivity in Angular. Conceptually, it’s nothing new, but in the Angular world, approaches like this are not mainstream due to the rapid development of other libraries, often adopted even though they cannot be fully relied upon or do not cover all segments and project needs.

Please share your experiences and feedback on REP! Your input can significantly contribute to refining REP and making it more accessible and efficient for the broader development community. Feel free to visit the GitHub repository and join the conversation. Your insights are valuable!

Cover Illustration: AI Generated

CONTACT US

Exceptional ideas need experienced partners.