Object-oriented Programming in Python. Lesson 3 — the “Visitor” Pattern
This is the third in a series of five lessons, summarizing the practical need for object-oriented programming, and presenting the common facilities provided by object-oriented languages. The examples are in Python, stressing the Python implementation and approach. In the previous chapters, we acquired the basic vocabulary of object-oriented programming: functional substitutability, using the message paradigm, to objects whose behavior and data are encapsulated in classes. In the present lesson, based on the observation that applying an object-oriented design in practice must be disciplined, following established idioms, patterns, and architectures, we proceed with a detailed example of the object-oriented approach to recursive data structures — the “Decorator” and “Composite” patterns. Finally, we will use this wisdom to conclude the example of the previous lesson.
Sections in this Lesson:
- The Common Case of Object-oriented Substitutability — “Left-handed” Polymorphism
- The Challenge of “Double” Polymorphism
- The Easy Way Out: “Multi-methods”
- The Object-oriented Way Out — “Double Dispatch”
- The “Visitor” Pattern
- The “Hierarchical Visitor” Variant
- Exercise. “Outline” file with multiple renderers
1. The Common Case of Object-oriented Substitutability — “Left-handed” Polymorphism
As introduced in the first lesson on this course, the object-oriented solution to requiring substitutable capability (we know what must be done, but there are many ways to do it) is left-handed polymorphism, counting on an (explicit or implicit) hierarchy of classes conforming to this required interface. Given an anonymous — but interface-conforming — object as the receiver of the message (positioned at the left side of the dot operator), we pass to it the message selector and arguments (on the right of the dot operator). Dynamic binding takes care of the rest.
To put it simply, the object-oriented interpretation of “we know what must be done, but there are many ways to do it…” proceeds with the case of “…and it (how to do it) depends upon the type of the object (the receiver of the action) — so let the object, whatever it may be, do it for us!”. Precisely: the object-oriented paradigm replaces the challenge of activating a substitutable capability with the much simpler challenge of addressing a substitutable object. Left-handed polymorphism is simple, self-explaining and very efficient to implement programmatically. (No embarrassing questions asked, such as explicit switch/case constructs). But with an obvious restriction (sadly neglected by some): a conformant problem domain! For left-handed polymorphism to be applicable, there must be an object on the left side, and exactly one such!
First, let us consider a conformant case — “Shape/point collision test”. A graphic editor presents to the screen a canvas over which several shapes are rendered. The user clicks the mouse. The program wakes up to the event, receiving the mouse click coordinate (x: y point). The list of presented shapes is scanned for the (first) shape that collides with the point. The object-oriented way to perform this is to pass the Boolean message “is colliding” to each shape and select the first shape that responds positively. Of course, all concrete Shapes (Circle and Rectangle) implement this method.
The object-oriented solution to the problem — “Shape, do you collide with this Point?” — is the correct design decision. A Shape knows about Points (e.g., it has a center-point), and there is only one Point type in this geometry. For comparison, the opposite object-oriented solution — “Point, do you happen inside this Shape?” is bad; Points do not know about complex shapes (and even if they would, the specific shape involved is not specified). And the global solution — “Is there a collision between this Shape (whatever it is) and this Point?” — is equally bad, and for the same reason.
Example code:
Footnotes:
- “Is colliding” is an abstract method in the abstract Shape.
- The Circle method of “is colliding”.
- The Rectangle method of “is colliding”.
Output:
Testing collision with 110:110
1. Circle at 100:100. r=50: True
2. Circle at 200:100. r=10: False
3. Rectangle at 100:200. 100x100: False
4. Rectangle at 100:105. 20x30: True
2. The Challenge of “Double” Polymorphism
“Left-handed” — or “singular” — polymorphism seems to satisfy — naturally or with some adjustment — most design challenges of functional substitutability (which accounts for its popularity). However — surprise, surprise! — it is no panacea (as some object-oriented zealots would have it). Like any tool, it has its field of applicability and its limits, which the professional must bear in mind. One well-known such limitation is multiple polymorphism, which is admittedly much rarer than the singular variety, but is there nevertheless, to be reckoned with. (Or is it rare because we are conditioned to avoid it?)
For example, we were able to solve the challenge of “Shape collides with Point” trivially, by passing the Boolean “is colliding” message to an anonymous shape with the point as argument, because (1), the Shape type was substitutable, but (2), the point type was constant. (i.e., the message receiver is substitutable, but the message is strongly typed — Point expected. Nothing else would be acceptable!)
Now, consider the use case of “Shape collides with Shape”. (For example, we may wish to paint the shared area in the mixture of both colors). There is little sense in forcing a left-handed solution on this problem (which, as we shall see, will not work anyway), because there is no reason to prefer “Circle, do you collide with this Rectangle” over “Rectangle, do you collide with this Circle” — Precisely, accounting for substitutability: “Left Shape, whatever you are, do you collide with the right shape, whatever it is?” and vice versa — other than narrow-minded object-oriented orthodoxy!
Here, two substitutable objects are involved, and both have equal claim to the title of “The Receiver of the Message”! So, none of them shall win! Here is a procedural challenge of “do these Circle and Rectangle collide?” (Precisely, accounting for substitutability: “do these two Shapes, whatever they are, collide?”). Since this challenge does not lend easily to the message — to a single object — paradigm, one is free to conclude that this use case does not suggest strict object-oriented design, if at all. (Apparently, such things do exist!)
A functional solution to this problem would suggest a (global) “is colliding” function with two — substitutable — parameters. Such a function is also called multi-method: a “method” of (as if) two (or more) classes. The design challenge here is that each combination of two Shapes — (1) Circle/Circle, (2) Circle/Rectangle, and (3) Rectangle/Rectangle) — needs a distinct method. (Unless you intend to count pixels, using Shape/Point collision, which is implementation-dependent anyway. Otherwise, bear in mind that the geometrical formulae required are different).
For a start, we need our programming language to support “function overloading”— the capability to define multiple functions (in the same namespace) by the same name, but with a different list of parameter types. (We need all these various functions to be called by the same name, because — during call-time — we do not know which implementation is needed!) And duck-typed Python will not recognize the following as two distinct functions: (1) “isColliding(Left: Circle, right: Circle)” and (2) “isColliding(Left: Circle, right: Rectangle)”. To increase the design challenge, the resolution of the required method must be deferred to runtime! (During compile-time, we are colliding anonymous Shape with anonymous Shape, which does not make us any wiser). This programmatic capability is nowadays called “multiple dispatch” and is supported by very few current programming languages (most notoriously Julia).
Python does not support — as stated — even the simple requirement for function overloading, and for a simple reason: the dictionary-based architecture. Since the function name — a string — is used as key to the object’s (or module’s) dictionary, there may only be one such function! And for the same reason, we may not provide each Shape class with methods for collision detection with each other Shape; function overloading is not supported in the object-namespace either (due to the same culprit— the attribute dictionary). Try doing this, and Python will mercilessly replace each method definition as it comes, ending up pointing to the last one, which is not what we want). And then, embedding the argument type name in the method name — such as “is colliding Circle”, “is colliding Rectangle”, etc. — will not work either; we will be shooting our own foot! Although these various methods — bearing distinct names — will be preserved, they will be unreachable! (Python will not be able to dispatch to the right type-related method, given another method name and some anonymous Shape).
And finally, providing exactly one “is colliding Shape” method will not work either, and for the same reason! Unless we resort to doing our own explicit type-dependent dispatch inside using metadata — which is cumbersome, to say the least, and is far from a general method — to copy/paste wherever the need arises. (However, note that compromising the open/closed principle — to be discussed later — is not an issue here; in this problem domain all Shape types are already known in advance and their repertory is not going to be augmented).
3. The Easy Way Out: “Multi-methods”
The challenge of double polymorphism is traditionally solved in either (1) the “functional” way — multiple dispatch (using “multi-methods”) — or (2) the object-oriented way (yes, there is one!) — “double dispatch”.
First, let us review the multi-method solution, because it is simple (at least for this simple problem). And here is the requirement: We need a Python implementation of multiple dispatch (function overloading that is resolved during runtime). Which means, in our case: For each combination of two Shapes, we must be able to define a separate global and strong typed(!) function (taking two objects of these explicit types). And all these functions should respond to the same name!
Although Python does not support multiple dispatch out of the box, it is not hard to extend the language, using a three-fold mechanism, as outlined by many authors, including Bjarne Stroustroup (for C++) and the BDFL himself (for Python): (1), a dictionary of multi-methods (one for each substitutable function name, keyed by an ordered list of parameter types), (2), a registration mechanism (to the appropriate dictionary) and (3), a runtime dispatch mechanism. Of course, we would welcome a smart solution that somehow manages to hide the existence of all this machinery (dictionary, registration, and runtime dispatch) under the hood of normal Python syntax.
Fortunately, Python is rich enough to stand up to the challenge! And even more fortunately, there is no need to write this mechanism on our own; there are several ready-made third-party contributions. For this lesson we are going to use the ”multipledispatch” package (available through the standard pip utility) which, although defined as “work in progress”, is stable and comprehensive enough to meet our simple challenge. Most important of all — this package is indeed user-transparent, hiding the entire non-trivial mechanism behind a Julia-style intuitive interface of — for each multimethod — function decorator using the appropriate type-list!
Here is an example:
Footnotes:
- Importing the “multiple dispatch” package.
- Multi-method for “is colliding” using Circle and Circle.
- Multi-method for “is colliding” using Circle and Rectangle.
- Multi-method for “is colliding” using Rectangle and Rectangle.
- Multi-method for “is colliding” using Rectangle and Circle. Since the combination is symmetrical, it is simply inverted, using the opposite method. There is no reason to copy the logic!
Output:
Circle at 100:100. r=50 / Circle at 150:200. r=60: False
Circle at 100:100. r=50 / Rectangle at 100:100. 10x10: True
Circle at 100:100. r=50 / Rectangle at 140:190. 20x30: False
Circle at 150:200. r=60 / Circle at 100:100. r=50: False
Circle at 150:200. r=60 / Rectangle at 100:100. 10x10: False
Circle at 150:200. r=60 / Rectangle at 140:190. 20x30: True
Rectangle at 100:100. 10x10 / Circle at 100:100. r=50: True
Rectangle at 100:100. 10x10 / Circle at 150:200. r=60: False
Rectangle at 100:100. 10x10 / Rectangle at 140:190. 20x30: False
Rectangle at 140:190. 20x30 / Circle at 100:100. r=50: False
Rectangle at 140:190. 20x30 / Circle at 150:200. r=60: True
Rectangle at 140:190. 20x30 / Rectangle at 100:100. 10x10: False
4. The Object-oriented Way Out — “Double Dispatch”
The “double-dispatch” idiom (a term that is much older than “multiple dispatch”, and not to be mixed with it) divides the multimethod in two. Take a deep breath: The left side takes the right side and then sends itself to the right side. This acrobatic feat involves twice solving a polymorphic left side (which happens to be the object-oriented specialty!) The constant performance (exactly two messages, compared with the unpredictable cost of dictionary lookup in the multi-method alternative) is achieved at the pre-condition of a “stable hierarchy” (all candidate Shapes must be known in advance), which, luckily for us, is indeed the present case.
The “Shape collides with shape” functional requirement is reduced, in the case of “Circle collides with Rectangle” to two messages: (1), “Circle, do you collide with this Shape” (which we know, happens to be a Rectangle) and (2), “Some Shape (which we know, happens to be the Rectangle from step 1), do you collide with me. I am a Circle!”
Example:
Footnotes:
- The common Shape method of “is colliding” (with another Shape) constructs the name of the complementary “is colliding” method (a string) appropriate to its own type (which is part of the method name on the other side), looks the method up in the other Shape’s attribute dictionary and invokes the found method (which, in Python, is already bound to its object), using itself.
- The method for colliding Circle with Circle.
- The method for colliding Circle with Rectangle.
- The method for colliding Rectangle with Circle. Since this double-polymorphism happens to be functionally symmetric — Circle/Rectangle and Rectangle/Circle give the same result — there is no need to copy the logic.
- The method for colliding Rectangle with Rectangle.
(Output remains the same)
The Python solution involves some metadata magic, which makes it is simpler than its traditional counterpart in strong-typed languages (such as C++, Java, or C#). Assuming that the right-side object, whatever it is, does have a method whose name consists of “is colliding”, followed by the name of the left-side object’s class (where we are), the left-side method constructs the name of the required method (a string), retrieves it from the other object’s attribute dictionary (if indeed implemented — otherwise error) and invokes it over the other object, using itself.
5. The “Visitor” Pattern
Suppose we are required to not only use our Shape library for geometric computations, but also to render designs made of Shapes. While there may be a default medium (say, “.png” files, rendered with the Python Imaging Library), we wish the design to allow open/closed extension to other rendering technologies and output media.
Obviously, our main design challenge is multiple substitutability! There are (1) multiple shape types to render, (2) multiple output media to render on, (3) multiple rendering engines to rely on, etc.
Multiple dispatch does not seem appropriate here (at least not as is), because it involves so many global functions that (apparently) do not know about each other and do not manage state. But this use-case calls for encapsulation: we need an object (a “Renderer”) that manages state (what it has rendered so far and the equipment it has come with: canvas, fonts, brushes, etc.). Furthermore, at any moment there may be multiple Renderers, busy rendering different Shape sets on different canvases, oblivious to each other. So, a global solution is out of the question. (Still, somehow-encapsulated multiple dispatch sounds like an interesting design challenge, and I would appreciate concrete suggestions!)
And then, here we encounter double-polymorphism of a different kind. For one thing, this functional substitutability is asymmetrical. For another, only one of the participants here is truly substitutable.
1. In the use-case “Shape may collide with Shape” the responsibility to share was symmetrical, distributed evenly between the two participants. Each has equal claim to be the receiver. We have no reason to prefer “Circle, do you collide with this Rectangle?” over “Rectangle, do you collide with this Circle?”. But in the use-case “Shape Renderer displays shape” the distribution of responsibility is asymmetrical: The significant functionality tips the scale in favor of the Renderer! “Renderer, please render this Shape” sounds natural. But “A Shape, please have yourself rendered by this Renderer”, although possible (and practiced) is a much weaker design, and the visibility loop it suggests is redundant. Although the Shape provides valuable information, without the Renderer, there would be no image (which is what we are contracted to deliver). But without the Shape, there still will be an image — an empty one. (One may argue over the usefulness of this output, still, one cannot deny its existence). And recall that functions are properly defined by their output (rather than the work hidden inside to produce it)!
2. The renderer’s identity and type are constant. We need the (same) Renderer (whoever it may be) at hand during the entire rendering operation. But the shapes it renders are replaced! So why don’t we tear the veil from the renderer’s face and reveal its true type to begin with? Because we need it to be configurable, and during runtime (precisely: during (1) program initialization, and then (2), subject to re-configuration by the user). So, when rendering begins, there is only one Renderer in the current context (that does not change), rendering so many Shapes (of any Shape type).
The “Visitor” pattern solves this asymmetric double-polymorphic challenge by double dispatch (it is an object-oriented solution). Take a deep breath: Given the current Shape, the Renderer asks it to accept it (the renderer), which makes the Shape (that knows its own type) ask the Renderer (whoever it may be) to render it, specifying its (the shape’s) type.
And then, it is customary in the object-oriented world to go one step beyond just the explicitly required capability (here: rendering) and implement a general “Visitor” superclass. This further abstraction gives us the potential to introduce any kind of manipulation on Shapes, with rendering being a specific case. It does not cost money to do this, and who knows?
Example:
Footnotes:
- Importing the Python Imaging Library
- The common Shape method to “accept” (this Visitor) constructs the name of the Visitor’s method appropriate to its (the Shape’s) type, looks the message selector up in the Visitor’s attribute dictionary and invokes the method using itself. (In Python, the method is already bound to the Visitor.)
- The Shape “get bounding box” method (needed by the Python Imaging Library, but common in graphics) is abstract and must be implemented (differently) in each concrete Shape.
- The color names are the ones expected by the Python Imaging Library but are generic.
- The Circle implementation for “get bounding box”.
- The Rectangle implementation for “get bounding box”.
- Interface for a general Shape “Visitor”.
- A concrete Shape Visitor for rendering “png” images.
- The Shape Visitor, required to draw a sequence of Shapes, forwards each Shape to “visit” it (the Visitor).
- The Circle rendering method, carrying the generic “visit Circle” name (in the Shape Visitor).
- The Rectangle rendering method, carrying the generic “visit Circle” name (in the Shape Visitor).
Output:
The “Visitor” pattern is a non-trivial solution to a non-trivial problem! Like all heavy-weight mechanisms, it does not come without payment. So, look out for the following constraints posed by its usage:
- All visit-able entities must be known in advance! Because the Visitor specifies a discrete “visit” capability for each particular entity, and there is no way getting around it. This is uncomfortable, to say the least, in OO languages that require all methods to be defined inside the class! (I did not inquire what that implies to languages that allow — or require — methods to be added to the class from without, like C#, Rust and Go — comments welcome!) This arrangement indeed suits graphic applications, where the repertory of shapes is typically well defined, based upon geometry and many years of actual use, and is not expected to change. On the contrary, the “Visitor” pattern a-priori would not fit into — for example — a messaging system, where the repertory of message types is naturally subject to extension without notice! (There, some callback registration mechanism is imminent.)
- The data declarations involved with the Visitor pattern create a cyclic namespace dependency in strongly typed languages, inviting dubious workarounds such as forward declaration. One may expect this problem to not come up in duck-typed Python. But if you write properly documented Python using type hints, then you must handle the cyclic namespace dependency (even in duck-typed Python), by e.g., quoting the forward-declared types!
- And most important of all: The Visitor pattern comes from the solution domain but may backfire a renewed understanding into the problem domain! Which may sound problematic, but is just natural, and must be considered! The Human way is to design (among other things) by free association, zigzagging back and forth between the problem and solution domains — what must be done and how to implement it. (On the contrary, strict comprehension from the problem to the solution, never looking back— e.g., the “waterfall model” — is a naive engineering dogma that never worked for anyone.) Often, we need to take full stock of the means at our disposal to make up our mind exactly what we may realistically require — to name one of the (many) reasons. For example, consider the introduction of a word that somehow changes the meaning of the sentence, just to make it rhyme, incidentally, realizing exactly what was it that we wanted to say, all that time). (Where rhyming is a poet’s implementation device). Consider selling a full-fledged smartphone to a client that requires a mobile phone just to call her aunt. You may be either doing her (the client, not the aunt) a favor, discovering how much she really needed a smartphone all those years, or a disgrace — having to rifle through multiple menus and options just to output — or answer — a call, and being interrupted by applications and setup functions she never invited and does not really need. In summary: Suggestions for re-thinking the problem domain coming from below (e.g. some instances of applying intrusive design patterns such as the “Visitor”) may turn out to be either a revelation or nuisance, depending on the occasion. Be wary!
Due to its intrusive nature, the “Visitor” pattern would be an acceptable (and potent) solution, where everyone agrees about the extended problem domain. “This family of business entities are made to be manipulated (and functionally extended) from outside, and here are the use cases!” What seemed at first like a technical solution to a technical problem has turned into a revelation.
But when this is not the case — the Visitor instance is just a design artifact imposed upon the problem — the development enters a bad phase that I define as “technological dissonance”. The developers are busy solving a problem which is not the one they contracted to solve (and are thus living in cognitive dissonance), with all the maintenance punishment this invites. Requests for extension/modification will come from one object model and must be mapped to — and from — another object model. Good luck!
6. The “Hierarchical” Visitor Pattern Variant
The “Visitor” pattern fits like a glove over the “Composite” pattern, introduced in the previous lesson. To remind, the “Composite” pattern suggested a non-trivial object model, combined with interface, to allow manipulating a recursive structure form the outside. And the “Visitor” pattern is about that: manipulating objects from the outside (with some help from inside). So, the combination of Visitor over Composite — known as the “Hierarchical Visitor” pattern is quite a natural selection. (In fact, it is so natural, that I have once stumbled upon a tutorial proudly entitled “The Visitor Pattern” that actually described the Hierarchical variant, no mention of its non-hierarchical origins!)
Consider this comprehensive Shape topology, complete with composite shape types, naturally, following the “Composite” pattern.
This object model features several Leaf types and a Composite sub-hierarchy. Line, Circle and Square are leaves. Multi-line and Multi-shape are composites (where Multi-line specializes in Lines), both inheriting the Composite behavior from the abstract Compo-shape.
The restrictive inheritance of association — in “Multi-line is only open to Lines” (although it is built to contain any Shape)— may look counter-intuitive but is frequent in object-modelling (factoring for re-use down the inheritance hierarchy). It does not compromise the principle of substitutability (to be discussed)! The inheritance of association by no means requires Multi-line to contain instances of all available Shape types. It only requires that whatever happens to be inside must comply with the Shape interface, and it — i.e. Line — indeed does!
Still, some refinement is imminent! It turns out that allowing a Visitor access to a composite hierarchy requires a more ambitious interface than just accept/visit (which here, will only work for Leaves). In a clean-designed composite pattern, Composites have nothing of their own to render. Rendering a Composite means iterating on rendering each component inside (which if Composite, recurses further, etc.). Of course, we need a more general (and properly substitutable) interface than expecting the Visitor to inquire if the current Shape is Composite or Leaf and act accordingly. The whole idea of the “Visitor” pattern is that the subject directs the visitor to visit it (the subject) — accept/visit — no embarrassing questions asked!
The solution is to extend the Hierarchical Visitor’s interface to three methods: (1) “Visit” Remains for Leaves. (2) “Visit begin” and (3) “visit end”, are only issued from the Composite’s accept. The iteration on each component (inside the composite) is made by the composite (rather than the visitor), to preserve this interface. Normally, “visit begin” does not render anything significant (besides perhaps a start-delimiter in textual forms), but rather prepares the state for iteration into a sub-level, and same goes for “visit end” (which may clean-up before exiting the level). Recall that “visit begin” does not proceed to iterate inside. This is under the responsibility of the Visitor’s subject!
7. Exercise. Outline with multiple renderers
We are going to build a non-trivial “Hierarchical Visitor”, based on the schoolbook solution to the Outline Document render function exercise from Lesson Two, listed below:
Footnotes:
- “Outline Component” is not abstract, because it implements the default behavior.
- This “generator” loops zero times by default, which is the Leaf behavior, representing emptiness. (We do not need to distinguish between a Leaf and an empty Composite)! The method is to be overridden with proper iteration by the composite implementation. The nonsensical test (“if False”) is a work-around to make Python identify this function as a generator (featuring “yield”, never mind that it is never used).
- The “getter” returns nothing by default, which is the Composite behavior. It is to be overridden by the Leaf implementations.
- The Leaf getter override returns the string hidden inside.
- The Composite iterator override honestly iterates over what is inside.
- The renderer queries the current component for its “header”. This is a more “polite” (and extensible) way than to try to reveal if it is a Leaf or Composite. Only Leaves have a “header” (for the block below, if any) — the string hidden inside.
The challenge: In the present “procedural” implementation, Outline Document renderer is a function that recourses over the Composite structure, issuing a “military numbering” textual output. Refactor this design to support any rendering style, using the “Hierarchical Visitor” pattern, offering the user (at least these) three styles to choose from: (1) military numbering (the present renderer logic), (2) XML, (3) dependency diagram, using the “graphviz” (or another) package.
Here is the object model, highlighting the accept/visit dialog.
Program input:
to develop software
to analyze the requirements
to understand the needs
to model the problem domain
to design the solution
to select architecture
to make design decisions
to model the solution domain
to apply design patterns
to confirm fidelity
to implement the solution
to code the solution
to test the solution
to package the solution
Output #1. Military numbering output:
1. to develop software
1.1. to analyze the requirements
1.1.1. to understand the needs
1.1.2. to model the problem domain
1.2. to design the solution
1.2.1. to select architecture
1.2.1.1. to make design decisions
1.2.2. to model the solution domain
1.2.2.1. to apply design patterns
1.2.3. to confirm fidelity
1.3. to implement the solution
1.3.1. to code the solution
1.3.2. to test the solution
1.3.3. to package the solution
Output #2. XML output:
<outline>
<block>
<line s="to develop software" />
<block>
<line s="to analyze the requirements" />
<block>
<line s="to understand the needs" />
<line s="to model the problem domain" />
</block>
<line s="to design the solution" />
<block>
<line s="to select architecture" />
<block>
<line s="to make design decisions" />
</block>
<line s="to model the solution domain" />
<block>
<line s="to apply design patterns" />
</block>
<line s="to confirm fidelity" />
</block>
<line s="to implement the solution" />
<block>
<line s="to code the solution" />
<line s="to test the solution" />
<line s="to package the solution" />
</block>
</block>
</block>
</outline>
Output #3. Graphic output:
Instructions for rendering the graphic style:
- Install the GraphViz application from http://www.graphviz.org/Download/ (The Python library does not use the C API, but rather invokes the program and directs it to produce a file).
- Make sure the program is in the OS executable search path. (For some reason, this is not the default, at least in the Windows installer).
- Install the Python interface to graphviz (e.g. “graphviz” — but there are many more out there), using the standard pip utility
Example for using the graphviz interface:
Output (file “myDrawing.png”):
FYI, the graphviz interface generates (and silently runs) this graphviz source file:
digraph {
root
A
root -> A
B
root -> B
}
For the schoolbook solution, press this link.
Before departing, here is a final note about injecting design patterns (or subscribing to other intrusive programmatic devices, such as third-party Server/Client/GUI frameworks, etc.) with the side effect of extending the problem domain. The following argument is often neglected, with disastrous results: Even when everyone (including the client) agrees that the required functionality indeed must be extended (regardless of whether emanating from implementation feedback or another source), ask yourself: Is this ready-made panacea that we impose on the problem domain indeed the right way to do it? Does it indeed respond, as stated, to significant business — rather than technical — needs)?
The authors of the “Visitor” pattern had a certain problem domain in mind, and they specify exhaustive examples to demonstrate it. But is this the correct extension (of the problem domain) to fit the requirements of the application that you are constructing? It may or it may not! You cannot tell, until you have scrutinized the real — rather than imposed — requirements and tried them with prototypes, or whatever it takes. As a sobering exercise, try to explain your conflict to the client (if you are fortunate enough to have one). In response, the client may come up with various use cases featuring extended functionality and (implied) object models. But it is unlikely that a (non-programmer) user will ever come up with the “Visitor” pattern, unaided. Because it is counter-intuitive, in the problem domain! Clients are concerned with responding to so many discrete use cases, and when pushed, may come up with so many discrete alternatives. On the contrary, the one-size-fits-all capability “to handle any alternative in any way” (design pattern magic) is a programmatic device which makes the lives of the implementer and maintainer comfortable and reduces construction costs. (Compare with prefabricated building parts in civil engineering). But — unless really needed — is acceptable only as long as may be kept user-transparent.
In case of the “Outline” application of our example, it is legitimate to return to the client with the revelation: “Although the original requirements call for the Military Numbering output style, we think you may need additional styles, such as in these enclosed samples. Fortunately, we know how to extend the code trivially to do that. In fact, it would be even easier for us that way!” The client may — or may not — agree that additional styles are imminent, and why that is so (thus extending the problem domain). However, before adopting the “Visitor” pattern out of the box, do some analysis (or prototyping) to make sure that the problem domain conceived by its authors indeed fits your requirements! Otherwise, when the time finally comes for extension, you may be disappointed to realize that your expensive infrastructure may indeed be smartly built for extension — but not this particular extension!
And finally, it may also turn out that “Military Numbering” is indeed the only style we will ever need — e.g. for a negligible script — and you should not waste our time and resources on futile experiments. (Unless you can cut/paste a ready-made solution — that does it silently — in less than 60 seconds).
7. What next?
In the first three lessons, we were introduced to the object-oriented solution to the design challenge of functional substitutability: the Message paradigm, involving passing a message to an object of unknown type, resolved during runtime by a method (by that name) in its class. We have seen how Python’s “duck typing” makes substitutability intuitive among any informally — but semantically — related types. We were also introduced to the inheritance object structure, and found it a useful — though in Python, not obligatory — infrastructure to ensure substitutability and factor commonality (of interface, data, and implementation) and facilitate variance (of implementation). We considered some polymorphic design patterns (mainly, “Composite” and “Visitor”) as examples of proper object-oriented design, in their use of substitutability and inheritance to solve common design problems. So far, the “glory” of object-oriented programming! In the next lesson, we are going to discuss some frequently used design patterns whose implementation in Python is so trivial (due to duck typing and the ready availability of object metadata) as to make them redundant, or almost redundant.
Lessons on this course:
- Substitutability and Inheritance
- The glory of OO Substitutability: the “Composite” Pattern
- The limits of OO substitutability: the “Visitor” Pattern (you are here!)
- Some boring design patterns
- The limits of inheritance