Practice
Computing Applications Practice

Dynamics of Change: Why Reactivity Matters

Tame the dynamics of change by centralizing each concern in its own module.
Posted
  1. Introduction
  2. How Change Propagates from One Module to Another
  3. Passive Programming
  4. Reactive Programming
  5. Passive vs. Reactive for Managing Essential Complexity
  6. Managing Dependencies and Ownership
  7. Example: Analytics Events
  8. Mind the Arrows
  9. References
  10. Author
  11. Figures
Dynamics of Change, illustration

back to top 

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.

Back to Top

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.

Back to Top

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.

Back to Top

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.

Back to Top

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.

Back to Top

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.

Back to Top

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).

Back to Top

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.

Back to Top

Back to Top

Back to Top

Figures

F1 Figure 1. Data Row for a codebase of e-commerce software.

F2 Figure 2. The Cart changes the Invoice.

F3 Figure 3. Passive programming with code in tail.

F4 Figure 4. Reactive programming with code in head.

F5 Figure 5. Public vs. private.

F6 Figure 6. Frequent passive pattern.

F7 Figure 7. Frequent reactive pattern.

F8 Figure 8. Dual properties.

F9 Figure 9. Reactive-only approach.

F10 Figure 10. A hybrid passively reactive solution.

F11 Figure 11. Passive-only approach.

F12 Figure 12. Public injection method.

Back to top

    1. Berry, G. Real-time programming: Special-purpose or general purpose languages. RR-1065. INRIA, 1989; https://hal.inria.fr/inria-00075494/document.

    2. Kiczales, G., Lamping, J., Mendhekar, A., Maeda, C., Lopes, C., Loingtier, J. M., Irwin, J. Aspect-oriented programming. In Proceedings of the 11th European Conference on Object-Oriented Programming (1997), 220–242.

Join the Discussion (0)

Become a Member or Sign In to Post a Comment

The Latest from CACM

Shape the Future of Computing

ACM encourages its members to take a direct hand in shaping the future of the association. There are more ways than ever to get involved.

Get Involved

Communications of the ACM (CACM) is now a fully Open Access publication.

By opening CACM to the world, we hope to increase engagement among the broader computer science community and encourage non-members to discover the rich resources ACM has to offer.

Learn More