Skip to content

Latest commit

 

History

History
184 lines (136 loc) · 8.19 KB

services-layer.asciidoc

File metadata and controls

184 lines (136 loc) · 8.19 KB

Services Layer

The services layer is more or less what we call 'business logic layer' on the server side. It is the layer where the business logic is placed. The main challenges are:

  • Define application state and an API for the components layer to use it

  • Handle application state transitions

  • Perform backend interaction (XHR, WebSocket, etc.)

  • Handle business logic in a maintainable way

  • Configuration management

All parts of the services layer are described in this chapter. An example which puts the concepts together can be found at the end Interaction of Smart Components through the services layer.

Boundaries

There are two APIs for the components layer to interact with the services layer:

  • A store can be subscribed to for receiving state updates over time

  • A use case service can be called to trigger an action

To illustrate the fact the follwing figure shows an abstract overview.

Smart and Dumb Components Interaction
Figure 1. Boundaries to components layer

Store

A store is a class which defines and handles application state with its transitions over time. Interaction with a store is always synchronous. A basic implementation using rxjs can look like this.

💡
A more profound implementation taken from a real-life project can be found here (Abstract Class Store).
Store defined using rxjs
@Injectable()
export class ProductSearchStore {

  private stateSource = new BehaviorSubject<ProductSearchState>(defaultProductSearchState);
  state$ = this.stateSource.asObservable();

  setLoading(isLoading: boolean) {
    const currentState = this.stateSource.getValue();
    this.stateSource.next({
      isLoading: isLoading,
      products: currentState.products,
      searchCriteria: currentState.searchCriteria
    });
  }

}

In the example ProductSearchStore handles state of type ProductSearchState. The public API is the property state$ which is an observable of type ProductSearchState. The state can be changed with method calls. So every desired change to the state needs to be modeled with an method. In reactive terminology this would be an Action. The store does not use any services. Subscribing to the state$ observable leads to the subscribers receiving every new state.

This is basically the Observer Pattern:
The store consumer registeres itself to the observable via state$.subscribe() method call. The first parameter of subscribe() is a callback function to be called when the subject changes. This way the consumer - the observer - is registered. When next() is called with a new state inside the store, all callback functions are called with the new value. So every observer is notified of the state change. This equals the Observer Pattern push type.

A store is the API for Smart Components to receive state from the service layer. State transitions are handled automatically with Smart Components registering to the state$ observable.

Use Case Service

A use case service is a service which has methods to perform asynchronous state transitions. In reactive terminology this would be an Action of Actions - a thunk (redux) or an effect (@ngrx).

Use Case Service
Figure 2. Use case services are the main API to trigger state transitions

A use case services method - an action - interacts with adapters, business services and stores. So use case services orchestrate whole use cases. For an example see use case service example.

Adapter

An adapter is used to communicate with the backend. This could be a simple XHR request, a WebSocket connection, etc. An adapter is simple in the way that it does not add anything other than the pure network call. So there is no caching or logging performed here. The following listing shows an example.

For further information on backend interaction see Consuming REST Services

Calling the backend via an adapter
@Injectable()
export class ProducsAdapter {

  private baseUrl = environment.baseUrl;

  constructor(private http: HttpClient) { }

  getAll(): Observable<Product[]> {
    return this.http.get<Product[]>(this.baseUrl + '/products');
  }

}

Interaction of Smart Components through the services layer

The interaction of smart components is a classic problem which has to be solved in every UI technology. It is basically how one dialog tells the other something has changed.

An example is adding an item to the shopping basket. With this action there need to be multiple state updates.

  • The small logo showing how many items are currently inside the basket needs to be updated from 0 to 1

  • The price needs to be recalculated

  • Shipping costs need to be checked

  • Discounts need to be updated

  • Ads need to be updated with related products

  • etc.

Pattern

To handle this interaction in a scalable way we apply the following pattern.

Interaction of Smart Components via services layer
Figure 3. Smart Component interaction

The state of interest is encapsualted inside a store. All Smart Components interested in the state have to subscibe to the store’s API served by the public observable. Thus, with every update to the store the subscribed components receive the new value. The components basically react to state changes. Altering a store can be done directly if the desired change is synchronous. Most actions are of asynchronous nature so the UseCaseService comes into play. Its actions are void methods, which implement a use case, i.e., adding a new item to the basket. It calls asynchronous actions and can perform multiple store updates over time.

To put this pattern into perspective the UseCaseService is a programmatic alternative to redux-thunk or @ngrx/effects. The main motivation here is to use the full power of TypeScript’s --strictNullChecks and to let the learning curve not to become as steep as it would be when learning a new state management framework. This way actions are just void method calls.

Example

Smart component interaction example
Figure 4. Smart Components interaction example

The example shows two Smart Components sharing the FlightSearchState by using the FlightSearchStore. The use case shown is started by an event in the Smart Component FlightSearchComponent. The action loadFlight() is called. This could be submitting a search form. The UseCaseService is FlightSearchService, which handles the use case Load Flights.

UseCaseService example

export class FlightSearchService {

  constructor(
    private flightSearchAdapter: FlightSearchAdapter,
    private store: FlightSearchStore
  ) { }

  loadFlights(criteria: FlightSearchCriteria): void {
    this.store.setLoadingFlights(true);
    this.store.clearFlights();

    this.flightSearchAdapter.getFlights(criteria.departureDate,
        {
          from: criteria.departureAirport,
          to: criteria.destinationAirport
        })
      .finally(() => this.store.setLoadingFlights(false))
      .subscribe((result: FlightTo[]) => this.store.setFlights(result, criteria));
  }

}

First the loading flag is set to true and the current flights are cleared. This leads the Smart Component showing a spinner indicating the loading action. Then the asynchronous XHR is triggert by calling the adapter. After completion the loading flag is set to false causing the loading indication no longer to be shown. If the XHR was successful, the data would be put into the store. If the XHR was not successful, this would be the place to handle a custom error. All general network issues should be handled in a dedicated class, i.e., an interceptor. So for example the basic handling of 404 errors is not done here.