Skip to content

Advanced Topics

Advanced Topics: Design Patterns

Object oriented code often utilizes design patterns, code templates that show how to achieve a desired effect. The term is inspired by pattern books, volumes that contain plans or diagrams to aid in making something. Such books are still used nowadays to sew or weave material, and in the 19th century were a common way of building a house without the expense of hiring an architect. A very famous computer science book, "Design Patterns: Elements of Reusable Object-Oriented Software"[^4] popularized the idea and provided a number of design patterns in widespread use. In this chapter, we show how to implement a selection of these patterns that we have found particularly useful in writing Morpho code. Certain Morpho libraries use these patterns, and knowledge of the terminology can help the reader understand their design. In some cases, the Morpho implementation differs from the original pattern because of the language's features. Also noteworthy is the idea of an anti-pattern[^5], a design approach that may be obvious, but contains undesirable features and is to be avoided. We do not dwell on anti-patterns here they are to be avoided after all!but a good programmer should read one of the many excellent texts on the subject.

Builder

The Builder pattern facilitates creation of objects that are complicated or expensive to create geometric data structures such as Meshes are a good example or must be constructed in multiple stages. They accomplish this by separating initialization code into a separate class. Another use case is where the type of object created may depend on parameters the user supplies: a MatrixBuilder, for example, might create a regular Matrix or a ComplexMatrix depending on whether the contents are real or complex numbers. A Builder is initialized with parameters describing the object to be created, and provides one method:

  • build() Constructs and returns the requested object.
    class Builder {
      init(parameter) {
        self.parameter = parameter
      }

      build() {
        var obj = Object() // Make object
        // Initialize it
        return obj
      }
    }

If the object requires multiple steps to create it, build can be replaced by appropriate build stages.

Visitor

The Visitor pattern accomplishes the task of processing a collection of heterogeneous objects. A Visitor object is constructed with the collection to process, and provides two methods:

  • visit() Processes a single element of the collection. Multiple implementations of this method are provided that process different kinds of object.

  • traverse() Loops over the collection, calling the visit() method for each object in turn. The correct implementation is selected automatically by multiple dispatch.

    class Visitor {
      init(collection) {
        self.collection = collection
      }

      visit(Type1 a) {
        // Do something
      }

      visit(Type2 b) {
        // Do something
      }

      visit(x) {
        // Default
      }

      traverse() {
        for (p in self.collection.contents()) self.visit(p)
      }
    }

Example: SVG export for vector graphics

To illustrate use of the Visitor class, let's create an example of a collection that might benefit from it. A 2D vector graphics image comprises various graphics primitives, e.g. lines, circles, polygons, etc. Here's a simple implementation that incorporates a couple of basic primitives:

class GraphicsPrimitive {}

class Circle is GraphicsPrimitive {
  init(x,y,r) { self.x = x; self.y = y; self.r = r }
}

class Line is GraphicsPrimitive {
  init(x1,y1,x2,y2) { self.start = [x1,y1]; self.end = [x2,y2] }
}

class Graphics2D {
  init() { self.contents = [] }

  append(GraphicsPrimitive x) { // Adds a primitive to the collection
    self.contents.append(x)
  }

  contents() { return self.contents }
}

The user can then build up an image by creating a blank Graphics2D object and adding elements one by one. Here, we create a disk enclosed by a square:

var g = Graphics2D()
g.append(Circle(50,50,50))
g.append(Line(0,0,0,100))     // Left
g.append(Line(100,0,100,100)) // Right
g.append(Line(0,0,100,0))     // Top
g.append(Line(0,100,100,100)) // Bottom

Now imagine that we would like to export our Graphics2D object to a file, e.g. SVG, PDF, postscript, etc. The exporter class must traverse the contents of the Graphics2D object one by one and build up the output. Here's an example of a working SVG Exporter that accomplishes this using the Visitor pattern:

class SVGExporter {
  init(Graphics2D graphic) {
    self.graphic = graphic
  }

  visit(Circle c, f) {
    f.write("<circle cx=\"${c.x}\" cy=\"${c.y}\" r=\"${c.r}\"/>")
  }

  visit(Line l, f) {
    f.write("<line x1=\"${l.start[0]}\" y1=\"${l.start[1]}\" x2=\"${l.end[0]}\" y2=\"${l.end[1]}\" stroke=\"black\"/>")
  }

  visit(x, f) { } // Do nothing for unrecognized primitives

  export(file) {
    var f = File(file, "w")
    f.write("<svg xmlns=\"http://www.w3.org/2000/svg\">")
    for (p in self.graphic.contents()) self.visit(p, f)
    f.write("</svg>")
    f.close()
  }
}

To use the exporter, the user first creates an SVGExporter, passing the Graphics2D object to the constructor, and then calls the export method with a desired filename:

var svg = SVGExporter(g)
svg.export("pic.svg")

The export method creates the requested file, generates header information, and then loops over the content of the Graphics2D object, calling the visit method for each object.

Advantages

The Visitor pattern enforces modularity by separating representation from processing of data. Since Graphics2D is intended to represent a vector image abstractly it shouldn't also provide the unrelated functionality of export facilities to particular filetypes. This separation makes it much easier to extend the program. A new graphics primitive could be defined without changing Graphics2D; only the SVGExporter would need to be modified, by adding an additional implementation of visit. Similarly, support for additional graphics formats could be achieved without modifying the existing code above at all. All that needs to be done is to create a new class analogous to SVGExporter for the desired filetype.

Comparison with other languages

Readers familiar with OOP design patterns may notice that the Visitor pattern described here is slightly different from the traditional implementation. In languages that lack multiple dispatch, the Visitor's export method first calls a method called accept on each object in the collection, rather than calling visit on the Visitor itself:

export(file) {
  // Create file and write header
  for (p in self.graphic.contents()) p.accept(self, f)
  // Write footer and close file
}

Each Graphics primitive must provide an accept method that in turn calls an appropriate method on the Visitor. For the Circle primitive, accept might call a method called visitCircle:

accept(visitor, f) {
  visitor.visitCircle(self, f)
}

The Visitor must therefore provide a different method for each object type. Here's the implementation of visitCircle:

visitCircle(c, f) {
  file.write("<circle cx=\"${c.x}\" cy=\"${c.y}\" r=\"${c.r}\"/>")
}

The procedure described, where the Visitor calls accept on the object being processed that then dispatches the call to the correct method on the Visitor, is called double dispatch. It's more convoluted and less efficient, requiring two method calls per processed object, than the multiple dispatch implementation. It's also less compact and less modular: with double dispatch, each primitive must provide an accept method which is unnecessary in the multiple dispatch version. Indeed, all the functionality of the Visitor with multiple dispatch is contained within the Visitor, which could be highly advantageous if a collection is from a library designed without the Visitor pattern in mind and where the code can't easily be modified.

Another advantage of the multiple dispatch is that a fallback visit method for unrecognized object types is trivially implemented. In other languages, adding a new primitive requires immediately modifying all Visitor objects to avoid potentially raising an ObjLcksPrp error; in the multiple dispatch implementation, unknown primitives are simply routed to the fallback visit method and ignored.