What Makes an Application Complex

An application can be described as complex when it exhibits one or more of the following characteristics:

  • Multiple components in the component tree that render the same slice of application state
  • Multiple sources that can modify the application state, such as:
    • Several users interacting with the application concurrently
    • Backend services delivering live state updates to the client
    • Background processes running on a schedule
    • Sensors on the device, including proximity detectors
  • Application state that gets refreshed with high frequency
  • A considerable number of components
  • Components with extensive code, echoing the Big Ball of Mud pattern often seen in legacy AngularJS controllers
  • Significant cyclomatic complexity within components — a dense concentration of conditional branches or asynchronous flows

At the same time, we aspire to have an application that is easy to maintain, easy to test, easy to scale and responsive.

It is uncommon for complex applications to possess all of these desirable traits. We cannot eliminate every complex characteristic while still fulfilling advanced project demands, but we can structure our code so that the valuable traits are maximized.

Dividing Responsibilities

Separation of concerns (SoC) can be viewed as a way to compartmentalize our codebase. We organize logic according to the system responsibility so that we can address one issue at a time. At the highest level, this is an architectural practice. In the daily workflow, it becomes an almost instinctive understanding of where each piece of code belongs.

Example of horizontal layers in a modern web application.

Example of horizontal layers in a modern web application.

Our applications can be divided vertically, horizontally, or in both directions. A vertical division groups software elements based on feature. A horizontal division groups them according to software layer. Within our code, we can assign software elements to these horizontal layers, or system responsibilities:

Horizontal layers of a web application.

This principle extends to our Angular components as well. They should be limited to handling the visual representation and user interaction layers. Carrying this out loosens the connections between the various parts of our system.

Admittedly, maintaining this separation requires rigorous effort because we are introducing more layers of abstraction, but the quality benefits we gain in the end make it worthwhile. Remember that we are merely establishing abstractions that ought to have existed from the outset.

Understanding Model-View-Presenter

Model-View-Presenter (frequently shortened to MVP) is an architectural pattern used for crafting the user interface (UI) of an application. Our goal is to keep intricate logic out of classes, functions, and modules (collectively, software artifacts) that are difficult to test. Specifically, we aim to keep complexity away from UI-specific artifacts like Angular components.

Model-View-Presenter, much like the Model-View-Controller pattern it descends from, keeps presentation apart from the domain model. The presentation layer keeps track of domain changes by relying on the Observer Pattern, as defined by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (known as “The Gang of Four”) in their seminal work, “Design Patterns: Elements of Reusable Object-Oriented Software”.

Within the Observer Pattern, a subject keeps a record of observers and informs them whenever its state changes. Does this concept sound familiar? You are correct — RxJS is built upon the Observer Pattern.

The view is devoid of logic or behavior, except for its data bindings and widget arrangement. It hands over control to a presenter when the user performs an action.

The presenter groups state changes so that when a user completes a form, it results in a single, comprehensive state update instead of several minor ones, such as updating the state once for the whole form rather than once for each field. This approach greatly simplifies undoing or redoing state changes. The presenter triggers a state update by sending a command to the model. The view then reflects the updated state via Observer Synchronization.

Adapting the Pattern for Angular

Model-View-Presenter can be combined with Angular.

Model-View-Presenter can be combined with Angular.

Drawing from the original Model-View-Presenter pattern and its many variants, we will design software artifacts that are well-suited to the Angular platform and its key UI construct, the component.

In an ideal scenario, an Angular component is concerned solely with presentation and user interaction. In practice, it requires strict vigilance to guarantee that our components are only involved in showing the user a portion of the application state and letting them modify it.

The Model-View-Presenter adaptation presented in this discussion is based on the Encapsulated Presenter Style. Still, our presenters will not hold a reference to their corresponding view. Instead, we will link the presenter to the model and the view through observables. This design enables presenters to be tested in isolation from the view.

When we apply the Model-View-Presenter pattern, we often employ the Supervising Controller approach. Our views (Angular components) simply depend on their presenter to manage user interactions. Given that our presenters are bound to their view, both data and events inevitably flow through the component model.

With the component model in play, our presenter transforms the user's action into an event specific to that component. This event is subsequently changed into a command that gets dispatched to the model. The last part of this transformation is managed by container components, which will be explained shortly.

Our presenter will share some attributes with a Presentation Model. It will handle presentational logic, such as exposing a boolean or an observable to specify if a DOM element should be disabled. Another illustration is a property that dictates the color in which a DOM element should be painted.

The view connects to the presenter’s properties to simply reflect the state it stands for, avoiding any extra logic. The outcome is a slim component model and a straightforward component template.

Key MVP Concepts in Angular

To implement the Model-View-Presenter pattern in an Angular app, we will adopt concepts that draw heavily from the React community. For the purpose of this series, we will classify our components into one of these three types:

