anmonteiro Code ramblings

An Exploration of Object Recursion Design Patterns with Om Next Recursive Queries - Part 2

This is part two of a series of posts that aim to demonstrate how to build classical Software Engineering object recursion patterns in Om Next. In the previous post, we explored the Om Next constructs of the Composite. This time around we will use the Decorator design pattern to build a simple component hierarchy that provides runtime extensibility.

The Decorator design pattern

Decorator design pattern

As with the Composite, the Decorator also appears in the Gang of Four book. It can be viewed as a restricted form of the Composite (1-Recursion vs Composite's n-Recursion), although it serves a different purpose. The Decorator's aim is to add — or remove — functionality (decorate) to an object at runtime. The same can be achieved by subclassing, although that approach will only provide compile-time extensibility. As can be perceived by the diagram above, several decorators can form a chain, with each new decorator providing a new piece of functionality. The core object lies at the end of the decorator chain.

The example

If you have read my last post, you saw that we built an example that rendered squares inside other squares. The example we will build today will also have a geometric square — our core object — and we are going to add functionality that decorates it with pieces of text and/or images. We adopt the nomenclature present in the pattern diagram, except for our concrete decorators, which we will call TextDecorator and ImageDecorator. Other than that, we need to define components for the ConcreteComponent and Component. The former implements the square rendering functionality, while the latter is responsible for combining the queries of both decorators and the core — ConcreteComponent.

The data

The core object is just a representation of the square: its width, height and the color. Here's how it looks like:

Now, the data in our decorators is quite similar to the Composite case, in the sense that it is recursive. However, since the Decorator pattern employs 1-ObjectRecursion, each data item in this example doesn't have a vector of children, but solely a "pointer" to the next decorator. As you can see below, we call this pointer :next. Notice how the data that represents the square is the end of the recursion.

The Om Next components (or: "Show me some code!")

We start by defining our ConcreteComponent. Its query is quite simple: the square's attributes.

(defui ConcreteComponent
  static om/IQuery
  (query [this]
    '[:id :width :height :color]))

The concrete decorators are presented below. They each query for their own set of attributes; those represent the functionality they add to the core object. The only attributes our concrete decorators have in common are the id by which they are identified, and the recursion query ({:next ...}) which simply tells Om Next that we are expecting to find an object of the same type under that entry 1.

(defui ImageDecorator
  static om/IQuery
  (query [this]
    '[:id :decorator/image :image/max-width {:next ...}]))

(defui TextDecorator
  static om/IQuery
  (query [this]
    '[:id :decorator/text {:next ...}]))

How should Component be defined, then? Just like in the Decorator pattern diagram, it sits on top of our hierarchy, and delegates functionality to its child components. As such, it needs to aggregate the queries of all the children and define their identity. Our Idents are defined using the following approach. We know that TextDecorators have a :decorator/text attribute and ImageDecorators have a :decorator/image attribute. If we find none in props, we are in the presence of the core object itself. Component is shown below.

(defui Component
  static om/Ident
  (ident [this {:keys [id decorator/text decorator/image]}]
      (not (nil? text)) [:text id]
      (not (nil? image)) [:image id]
      :else [:component id]))
  static om/IQuery
  (query [this]
    {:text (om/get-query TextDecorator)
     :image (om/get-query ImageDecorator)
     :component (om/get-query ConcreteComponent)}))

The last piece of our example is a component that contains the top-level query which finalizes the definition of the union query. This is also our root component. We will call it DecoratorApp.

(defui DecoratorApp
  static om/IQuery
  (query [this]
    [{:decorator/app (om/get-query Component)}]))

The end result

After we implement our Om parser and render methods, we see the final result. The card below shows the result of passing only the square data to the example we just built. Pretty boring, heh? Scroll down!

However, if we pass the state that contains our decorators, we see that the previously boring square has been decorated with that data! One more thing: if you simply supply data that has more (or less, for that case) decorators to our example, those will be reflected in our core object. We have thus achieved runtime extensibility.

The complete source code for this post is published in this gist.

Thanks for reading!

1 here, "an object of the same type" refers to any one that the union query satisfies (which might not share the same query). Simple recursion can also be specified in Om Next, which allows to strictly recurse into an object which has the exact same query. For tips on how to use that syntax, visit my post on that topic