CoSApp examples
Visitors¶
General concept¶
Visitors are special objects meant to analyze, or “visit” other objects, referred to as components. The main idea is that a visitor usually responds differently when confronted to different types of components.
A real-life analogy would be a veterinarian performing health checks on animals. The actual care provided will obviously differ for cats, rabbits and snakes.
A list of animals to inspect are submitted to the veterinarian. The purpose of the visitor pattern is to avoid writting a set of instructions of the kind: “if it’s a cat, do this; if it’s a rabbit, do that; etc.”
Here is how it goes, roughly: * Each animal (a.k.a. component) has a method accept(visitor)
, specifying which method the visitor should use on them. It is typically something like:
class Cat(Animal):
def accept(self, visitor):
visitor.visit_cat(self)
class Rabbit(Animal):
def accept(self, visitor):
visitor.visit_rabbit(self)
# And so on
Any visitor knowing what to do with cats, rabbits or snakes can now be sent to visit a group of animals. Thus, the base class for veterinarians will be something like:
class Veterinarian:
def visit_cat(self, cat):
pass
def visit_rabbit(self, rabbit):
pass
def visit_snake(self, snake):
pass
Implentations of Veterinarian
will specify actions to be performed for all three kinds of supported animals.
class DentalChecker(Veterinarian):
def visit_cat(self, cat):
# check canines and back teeth
def visit_rabbit(self, rabbit):
# check front teeth
def visit_snake(self, snake):
# check fangs and venom ducts
class LegChecker(Veterinarian):
def visit_cat(self, cat):
# check retractable claws
def visit_rabbit(self, rabbit):
# check back leg tonicity
def visit_snake(self, snake):
pass # nothing to do!
Thus, different kinds of visitors can be sent to different groups of animals, without any type check, and any change in the animal classes.
def send_vet(vet: Veterinarian, animals: List[Animal]):
"""Have `vet` visit `animals`"""
for animal in animals:
animal.accept(vet)
kitty = Cat()
simba = Cat()
roger = Rabbit()
monty = Snake()
conda = Snake()
legcheck = LegChecker()
dental = DentalChecker()
send_vet(dental, [simba, monty, roger])
send_vet(legcheck, [kitty, roger, simba, conda])
Implementation in CoSApp¶
A basic visitor pattern has been implemented in CoSApp, distinguishing systems, ports and drivers. The base interface is similar to that of Veterinarian
, with empty implementations.
[1]:
from cosapp.patterns.visitor import Visitor
help(Visitor)
Help on class Visitor in module cosapp.patterns.visitor:
class Visitor(builtins.object)
| Base class for visitors
|
| Methods defined here:
|
| visit_driver(self, driver) -> None
|
| visit_port(self, port) -> None
|
| visit_system(self, system) -> None
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables
|
| __weakref__
| list of weak references to the object
A visitor can be something like:
[2]:
from cosapp.patterns.visitor import Visitor, send as send_visitor
class DataCollector(Visitor):
def __init__(self):
self.data = {
'systems': dict(),
'ports': dict(),
'drivers': None,
}
def visit_system(self, system) -> None:
key = system.full_name()
self.data['systems'][key] = system.size
send_visitor(self, system.inputs.values())
def visit_port(self, port) -> None:
key = type(port).__name__
self.data['ports'].setdefault(key, 0)
self.data['ports'][key] += 1
Specialize further a visitor¶
The basic interface of Visitor
does not make any distinction between systems themselves. This arguably defeats the primary purpose of the visitor pattern, if the component collection only contains system.
Worry not! Here is how you can introduce more granularity in class selection.
[3]:
import numpy
from cosapp.base import Port, System
from cosapp.patterns.visitor import Visitor
class AbPort(Port):
def setup(self):
self.add_variable('a', 1.2)
self.add_variable('b', 0.0)
class XyPort(Port):
def setup(self):
self.add_variable('x', numpy.zeros(3))
self.add_variable('y', 1.0)
class Foo(System):
def setup(self):
self.add_input(AbPort, 'ab_in')
self.add_output(XyPort, 'xy_out')
def compute(self):
ab = self.ab_in
self.xy_out.x[:] = ab.a
self.xy_out.y = 2 * ab.a - ab.b**2
def accept(self, visitor: Visitor):
try:
visitor.visit_foo(self)
except AttributeError:
# Visitor does not have `visit_foo`
# Fall back to base class implementation
super().accept(visitor)
class Bar(System):
def setup(self):
self.add_input(XyPort, 'xy_in')
self.add_output(AbPort, 'ab_out')
self.add_output(XyPort, 'xy_out')
def compute(self):
# irrelevant, here
pass
def accept(self, visitor: Visitor):
try:
visitor.visit_bar(self)
except AttributeError:
super().accept(visitor)
class Bogus(System):
def setup(self):
self.add_inward('u', 1.0)
self.add_outward('v', 0.0)
# whatever
[4]:
from cosapp.patterns.visitor import Visitor
class FooBarVisitor(Visitor):
def __init__(self):
self.data = dict()
def visit_system(self, system) -> None:
# Default behaviour for `System`
self.data.setdefault('System', [])
self.data['System'].append(type(system).__name__)
def visit_foo(self, foo) -> None:
# Collect names of `Foo` instances
self.data.setdefault('Foo', [])
self.data['Foo'].append(foo.name)
def visit_bar(self, foo) -> None:
# Count `Bar` instances
self.data.setdefault('Bar', 0)
self.data['Bar'] += 1
[5]:
from cosapp.patterns.visitor import send as send_visitor
foo = Foo('foo')
# Primary sub-systems
foo.add_child(Bogus('p'))
foo.add_child(Bar('q'))
# Secondarty sub-systems
foo.p.add_child(System('empty'))
foo.p.add_child(Bar('bar'))
foo.q.add_child(Foo('subfoo'))
visitor1 = DataCollector()
visitor2 = FooBarVisitor()
send_visitor(visitor1, foo.tree())
send_visitor(visitor2, foo.tree())
print("visitor1:", visitor1.data, sep="\n")
print("visitor2:", visitor2.data, sep="\n")
visitor1:
{'systems': {'foo.p.empty': 1, 'foo.p.bar': 1, 'foo.p': 3, 'foo.q.subfoo': 1, 'foo.q': 2, 'foo': 6}, 'ports': {'ExtensiblePort': 6, 'ModeVarPort': 6, 'XyPort': 2, 'AbPort': 2}, 'drivers': None}
visitor2:
{'System': ['System', 'Bogus'], 'Bar': 2, 'Foo': ['subfoo', 'foo']}
Notes¶
Notice that the fallback case in
Foo.accept
andBar.accept
invokesuper().accept(visitor)
, rather thanvisitor.visit_system(self)
. Even though the two calls are equivalent, the former is more robust to changes, and semantically more correct.Data collection through a system tree without type specialization can usually be achieved more simply with a plain function using iterator
system.tree()
(see tips & tricks). Use of the visitor pattern is interesting for more complex cases.The per-class specialization of method
accept
can also be implemented for ports and drivers.