React developers have been separating presentational and container components from their mixed counterparts for a long time. We can apply these same ideas to our Angular projects. Beyond this, we will bring in the notion of presenters.

Presentational Components

Presentational components are strictly for display and interaction. They show the user a slice of the application state and permit them to change it.

Apart from presenters, these components have no awareness of the rest of the application. They expose a data binding interface that clarifies the user actions they manage and the data they require.

To significantly reduce the need for unit tests on the UI, we strive to keep the complexity of presentational components as low as possible, both concerning the component model and the component template.

Container Components

Container components provide presentational components with slices of the application state. They connect the presentational layer to the rest of our app by converting component-specific events into commands and queries for the underlying non-presentational layers.

We typically maintain a 1-to-1 relationship between a container component and a presentational component. The container's class properties align with the input properties of its presentational component, and its methods react to the events emitted via the presentational component's output properties.

Mixed Components

If a component doesn't fit the definition of either container or presentational, we label it a mixed component. In an existing codebase, you'll likely find that the majority of components fall into this category. They are called mixed because they blend system responsibilities — incorporating logic that spans several horizontal layers.

Don't be taken aback if you encounter a component that, alongside holding an array of domain objects for display, also accesses the device camera, makes HTTP requests, and stores app state using WebStorage.

While this type of logic is anticipated in any application, gathering it all in one location makes the code hard to test, difficult to follow, tricky to reuse, and tightly coupled.

Presenters

Behavioral logic and intricate presentational logic are moved out into a presenter to achieve a simple presentational component. Presenters have no UI and typically have few or no injected dependencies, which makes them simple to test and understand.

A presenter rarely knows about the rest of the application's architecture. Generally, only a single presentational component will reference a given presenter.

The MVP Triad

Alt Text

The Model-View-Presenter triad for an Angular application.

These three artifacts come together to form what we call a Model-View-Presenter triad. The model — embodied by container components — is the application state that the user views and modifies.

The view, embodied by presentational components, acts as a lightweight user interface that shows the application state and translates user actions into events relevant to the component, typically passing control on to the presenter.

The presenter is often a simple class instance that remains unaware of the rest of the application.

Understanding the flow of data and events

To understand how the Model-View-Presenter triad operates, it helps to trace the journey of both data and events through the component tree.

Data moves downward through the component hierarchy

Figure 2. Data flow starting at a service and ending in the DOM.

Figure 2. Data flow starting at a service and ending in the DOM.

Figure 2 begins with a state change occurring within a service. The container component is aware of this change because it has subscribed to an observable property exposed by the service.

Upon receiving the emitted value, the container component reshapes it into a format that is most suitable for the presentational component. Angular then assigns this new value and reference to the input properties bound on the presentational component.

From there, the presentational component hands the updated data over to the presenter. The presenter is responsible for recalculating any derived properties that the template of the presentational component relies upon.

At this point, the downward journey of the data is complete. Angular renders the updated state to the DOM, presenting the user with the refreshed list.

Events move upward through the component hierarchy

Figure 3. Event flow starting with a user interaction and ending in a service.

Figure 3. Event flow starting with a user interaction and ending in a service.

In Figure 3, a user activates a button. Because of an event binding in the template, Angular hands control over to an event handler within the presentational component model.

The presenter intercepts this user interaction. It translates the raw event into a structured data format and emits it through an observable property. The presentational component model is listening for this emission and then passes the value along via an output property.

Angular, in turn, notifies the container component about the value emitted from the component-specific event, having detected it through the event binding present in its own template.

Now that the event has finished moving up the component tree, the container component converts the data structure into arguments that it passes to a method on the service.

When a command to alter the application state is executed, the service often broadcasts the resulting state change through its observable properties. This initiates the familiar downward flow of data, just as we saw in Figure 2.

One might look at the resulting UI architecture and dismiss it as an exercise in over-engineering. However, what we have actually created is a collection of simple, focused software modules. This modularity is the key to what we might call agility. We are not referring to the agility of a specific process or ceremony, but rather an agility that is measured by the cost of change. By being proactive rather than reactive in our approach to shifting customer requirements, we avoid building up a mountain of technical debt. Reaching this level of agility is nearly impossible if the system is tightly coupled, difficult to test, and requires months of effort to refactor.

A modular software architecture enables us to be agile.

We are able to respond to changing customer requests proactively, rather than reactively. If we had instead built a tightly coupled system with poor testability, one that takes months to refactor, this level of agility would be unattainable.

Maintainability

While the overall system is composed of many interconnected components, each individual component is remarkably simple and is dedicated to a single concern. This is helped by a clear and established convention for where each type of logic belongs.

Testability

We deliberately minimise the amount of logic contained within Angular-specific software artifacts, as these can be difficult and slow to test. Since each piece of software is focused on a single concern, its behaviour is straightforward to reason about. This clarity allows us to write automated tests that easily verify our assumptions.

