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.