Software Patterns & Design Strategies
This document will describe some of the software design patterns that are used throughout the MaXX Desktop Architecture, how they provide reliable solutions to common requiring problems and most importantly, shed some light on some selected strategies for building different types of desktop applications.
As per the Architecture document, we now know that MaXX Desktop was designed using a multi-layered and message-driven architecture which follows the SOLID principles and Clean Architecture. Each layer of the architecture hosts applications or services with specific characteristics, behaviors and responsibilities.
We see here a great opportunity for defining reusable and common design strategies for building high performance visual applications, dependable desktop support and back-end services. By promoting proper use of design patterns we can bring up the code quality, maintain a strong, robust and yet flexible architecture, increase predictability and lowers risks associated to changes. All this translates into better user experience and a software system that can evolve over time without suffering from a middle-age crisis every two-three years...
A few words on Clean Architecture first. Our intent is to follow what makes sense for the MaXX Desktop and to provide concrete scenario in order to avoid ambiguity.
So good news folks, this document will try to address do just that, and maybe pick your curiosity and learn a different way to write code.
This section will go over some of the design patterns used in MaXX Desktop and how we put them together to build reusable components.
Throughout the document, we will use the following arrows to illustrate the relationship type between objects. For example, A⟶B, reads A is using B as a dependency type relationship. The arrow marks the direction of the dependency.
The legend below will help the reader to better understand the intent and relationship between objects.
The Observer pattern is one of the twenty-three well-known "Gang of Four" design patterns describing how to solve recurring design challenges in order to design flexible and reusable object-oriented software. An Observer simply observe objects, named the Observable (or subject in generic term), by maintains a list of those and notifies them automatically of any state changes. This behavior is usually implemented from an Interface rather than inherited. It is mainly used for implementing local or distributed event handling systems, in "event driven" software.
Diagram illustrates the Observer/Observable in context with ModelView and View.
The Observable pattern facilitates event-driven behaviors where Observers stands ready to react from an Observable state change in a non-blocking and asynchronously way. The Observable emits a state change notification to all its subscribed Observers. Upon reception of such notification, the Observer can react accordingly to that state change. This behavior is usually inherited instead of been implemented from an Interface.
The View design pattern is one of the most popular one and easy to understand. But sadly the View pattern as been poorly used throughout the years, and on an epic scale. The View is simply a device specific delivery mechanism that displays the content of a ModelView, captures user's input, and send those inputs to a Controller. The View must implement the Observer pattern in order to receive data modification notifications from the ModelView.
The ViewModel pattern is defined as a simple Observable values container used by the View. The ModelView should not contain business logic and the data transformation responsibility is passed to the Presenter. The ModelView only contains simple values like Strings, flags and others which are already transformed values ready to be displayed by the View. The ModelView must extend the Observable pattern in order to emit data modification notifications to its View. Those notifications are triggered when the Presenter sets the ModelView data.
Diagram illustrates the use of the ModelView, View, Presenter and Controller Patterns.
The responsibility that characterized the Presenter pattern is to reformat a ResponseModel object received from an Interactor into a ModelView. Upon reception, the Presenter transforms the received ResponseModel into a viewable representation as a ViewModel.
The Controller pattern definition in our architecture, is an object that handles inputs from a View, converts them into RequestModel and send them to the Interactor via a Boundary. The inputs are usually events generated from the View. In term of responsibility and features, that it. Nothing else.
The ResponseModel pattern is an object that represents the output that is sent to the user of the system, usually in response to the input or due to other triggers such as a scheduled time or an UI event happening. The ResponseModel is a DTO (Data Transfer Object) containing values such as text and numbers.
The RequestModel pattern is an object that represents the input data from a View. The RequestModel can be describe as a generic representation of the input data required for a computation or function call, which usually contains simple data types like numbers and text. The RequestModel is also a DTO.
Diagram illustrates the interactions between Interactor, Boundary, Presenter and Controller Patterns.
Our Architecture is designed in layers and follows the Clean Architecture and SOLID Principles, so that peripherals, services, computational resources, and data provider components such as MaXX Settings can be swapped as requirements change. For this reason, the core components of the Desktop applications, i.e. Entities and Interactors, never talk directly to those components. Rather, Interfaces called Boundaries are made so that calls are made across them. One side of the Boundary makes calls and expects a form of response that is agreed upon. The other side of the Boundary receives the calls and returns those responses. Both sides usually do not know who is on the other side. They just act upon the requests and responses. Such design also allows components to be tested individually by using mock / fake components across the boundaries.
Let’s talk about boundary types.
This is the Boundary between the input system and the Interactor. The input system does not deal directly with the Interactor. Instead an Interface is offered along with a set of method calls that receives a request. These calls promise that the input will reach the Interactor properly and that the use case will be executed.
This is the Boundary between the output system and the Interactor. This makes sure that the Interactor does not know how the response will be shown to the user. The Interactor passes along the data inside the response. But formatting the response is upon the output system on the other side of the Boundary.
Interactor is a design pattern that could be describe as a specialization of the Command pattern with the specific responsibility of fulfilling a specific use-case. The Interactor receives a RequestModel from its Input Boundary Interface, then sets things in motion like an orchestra director, coordinate the execution of a use case. There must be one Interactor per use case in a properly designed system/application. It is not uncommon to see the use of the Service pattern in conjunction with the Interactor. This allows an even better separation of responsibilities.
The Entity pattern focus is on the domain data, its validation rules and business logic that creates an output in response to an input. After receiving input from the user, the Interactor uses different Entities in the system to achieve the output that is to be sent to the user. The Entity, like the Interactor is using a Boundary interface to access its data source through a Gateway. Remember that the Interactor itself should NEVER directly contain the logic that transforms input into output.
The Interactor or Entity will often need to interact with an external system, or a Desktop Support or Back-end service, for that purpose we encourage using the Gateway pattern. A Gateway has to cross boundaries as well, but toward another system or service, and it is fair to call then Boundary. However it would add ambiguity to the intent, therefor, Gateway it is. Gateway is a specialization of Boundary which acts as a Reverse-Proxy and instead of hard-wiring the code specific to an external service access inside an Entity, a Boundary Interface is made. The Entity calls the methods over a Gateway's boundary interface and the components on the other side will use the specific service. This approach makes the code very modular and plug-and-play.
Diagram illustrates the interactions between Entity and an external service via a Gateway.
Here are some more complete examples on how we put together well defined patterns and build reusable design strategies for the MaXX Desktop that are robust, easy to test, and flexible. Below, we demonstrates one strategy per actual architectural layer and some re-usability scenarios.
Here are the assumptions from which the strategies are put together.
Inter Application Communication
One very apparent advantage of proper architecture and design pattern selection is that we can see right away some similarities in the strategies. A first use case is the "inter-application communication" where the same recipe is use over and over. This is good! Now we can focus on building that strategy right with re-usability and flexibility in mind for both how UX and Desktop Support applications are communicating to the outside world. The only variant in both use cases is the "consumer" in front of the Gateway.
We could even push one step further and adapt that strategy for Back-end Services data access mechanism by replacing the MaXX Links component with either a database like access for filesystem, MaXX Monitor metrics and MaXX Settings CLI. Write once, use everywhere :)
A second use case is with the "Boundaries" where the same recipe is used repetitively, with only one small variant for UX. This reusable strategy will serve as a massive improvement over the old MVC pattern (which we do not use) and as INGRES for both Desktop Support and Back-end Services. Suddenly the entire Desktop code base is considerably reduces in complexity and size thanks to reusable strategies and a clean architecture.
User Experience (UX)
From what we now know, most of the UX applications (except the window manager and a few very specialized use cases) are much simpler to build. For example, the User Preferences Panels will be reusing over 50% of their EGRESS code, which leaves us with the View, ModelView, Presenter, Interactor an Entry.
Here's how we see one good reusable strategy for visual application on the UX layer.
The Desktop Support services are even simpler to build. Al of our attention should be directed to the Interactors which are use cases execution orchestration. The INGRESS components are quite similar with small variant in the messages routing and EGRESS are identical.
Here's how a good reusable strategy for Desktop Support service looks like.
The Back-end Services will leverage the most out of the reusable strategies. The INGRESS components are almost identical, Interactors will have similar logic with small variances, Entities and Data Source adapters will require most of the code time. This is still a massive step up for such an architecture.
Back-end Service Strategy should looks like this.