Professional programming is about dealing with software at scale. Everything is trivial when the problem is small and contained: it can be elegantly solved with imperative programming or functional programming or any other paradigm. Real-world challenges arise when programmers have to deal with large amounts of data, network requests, or intertwined entities, as in user interface (UI) programming.
Of these different types of challenges, managing the dynamics of change in a code base is a common one that may be encountered in either UI programming or the back end. How to structure the flow of control and concurrency among multiple parties that need to update one another with new information is referred to as managing change. In both UI programs and servers, concurrency is typically present and is responsible for most of the challenges and complexity.
Some complexity is accidental and can be removed. Managing concurrent complexity becomes difficult when the amount of essential complexity is large. In those cases, the interrelation between the entities is complex—and cannot be made less so. For example, the requirements themselves may already represent essential complexity. In an online text editor, the requirements alone may determine that a keyboard input needs to change the view, update text formatting, perhaps also change the table of contents, word count, paragraph count, request the document to be saved, and take other actions.
Because essential complexity cannot be eliminated, the alternative is to make it as understandable as possible, which leads to making it maintainable. When it comes to complexity of change around some entity Foo, you want to understand what Foo changes, what can change Foo, and which part is responsible for the change.
How Change Propagates from One Module to Another
Figure 1 is a data flow chart for a code base of e-commerce software, where rectangles represent modules and arrows represent communication. These modules are interconnected as requirements, not as architectural decisions. Each module may be an object, an object-oriented class, an actor, or perhaps a thread, depending on the programming language and framework used.
An arrow from the Cart
module to the Invoice
module (Figure 2a) means the cart changes or affects the state in the invoice in a meaningful way. A practical example of this situation is a feature that recalculates the total invoicing amount whenever a new product is added to the cart (Figure 2b).
The arrow starts in the Cart
and ends in the Invoice
because an operation internal to the Cart
may cause the state of the Invoice
to change. The arrow represents the dynamics of change between the Cart
and the Invoice.
Assuming all code lives in some module, the arrow cannot live in the space between; it must live in a module, too. Is the arrow defined in the Cart
or in the Invoice
? It is up to the programmer to decide.
Passive Programming
It is common to place the arrow definition in the arrow tail: the cart. Code in the Cart
that handles the addition of a new product is typically responsible for triggering the Invoice
to update its invoicing data, as demonstrated in the chart and the Kotlin (https://kotlin-lang.org/) code snippet in Figure 3.
The Cart
assumes a proactive role, and the Invoice
takes a passive role. While the Cart
is responsible for the change and keeping the Invoice
state up to date, the Invoice
has no code indicating the update is coming from the Cart
. Instead, it must expose updateInvoicing
as a public method. On the other hand, the cart has no access restrictions; it is free to choose whether the ProductAdded
event should be private or public.
Let’s call this programming style passive programming, characterized by remote imperative changes and delegated responsibility over state management.
Reactive Programming
The other way of defining the arrow’s ownership is reactive programming, where the arrow is defined at the arrow head: the Invoice
, as shown in Figure 4. In this setting, the Invoice
listens to a ProductAdded
event happening in the cart and determines that it should change its own internal invoicing state.
The Cart
now assumes a broadcasting role, and the Invoice
takes a reactive role. The Cart's
responsibility is to carry out its management of purchased products, while providing notification that a product has been added or removed.
Therefore, the Cart
has no code that explicitly indicates its events may affect the state in the Invoice
. On the other hand, the Invoice
is responsible for keeping its own invoicing state up to date and has the Cart
as a dependency.
The responsibilities are now inverted, and the Invoice
may choose to have its updateInvoicing
method private or public, but the Cart
must make the ProductAdded
event public. Figure 5 illustrates this duality.
The term reactive was vaguely defined in 1989 by Gérard Berry.1 The definition given here is broad enough to cover existing notions of reactive systems such as spreadsheets, the actor model, Reactive Extensions (Rx), event streams, and others.
Passive vs. Reactive for Managing Essential Complexity
In the network of modules and arrows for communication of change, where should the arrows be defined? When should reactive programming be used and when is the passive pattern more suitable?
There are usually two questions to ask when trying to understand a complex network of modules:
- Which modules does module X change?
- Which modules can change module X?
The answers depend on which approach is used: reactive or passive, or both. Let’s assume, for simplicity, that whichever approach is chosen, it is applied uniformly across the architecture. For example, consider the network of e-commerce modules shown in Figure 6, where the passive pattern is used everywhere. To answer the first question for the Invoice
module (Which modules does the invoice change?), you need only to look at the code in the Invoice
module, because it owns the arrows and defines how other modules are remotely changed from within the Invoice
as a proactive component.
To discover which modules can change the state of the Invoice
, however, you need to look for all the usages of public methods of the Invoice
throughout the code base.
In practice, this becomes difficult to maintain when multiple other modules may change the Invoice
, which is the case in essentially complex software. It may lead to situations where the programmer has to build a mental model of how multiple modules concurrently modify a piece of state in the module in question. The opposite alternative is to apply the reactive pattern everywhere, illustrated in Figure 7.
To discover which modules can change the state of the Invoice
, you can just look at the code in the Invoice
module, because it contains all “arrows” that define dependencies and dynamics of change. Building the mental model of concurrent changes is easier when all relevant entities are co-located.
On the other hand, the dual concern of discovering which other modules the Invoice
affects can be answered only by searching for all usages of the Invoice
module’s public broadcast events.
When arranged in a table, as in Figure 8, these described properties for passive and reactive are dual to each other.
The pattern you choose depends on which of these two questions is more commonly on a programmer’s mind when dealing with a specific code base. Then you can pick the pattern whose answer to the most common question is, “look inside,” because you want to be able to find the answer quickly. A centralized answer is better than a distributed one.
While both questions are important in an average code base, a more common need may be to understand how a particular module works. This is why reactivity matters: you usually need to know how a module works before looking at what the module affects.
Because a passive-only approach generates irresponsible modules (they delegate their state management to other modules), a reactive-only approach is a more sensible default choice. That said, the passive pattern is suitable for data structures and for creating a hierarchy of ownership. Any common data structure (such as a hash map) in object-oriented programming is a passive module, because it exposes methods that allow changing its internal state. Because it delegates the responsibility of answering the question “When does it change?” to whichever module contains the data-structure object, it creates a hierarchy: the containing module as the parent and the data structure as the child.
Managing Dependencies and Ownership
With the reactive-only approach, every module must statically define its dependencies to other modules. In the Cart
and Invoice
example, Invoice
would need to statically import Cart
. Because this applies everywhere, all modules would have to be singletons. In fact, Kotlin’s object keyword is used (in Scala as well) to create singletons.
In the reactive example in Figure 9, there are two concerns regarding dependencies:
- What the dependency is: defined by the import statement.
- How to depend: defined by the event listener.
The problem with singletons as dependencies relates only to the what concern in the reactive pattern. You would still like to keep the reactive style of how dependencies are put together, because it appropriately answers the question, “How does the module work?”
While reactive, the module being changed is statically aware of its dependencies through imports; while passive, the module being changed is unaware of its dependencies.
So far, this article has analyzed the passive-only and reactive-only approaches, but in between lies the opportunity for mixing both paradigms: keeping only the how benefit from reactive, while using passive programming to implement the what concern.
The Invoice
module can be made passive with regard to its dependencies: it exposes a public method to allow another module to set or inject a dependency. Simultaneously, Invoice
can be made reactive with regard to how it works. This is shown in the example code in Figure 10, which yields a hybrid passively reactive solution:
- How does it work? Look inside (reactive).
- What does it depend on? Injected via a public method (passive).
This would help make modules more reusable, because they are not singletons anymore. Let’s look at another example where a typical passive setting is converted to a passively reactive one.
Example: Analytics Events
It is common to write the code for a UI program in passive-only style, where each different screen or page of the program uses the public methods of an Analytics
module to send events to an Analytics
back end, as illustrated in the example code in Figure 11.
The problem with building a passive-only solution for analytics events is that every single page must have code related to analytics. Also, to understand the behavior of analytics, you must study it scattered throughout the code. It is desirable to separate the analytics aspect from the core features and business logic concerning a page such as the LoginPage
. Aspect-oriented programming2 is one attempt at solving this, but it is also possible to separate aspects through reactive programming with events.
In order to make the code base reactive only, the Analytics
module would need to statically depend on all the pages in the program. Instead, you can use the passively reactive solution to make the Analytics
module receive its dependencies through a public injection method. This way, a parent module that controls routing of pages can also bootstrap the analytics with information on those pages (see Figure 12 for an example).
Mind the Arrows
Introducing reactive patterns in an architecture can help better define which module owns a relationship of change between two modules. Software architectures for essential complex requirements are often about structuring the code in modules, but do not forget that the arrows between modules also live in modules. Some degree of reactivity matters because it creates separation of concerns. A particular module should be responsible for its own state. This is easily achievable in an event-driven architecture, where modules do not invasively change each other. Tame the dynamics of change by centralizing each concern in its own module.
Figures
Figure 1. Data Row for a codebase of e-commerce software.
Figure 2. The Cart
changes the Invoice
.
Figure 3. Passive programming with code in tail.
Figure 4. Reactive programming with code in head.
Figure 6. Frequent passive pattern.
Figure 7. Frequent reactive pattern.
Figure 9. Reactive-only approach.
Figure 10. A hybrid passively reactive solution.
Join the Discussion (0)
Become a Member or Sign In to Post a Comment