User interfaces are notoriously difficult and slow to test, and Angular is no exception. With the Model-View-Presenter pattern, we reduce the logic in our presentational components to the point where they are hardly worth testing. We can even choose to skip unit testing them altogether, relying instead on our development tooling, integration tests, and end-to-end tests to catch simple errors such as typos, syntax mistakes, or uninitialised properties.

Scalability

Features can be developed independently of one another. Even software artifacts across different horizontal layers can be developed and tested in isolation. We always know precisely where any piece of logic should reside.

This ability to develop layers in isolation allows us to differentiate between technical and visual front-end development. One developer may excel at implementing behaviour with RxJS, another might have a passion for back-end integration, and yet another might focus on perfecting design and accessibility with CSS and HTML.

Since features are developed in isolation, tasks can be divided among different teams. In an e-commerce system, for instance, one team could handle the product catalogue while another addresses new features or bug fixes for the shopping cart.

Performance

A proper separation of concerns generally results in high performance, particularly in the presentation layer. Performance bottlenecks are easy to locate and isolate.

By using the OnPush change detection strategy, we can minimise the performance impact that Angular's change detection cycles have on our application.

The “Tour of Heroes” tutorial application.

The “Tour of Heroes” tutorial application.

We pick up where the Angular.io "Tour of Heroes" tutorial ends. This is a good starting point because it is a tutorial that many Angular developers already know.

The components in the final Tour of Heroes tutorial code are all mixed components. This is evident from the fact that none of them have output properties, yet several of them alter the application state.

In the related articles, we will apply the Model-View-Presenter pattern to a selection of these components, step by step, with plenty of hands-on code examples. We will also discuss which behaviours are appropriate to test within a Model-View-Presenter triad.

It is worth noting that we are not changing any features or behaviour of the application. We are simply refactoring its Angular components into more specialised software artifacts.


While these articles only cover a subset of the Tour of Heroes components, I have applied the Model-View-Presenter pattern to the entire application. You can find the complete project, including test suites for container components and presenters, in this GitHub repository.


Prerequisites

Aside from the concepts presented here, you will only need to be familiar with a few core Angular concepts. The specifics of the Model-View-Presenter pattern will be covered in depth in the related articles.

A good understanding of Angular components is assumed, particularly data binding syntax and input and output properties. You will also need basic RxJS knowledge—a working familiarity with observables, subjects, operators, and subscriptions.

Our isolated unit tests will stub service dependencies with Jasmine spies. You don't need a deep understanding of stubs or other test doubles. Instead, focus on the test cases themselves and try to understand why we are testing the behaviour that these tests target.

Browse the final Tour of Heroes tutorial code on StackBlitz.

Download the final Tour of Heroes tutorial code (zip archive, 30 KB)

Browse the Tour of Heroes—Model-View-Presenter style repository on GitHub.

Watch my talk "Model-View-Presenter with Angular" from Angular Online Meetup #8:

View the slides from my talk "Model-View-Presenter with Angular":

Learn about the history of the Model-View-Presenter pattern and how its sibling pattern, Model-View-Controller, was introduced to client-side UI frameworks for the web. Read "The history of Model-View-Presenter".

Are you tired of worrying about state management and back-end concerns in your Angular components? Extract all of that non-presentational logic into container components. Read how in "Container components with Angular".

Learn how to test container component logic with blazingly fast unit tests in "Testing Angular container components".

"Presentational components with Angular" discusses pure, deterministic, and potentially reusable components which rely solely on input properties and user interaction-triggered events to determine their internal state.

Learn how to extract a presenter from a presentational component in "Presenters with Angular".

In "Lean Angular components", we discuss the importance of a robust component architecture. Model-View-Presenter encapsulates several of the patterns that help us achieve this.

The animated flow charts were created by my good friend and fellow software developer Martin Kayser.

Achieving a high degree of separation of concerns is inspired by the works of Robert "Uncle Bob" Martin, particularly his book "Clean Architecture: A Craftsman's Guide to Software Structure and Design".

Applying the Model-View-Presenter pattern to an Angular app was inspired by the article "Model View Presenter, Angular, and Testing" by Dave M. Bush.

In my initial research, I examined the Model-View-Presenter pattern for vanilla JavaScript as described in the article "An MVP guide to JavaScript — Model-View-Presenter" by Roy Peled.

Acknowledgments for the Editor

I would like to extend my sincere gratitude to Max Koretskyi for his guidance in refining this piece to its final form. The effort you invest in sharing your insights with the software engineering community is truly commendable, and I deeply value your contributions.

Recognition of Peer Reviewers

To all the reviewers who dedicated their time and expertise, your thoughtful feedback was instrumental in completing this work. I am deeply grateful for each of your perspectives.