Splitting applications into layers are intended to decouple the presentation layer from the logic of the application. This ensures that components only respond to information and user actions. This means that they are unaware of all the processes that are going on behind. It implies that, for example, the frontend won’t have to deal with the backend.
It's not a good design pattern that the components process data (injected with many services). Instead, these are encapsulated in other complex components.
To solve these problems, it is suggested that an application is divided in the following way:
- Presentation layer
- Abstraction Layer
- Core layer
The presentation layer is composed of smart and dumb components that define the view. The abstract layer represents a facade which is a middleware that abstracts the core view. The middleware is responsible for the application’s state and the asynchronous services.
The main role of the presentation layer is to display the UI and to respond to user interactions. In this layer, the visual skeleton of the application is set. It includes routing and data metathesis. Also in this layer, we keep the IU states (open/closed, enable / disabled).
In this layer, we do not want our components to have access to the logic of the application and process data, nor do we want to inject any service in them.
In this case, we will end up with a very large builder with a lot of dependencies.
This is how we get a knot of interactions, and of course, we do not want to do that.
In this layer, here we help abstraction layer that detaches the presentation layer and delegates any logic that lies behind.
Another important reason to use this architecture is that we do not want the presentation layer to control the App’s states as this is not its end purpose. Also, it does not need to know how to do it.
The main purpose of this layer is only to "present" and "delegate". This clear purpose of the layer allows us to:
- have a clear encapsulation
- have a certain ease of testing this layer
- work in parallel as we have an API from the abstraction layer
- keep the presentation layer decoupled from the App’s REST services
- make the REST services easier to change
How to structure the view of a presentation layer
Another important point on our list is how we structure the view of a presentation layer. In fact, in most cases, a view is made up of a series of components that interact interdependently. In order to have an easier control and a more scalable architecture, the components should be divided into two categories: Smart and Dumb components.
Smart components should have the role of a container, that also contains other smaller components. These containers are usually routing components. They inject the facade and must send the data to the dumb components but also respond to the events received from them.
Dumb or pure components must be the smallest components of a view/container. These only provide more specialized functions.
So they do not have to work directly with the facade to get data from it, but the data has to come from Inputs and emit Outputs events in response to the user interaction. To be even better, they must use the OnPush change detection strain.
The abstraction layer's main role is to decouple the core layer from the presentation layer (UI). This layer should expose an API for the presentation layer. Thus, for a particular situation or functionality, the layer must provide an API for which components must subscribe to receive the stream of states.
If we take into account that we use Redux (NgRx store - Angular package) for state management, we do not let the components directly manage the states. This will only be done through the API.
So this layer must be like a sandbox. It needs to dispatch actions and calls async services, handle the optimistic update. Also, it needs to know exactly what is done in a specific module.
This means that the components must focus on their data display responsibility, and they do not need to know what's going on behind when an entity is being updated. The same components do not need to know who provides the data for those streams and where do we save the final information.
The role of a core layer is to store the entire logic of the application. This is where all the data manipulations with Reducers and Actions, as well as the back-end communication are produced. Here you can expose all the global services, include internalization modules, configs, etc.
In a complex application, the components must communicate with each other. When a change occurs, several components should be updated. This should be the actual data-flow of an application.
In some (too many) cases, the following manners are used to implement these functions:
- the sibling components communicate with each other
- the parent components notify the child components that an action has occurred
- events are sent between components
- models are injected to send some states
This is a bad and poor design because we never know the data flow and in what state the application is at a particular point.
It is very difficult to maintain this code, to fix bugs and implement new features. For this reason, a scalable architecture usually uses unidirectional dataflow like Flux or Redux.
If we analyze this functionality on the diagram we will again notice that the application is divided into 3 different layers: core, abstract and presentation. Unidirectional data flow appears when from the component, actions go to the core and from there the data comes back.
Thus, for the application is very clear when the data comes, where it has to spread them and where to apply the change detection.
From the perspective of the presentation layer that is made up out of components, these can have inputs and outputs that send only some actions, and always wait for a new state to update the application.
Already at the presentation and core layer, a NgRx store should be implemented to handle state management. So at any action to the facade, you only have to select the states that are responsible for the changes made and sent to the core.
The Core is already working to update back-end data, store it and change the application state to transmit the data.
Thus, when a change is made to the application, all the application states are updated and all interested components (subscribed to this state) are “announced” about that change. For this reason, it is called “unidirectional dataflow”.
The advantages of this approach are:
- we can disconnect the components from each other
- the application becomes more manageable and easy to maintain
- implementing an application in real time is easier because it is in reactive software
- we can monitor with different tools the actions and states produced in the application
In the end, this is an approach that I’ve met in several courses and workshops, at ngViking and other events. I believe that such an architecture is necessary for a large commercial application, which should be as easy to maintain and develop as possible.
It takes a lot of time to implement this approach, but as you will notice, it is an approach for the ones that predict and embrace change and development.