© Giuliana Carullo 2020
G. CarulloImplementing Effective Code Reviewshttps://doi.org/10.1007/978-1-4842-6162-0_5

5. Software Architectures

Giuliana Carullo1 
(1)
Dublin, Ireland
 

There is nothing new under the sun. It has all been done before.

—Sherlock Holmes in Sir Arthur Conan Doyle’s “A Study in Scarlet (1887)

In Chapter 2, we started taking a look at some of the possible dos and don’ts around the design phase and introduced why software architectures are important. However, this aspect requires a deeper look. In particular, a strong emphasis on using design patterns properly is needed when dealing with architectures.

Hence, in this chapter, we will dive into the following aspects:
  • Design patterns

  • What each pattern does, how they are used, when to use them, and what not to do

  • Main issues at design time

Code Under the Shower

If the architecture smells, a shower is needed to rinse out the obvious dirty issues so we can better scrub out the worse ones. One of the common ways to improve the design is by using appropriate design patterns to minimize the dirt. Design patterns can be broken down into four main categories:
  • Creational: Which deals with common pattern for object creation

  • Structural: Which aims at simplifying the relations between components

  • Behavioral: Which establishes common pattern for communications

  • Concurrency: Which is designed to address multithreading scenarios

In this section, we will address the main patterns in the first three categories as shown in Table 5-1. Concurrency patterns will not be considered in this book. Although very interesting and useful, they are intentionally left out of its scope. What this book is meant to provide, indeed, is a well-rounded review yet not dealing with all the intricacies of definitely more complex scenarios. However, foundations about concurrent programming will be discussed in Chapter 9, to provide some general guidance in the area.
Table 5-1

Design Patterns

Class

Patterns

Creational

Singleton

 

Lazy initialization

 

Builder

 

Abstract factory

 

Factory method

Structural

Adapter

 

Decorator

 

Façade

 

Composite

Behavioral

Publisher-subscriber

 

Iterator

 

Visitor

 

State

 

Chain of responsibility

Concurrency

See Chapter 9

Note

The images accompanying the pattern descriptions aim to illustrate the general concepts. They are not meant to model the relative Python structure.

Creational Design Patterns: The Days of Creation

In this section, we will explore some of the main creational design patterns (Table 5-2).
Table 5-2

Creational Design Pattern

Class

Patterns

Creational

Singleton

 

Lazy initialization

 

Builder

 

Abstract factory

 

Factory method

Singleton

A singleton is probably the easiest pattern you can encounter. It aims at restricting the number of instances of a class.

How

This pattern hides the constructor of the class by means of declaring it private. A get_instance() method is used to create the instance upon the first call and returning the unique instance at later invocations. See Figure 5-1.
../images/485619_1_En_5_Chapter/485619_1_En_5_Fig1_HTML.jpg
Figure 5-1

Singleton. Getting some “me” time

When

A singleton might be required when
  • There is a need to implement controls to access to shared resources (e.g., in the context of concurrency).

  • A single resource is accessed from several parts of the system.

  • Only a single instance of a class is needed.

A common scenario is to use a singleton for logging purposes.

A very simple implementation of the singleton is in the following snippet of code:
class Singleton:
    def __init__(self):
        if Singleton.__instance:
            raise Exception("I am a singleton!")
        else:
            Singleton.__instance = self
    @staticmethod
    def get_instance():
        if not Singleton.__instance:
            Singleton()
        return Singleton.__instance

Guideline

Singleton inspires controversial thoughts. Some people love it; some do not. Someone else both hates it and loves it at the same time. From time to time, the singleton has been pointed out as a “bad smell. One of the reasons is that it can easily fall into the first lady category analyzed in Chapter 4. You might have this big giant global instance trying to do everything. Personally, I do not see a problem in the singleton per se. If something similar happens, you are just using the wrong pattern for the problem you are trying to solve.

Lazy Initialization

This pattern allows to instantiate an object only when actually required.

How

Implementing this pattern is a simple task. Instead of creating the object into the constructor, it is created upon the first actual request that needs to be performed on the instance. See Figure 5-2.
../images/485619_1_En_5_Chapter/485619_1_En_5_Fig2_HTML.jpg
Figure 5-2

Lazy initialization. From time to time, procrastinating is done for a good cause

The following is a snippet of code showing an example of lazy initialization:
class MyHeavyObject:
    def __init__(self):
        self._heavy = []
    @property
    def heavy(self):
        if not self._heavy:
            print ('I am doing heavy computation.')
            # expensive computation
            ...

When

Lazy instantiations are used when the heavy lifting computational job the object needs to do can be postponed for performance reasons. A common example of implementation is its alternative lazy load. A lazy load is used when integration is needed with a database (DB). Data from the DB is loaded in memory only when required.

This pattern is more on the performance side of the house rather than just clean code. There is no strict rule on when it can be used.

Note

I’ve referred to the lazy load as a common implementation for reading data from a database. However, lazy initialization can be applied to a multitude of applications including web applications.

Guideline

If you don’t have an actual bottleneck in performances that can be improved by using this pattern, don’t use it. Simple, isn’t it?

Builder

The builder pattern follows the KISS (keep it simple stupid) principle discussed in Chapter 2. It breaks down the creation of a complex object into smaller separated creational tasks.

How

The builder exposes the interface to create the complex object as a single task. However, it internally manages the calls to the concrete builders each performing one step of the entire processing needed. See Figure 5-3.
../images/485619_1_En_5_Chapter/485619_1_En_5_Fig3_HTML.jpg
Figure 5-3

Builder. Keep it simple

A simplified version of the builder pattern in Python code is shown in the following:
# Abstract Builder
class Builder(object):
    def __init__(self):
        self.laptop = None
    def new_laptop(self):
        self.laptop = Laptop()
# Concrete Builder
class BuilderVirtualLaptop(Builder):
    def build_cpu(self):
        self.laptop.cpu ='whatever cpu'
    def build_ram(self):
        self.laptop.ram = 'whatever ram'
    ...
# Product
class Laptop(object):
    def __init__(self):
        self.cpu = None
        self.ram = None
        ...
    # print of laptop info
    def __repr__(self):
        return 'Laptop with cpu = {} and ram = {}'.format(self.cpu, self.ram)
# Director
class Director(object):
    def __init__(self):
        self.builder = BuilderVirtualLaptop()
    def construct_laptop(self):
        self.builder.new_laptop()
        self.builder.build_cpu()
        self.builder.build_ram()
        ...
    def get_building(self):
        return self.builder.laptop
#Simple Client
if __name__=="__main__":
    director = Director()
    director.construct_laptop()
    building = director.get_building()
    print (building)

The example code shows the creation of a laptop. Virtually, a laptop can be broken down into several creational tasks that need to be performed in order to have the final product such as CPU, RAM, disk, and so on.

When

In general, this pattern is helpful when the final object can be constructed via an arbitrary (depending on the context) number of tasks. The builder can be considered when the solutions can benefit from
  1. 1.

    Better control of the creational process.

     
  2. 2.

    Hiding complex creation with the builder allows for easier to read and use objects.

     
  3. 3.

    If any of the creational steps need to change, this will only affect a limited portion of the code (i.e., the builder itself)—not directly impacting the code that builds on top of the created object.

     

Guideline

The definition is fairly simple; thus, stick with it, taking into account that embracing this design pattern has minor disadvantages including writing more lines of code (LOCs). A signal that a builder is not appropriate, or not appropriately used, is when the builder constructor has a long list of parameters required to deal with each concrete builder.

Consider, for example, the case where you have a pub and you offer to customers the option to customize their hamburger meal with several toppings.

Building such hamburger might have a long list of toppings (cheese, bacon, lettuce, tomato, onion, ketchup, etc.)

As the business needs to offer a broader choice of toppings to customers, the initially simple builder
Hamburger(cheese)
can quickly escalate to
Hamburger(cheese, lettuce, tomato, onion, bacon, you_name_it)

In such case, proper use of the builder would entail the possibility of customizing the order by, for example, set functions instead of providing support for each and every topping at creation time.

Note

When using this pattern, do not neglect to think longer term. Back to our burger example. What if you are offering only two options, hamburger and cheeseburger, but you ideally will add also customization with toppings in the future? Looking a bit ahead of times can help in achieving the right design and usage from the start.

Abstract Factory

The abstract factory allows you to hide complexity during object creation. This pattern enables to create different versions of the same object.

How

The abstract factory component creates the actual object. It internally manages different components that represent different flavors of the final product. See Figure 5-4.
../images/485619_1_En_5_Chapter/485619_1_En_5_Fig4_HTML.jpg
Figure 5-4

Abstract factory. Simple is better than complex

An example of Python implementation is in the following:
# Interface for operations supported by the factory
class AbstractFactory():
    def create_linux(self):
        pass
    def create_win(self):
        pass
# Concrete factory. Managing object creation.
class ConcreteFactoryOS(AbstractFactory):
    def create_linux(self):
        return ConcreteProductLinux()
    def create_win(self):
        return ConcreteProductWin()
# Abstract Linux product
class AbstractProductLinux():
    def interface_linux(self):
        pass
# Concrete linux product
class ConcreteProductLinux(AbstractProductLinux):
    def interface_linux(self):
        print ('running linux')
# Abstract win product
class AbstractProductWin():
    def interface_win(self):
        pass
# Concrete win product
class ConcreteProductWin(AbstractProductWin):
    def interface_win(self):
        print ('running win')
# Factory usage and testing out
if __name__ == "__main__":
    factory = ConcreteFactoryOS()
    product_linux = factory.create_linux()
    product_win = factory.create_win()
    product_linux.interface_linux()
    product_win.interface_win()

When

In the example code, we have our laptop, but no operating system (OS) is on top yet. The OS can be modeled as an abstract factory, and it returns an instance of one of the different flavors (different components) it supports (e.g., Linux, Mac, Windows). In general, this pattern can be used every time we support different variations of the same object.

Guideline

This pattern is a nice way to hide complexity when the caller does not need to deal with underlying computation. Common sense, do not add complexity when not required. Indeed, adding a new product is not that scalable since it requires new implementations for each factory.

Factory Method

The factory method is similar to the abstract factory, thus often confused. Guess what? Instead of building a factory object, this pattern can be synthesized as a factory (actual) method.

How

By means of inheritance, the factory method can be overwritten to implement different flavors. See Figure 5-5.
../images/485619_1_En_5_Chapter/485619_1_En_5_Fig5_HTML.jpg
Figure 5-5

Factory method. Complex is better than complicated

An example of Python implementation is in the following:
# First Product
class ProductA(object):
    def __init__(self):
        print ('Building Product A')
# Second Product
class ProductB(object):
    def __init__(self):
        print ('Building Product B')
# Factory Method
def factory_method(product_type):
    if product_type == 'PA':
        return ProductA()
    elif product_type == 'PB':
        return ProductB()
    else:
        raise ValueError('Cannot find: {}'.format(product_type))
# Client: testing out
if __name__ == '__main__':
    for product_type in ('PA', 'PB'):
        product = factory_method(product_type)
        print(str(product))

When

In general, instead of dealing with the composition of different sub-objects, this pattern is meant to create an object hiding internal details, while being a single concrete product. This is opposite to the abstract factory.

Guideline

The recommendation is exactly the same as the abstract factory. Do not opt for factories when object abstraction is not needed.

Structural Patterns: The Big Puzzle

This section walks through the principal structural patterns. I like to think about this category as a big puzzle. You have interfaces and code already in place, but you still have to make all the components interact and work in the best possible way. The following patterns help in achieving it. See Table 5-3.
Table 5-3

Structural Design Pattern

Class

Patterns

Structural

Adapter

 

Decorator

 

Façade

 

Composite

Adapter

The adapter is also known as wrapper. It wraps another object, redefining its interface.

How

A new class simply encapsulated the incompatible object, thus providing the desired interface. See Figure 5-6.
../images/485619_1_En_5_Chapter/485619_1_En_5_Fig6_HTML.jpg
Figure 5-6

Adapter. A piece of the puzzle

../images/485619_1_En_5_Chapter/485619_1_En_5_Fig7_HTML.jpg
Figure 5-7

Decorator. Some software craft

The following code shows a general implementation of this pattern:
# Adapter: our wrapping class
class Adapter:
    def __init__(self):
        self._adaptee = Adaptee()
    def request(self):
        self._adaptee.legacy_request()
# Adaptee: existing interface
class Adaptee:
    def legacy_request(self):
        print ('Matchy-matchy now! yay!')
# Client: testing out
if __name__ == "__main__":
    adapter = Adapter()
    adapter.request()

When

The adapter pattern provides a simple way for solving compatibility issues between different interfaces. Suppose a caller is expecting a different interface from a certain object (callee), it can be made compatible by means of the adapter. They can be handy for legacy software. It enables reusability for a lower price.

Guideline

Likewise, all the patterns we discuss in this book add complexity to the code. Always follow the KISS principle and make sure you have interface problems between various components when deciding to implement an adapter.

Note

As for the builder pattern, do not neglect to think longer term. Do you foresee possible changes to an interface you are designing? Adding an adapter might be very well appropriate. At the same time, if an interface can flexibly be changed without causing dependencies to break, the adapter would add unneeded complexity.

Decorator

The decorator enables reusability by means of enhancing an object behavior.

How

Similar to the adapter pattern, it wraps the object adding the wanted functionalities. See Figure 5-7.

A simplified example of decorator is shown in the following snippet of code:
# Decorator interface
class Decorator:
    def __init__(self, component):
        self._component = component
    def operation(self):
        pass
# Decorator
class ConcreteDecorator(Decorator):
    """
    Add responsibilities to the component.
    """
    def operation(self):
        self._component.operation()
        print ('And some more makeup!)'
# Component that needs to be decorated
class Component:
    def operation(self):
        print ('I have some makeup on!)'
# Client: testing out
if __name__ == "__main__":
    component = Component()
    decorator = ConcreteDecorator(component)
    decorator.operation()

When

The decorator design pattern helps in fighting the first lady component smell. Thus, it adds functionalities to an object, while maintaining single responsibility principle. Indeed, the decorator allows for additional behavior without impacting the decorated component. They provide a nice alternative to inheritance and are useful when the behavior needs to be modified at runtime.

Guideline

Simple yet powerful. But, don’t make the decorator become the new first lady. Decorators can complicate the initialization process and the overall design (depending on how many decorators you implement). Make sure to not overcomplicate the design (special attention to multiple layers of decorators): you might be pushing decorators beyond the purpose they are meant to serve.

Facade

A facade can be somehow ideally associated with the abstract factory. However, instead of creating an object, it provides a simpler interface for other more complex interfaces.

How

This pattern provides a brand new higher-level interface in order to make the subsystems (often independent classes with complex logic) easier to use and interact with. See Figure 5-8.
../images/485619_1_En_5_Chapter/485619_1_En_5_Fig8_HTML.jpg
Figure 5-8

Facade. Putting it all together

A simplified example of a facade is shown in the following snippet of code:
# Facade
class Facade:
    def __init__(self):
        self._subsystem_1 = Subsystem1()
        self._subsystem_2 = Subsystem2()
    def operation(self):
        self._subsystem_1.operation1()
        self._subsystem_2.operation2()
# Subsystem
class Subsystem1:
    def operation1(self):
        print ('Subsystem 1: complex operations')
# Subsystem
class Subsystem2:
    def operation2(self):
        print ('Subsystem 2: complex operations')
# Client
if __name__ == "__main__":
    facade = Facade()
    facade.operation()

When

If you are looking at your architecture and it is highly coupled, a facade might help in reducing it.

Guideline

It is very easy to make a facade that acts as a first lady. Please avoid it at all costs.

Composite

The composite pattern provides an interface that aims at managing a group of complex objects and single objects exposing similar functionalities in a uniform manner.

How

It composes objects into a tree structure, in such a way that nodes in the tree—regardless of whether they are a leaf (single object) or complex object (i.e., non-leaves)—can be accessed in a similar way, abstracting complexity to the caller. In particular, when a method is called on a node, if it is a leaf, the node manages it autonomously. Otherwise, the node calls the method upon its children. See Figure 5-9.
../images/485619_1_En_5_Chapter/485619_1_En_5_Fig9_HTML.jpg
Figure 5-9

Composite. Uniform is good

An example of a composite pattern is shown in the following snippet of code:
# Abstract Class
# Defining the interface for all the components in the composite
class Component():
    def operation(self):
        pass
# Composite: managing the tree structure
class Composite(Component):
    def __init__(self):
        self._children = set()
    def operation(self):
        print ('I am a Composite!')
        for child in self._children:
            child.operation()
    def add(self, component):
        self._children.add(component)
    def remove(self, component):
        self._children.discard(component)
# Leaf node
class Leaf(Component):
    def operation(self):
        print ('I am a leaf!')
# Client: testing out
if __name__ == "__main__":
    # Tree structure
    leaf = Leaf()
    composite = Composite()
    composite.add(leaf)
    composite_root = Composite()
    leaf_another = Leaf()
    composite_root.add(composite)
    composite_root.add(leaf_another)
    # Same operation on the entire tree
    composite_root.operation()

When

This pattern can be used when you have to selectively manage a group of heterogeneous and hierarchical objects as they would ideally be the same object. Indeed, this pattern allows for same exploration of the hierarchy, independently from the node type (i.e., leaf and composite).

As a concrete example, think about the hierarchical structure of folders, subfolders, and files on a computer. And consider that the only operation allowed is deletion. For every folder, subfolder, or file, you want the delete operation to be uniformly applied to any substructure (if any). In other words, if you delete a folder, you want to delete also all the subfolders and files contained in it. Modeling this structure as a composite would allow you to perform the deletion in a simpler and cleaner manner.

Guideline

Take a deeper look at your tree structure. A lot of initialized while not used nodes at the frontier (i.e., leaves) might signal that some refactoring is required.

Behavioral Design Patterns: Behave Code, Behave!

Finally, this section explores behavioral patterns: they help in designing common relationships between components. See Table 5-4.
Table 5-4

Behavioral Design Pattern

Class

Patterns

Behavioral

Observer

 

Publisher-subscriber

 

Iterator

 

Visitor

 

State

 

Chain of responsibility

Observer

In operating systems, a common way of notifying changes happening in the system is the polling and interrupts mechanisms. In the context of higher-level programming, a smarter way for notifying changes has been ideated: the observer pattern.

How

A component, namely, a subject, whose state needs to be notified stores a list of dependencies, namely, observers. Each and every time a change occurs in the subject, it notifies it to its stored list of observers as shown in Figure 5-10.
../images/485619_1_En_5_Chapter/485619_1_En_5_Fig10_HTML.jpg
Figure 5-10

Observer. At a glance

../images/485619_1_En_5_Chapter/485619_1_En_5_Fig11_HTML.jpg
Figure 5-11

Publisher-subscriber. Please, let me know what’s going on

An example of an observer pattern is shown in the following snippet of code:
# The observable subject
class Subject:
    def __init__(self):
        self._observers = set()
        self._state = None
    def subscribe(self, observer):
        observer._subject = self
        self._observers.add(observer)
    def unsubscribe(self, observer):
        observer._subject = None
        self._observers.discard(observer)
    def _notify(self):
        for observer in self._observers:
            observer.update(self._state)
    def set_state(self, arg):
        self._state = arg
        self._notify()
# Interface for the Observer
class Observer():
    def __init__(self):
        self._subject = None
        self._observer_state = None
    def update(self, arg):
        pass
# Concrete observer
class ConcreteObserver(Observer):
    def update(self, subject_state):
        self._observer_state = subject_state
        print ('Uh oh! The subject changed state to: {}'.format(subject_state))
        # ...
# Testing out
if __name__ == "__main__":
    subject = Subject()
    concrete_observer = ConcreteObserver()
    subject.subscribe(concrete_observer)
    # External changes: testing purposes
    subject.set_state('Ping')
    subject.set_state('Pong')
Note

The ordering of notifications with this pattern is not strictly related to the ordering of registration.

When

The observer pattern can be used when you need different objects to perform—automatically—some functions based on the state of another one (one to many). It generally suits well cases where broadcasting of information needs to happen and the subject does not need to know specifics or the number of observers.

Guideline

Once again, keep it simple and do not add unnecessary complexity. Always carefully consider the context: do you have observers that might not be interested to all the status changes? This pattern may notify observers also of changes that they are not interested in.

Publisher-Subscriber

Similar to observer patterns, publisher-subscriber patterns enable you to monitor state changes.

How

As confusing as it might initially sound, this pattern is very similar to observer patterns, but they are not actually the same. There are two basic components: publisher, the entity whose state is monitored, and subscriber, the one that is interested in receiving state changes.

The main difference is that the dependency between them is abstracted by a third component—often referred to as broker—that manages the state’s update as shown in Figure 5-11. As a consequence, different from the observer, publisher and subscribers do not know about each other.

Note

A common way for brokers to identify which message needs to be sent to whom is by means of topics. A topic is nothing else than an expression of interest in a specific category of message to be received. Think about subscribing to a mailing list of a library, but you only want to receive messages only for programming and fantasy books. Programming and fantasy would be the topics you subscribed to.

An example of a publisher-subscriber pattern is shown in the following snippet of code:
# Publisher
class Publisher:
    def __init__(self, broker):
        self.state = None
        self._broker = broker
    def set_state(self, arg):
        self._state = arg
        self._broker.publish(arg)
# Subscriber
class Subscriber():
    def __init__(self):
        self._publisher_state = None
    def update(self, state):
        self._publisher_state = state
        print ('Uh oh! The subject changed state to: {}'.format(state))
        # ...
# Broker
class Broker():
    def __init__(self):
        self._subscribers = set()
        self._publishers = set()
        # Setting up a publisher for testing purposes
        pub = Publisher(self)
        self._publishers.add(pub)
    # Triggering changes: testing purposes
    def trigger(self):
        for pub in self._publishers:
            pub.set_state('Ping')
    def subscribe(self, subscriber):
        self._subscribers.add(subscriber)
    def unsubscribe(self, subscriber):
        self._subscribers.discard(subscriber)
    def publish(self, state):
        for sub in self._subscribers:
            sub.update(state)
# Testing out
if __name__ == "__main__":
    # Setting an example
    broker = Broker()
    subscriber = Subscriber()
    broker.subscribe(subscriber)
    # External changes: testing purposes
    broker.trigger()
Note

Be aware that the preceding code is only for showcasing the interactions between the two main components. Some methods—for example, trigger()—are added only to allow a simple flow of execution.

When

This third-party exchange is helpful in any context where message exchange is required without components (publisher with relative subscribers) being aware of each other’s existence. It is common to find pub-sub applications in almost any distributed message exchanging scenarios. For example, in the Internet of Things (IoT) world, tools such as Mosquitto and MQTT1 are commonly used to implement publisher-subscriber that allows for message exchanges between distributed elements in the network.

Note

As you can imagine from the IoT example, any application of this pattern can use, but it is not limited to, a single publisher. Indeed, this pattern can be generalized to allow exchange of messages between any arbitrary number of publishers and subscribers.

Guideline

Do not overlook the scalability requirements of your solution. The broker might constitute a bottleneck for the entire message exchange.

Iterator

The iterator allows to navigate elements within an object, abstracting internal management.

How

Commonly, this pattern exposes two methods next() and hasNext() to perform the traversal. See Figure 5-12. Python implementations normally require an iterable object to implement:
  1. 1.

    Iter: Which returns the instance object

     
  2. 2.

    Next: Which will return the next value of the iterable

     
../images/485619_1_En_5_Chapter/485619_1_En_5_Fig12_HTML.jpg
Figure 5-12

Iterator. Looking forward

A simple implementation of these two methods is presented in the following snippet:
# Our collection of elements
class MyCollection():
    def __init__(self):
        self._data = list()
    def populate(self):
        for el in range(0, 10):
            self._data.append(el)
    def __iter__(self):
        return Iterator(self._data)
# Our iterator
class Iterator():
    def __init__(self, data):
        self._data = data
        self._counter = 0
    def next(self):
        if self._counter == len(self._data):
            raise StopIteration
        to_ret = self._data[self._counter]
        self._counter = self._counter + 1
        return to_ret
# Testing out
if __name__ == "__main__":
    collection = MyCollection()
    collection.populate()
    for el in collection:
        print (el)

In the code example, StopIteration signals that there are no more elements in the collection.

When

Probably one of the most common applications is for data structures where elements within them can be (oftentimes sequentially) accessed without knowing inner functioning. However, it can be used any time a traversal is needed without introducing changes to current interfaces.

Guideline

If the collection is small and simple, it might be not really required.

Visitor

The visitor allows you to decouple operational logic (i.e., algorithms) that would be—otherwise—scattered throughout different similar objects.

How

A visitor provides a visit( ) interface that allows to traverse the objects. A ConcreteVisitor implements the actual traversal. A visitable interface defines an accept( ) method. A ConcreteVisitable given the visitor object implements the accept operation. See Figure 5-13.
../images/485619_1_En_5_Chapter/485619_1_En_5_Fig13_HTML.jpg
Figure 5-13

Visitor. I'll be there in a second

../images/485619_1_En_5_Chapter/485619_1_En_5_Fig14_HTML.jpg
Figure 5-14

State. How are you doing?

An example of a visitor pattern is shown in the following snippet of code:
# Visitable supported operations
class VisitableElement():
    def accept(self, visitor):
        pass
# Concrete element to be visited
class ConcreteElement(VisitableElement):
    def __init__(self):
        self._el = 'Concrete element'
    def accept(self, visitor):
        visitor.visit(self)
# Visitor allowed operations
class Visitor():
    def visit(self, concrete_element_a):
        pass
# Implementing actual visit
class ConcreteVisitor(Visitor):
    def visit(self, concrete_element):
        print ('Visiting {}'.format(concrete_element._el))
# Testing out
if __name__ == "__main__":
    concrete_visitor = ConcreteVisitor()
    concrete_element = ConcreteElement()
    concrete_element.accept(concrete_visitor)

When

An example of an application of the visitor pattern is within data structures for tree traversal (e.g., pre-order, in-order, post-order). It suites fairly well treelike structures (e.g., syntax parsing), but is not strictly tight to these cases. Visitor pattern is not used only for treelike structures. Generally speaking, it can be applied when a complex computation needs to be applied depending on the object traversed. Back to our folder example, folder deletion can be performed at every layer (subfolder, files), yet the actual deletion requires different code for the operation to be performed.

Guideline

Avoid building visitors around unstable components. If the hierarchy is likely to change over time, the visitor pattern may not be appropriate. If the structure is stable and you want to apply the same function within it, it is more likely that the visitor pattern might suit your needs.

State

The state pattern enables context-aware objects.

How

The state pattern design is fairly simple: a context that represents the external interface, a state abstract class, and different state implementations that define the actual states. See Figure 5-14.

The following is a simple implementation of the state design pattern:
# Context definition
class Context:
    def __init__(self, state):
        self._state = state
    def manage(self):
        self._state.behave()
# Abstract State class
class State():
    def behave(self):
        pass
# Concrete State Implementation
class ConcreteState():
    def behave(self):
        print ('State specific behaviour!')
# Testing out
if __name__ == "__main__":
    state = ConcreteState()
    context = Context(state)
    context.manage()

When

The state pattern is helpful each time the behavior of an object is dependent of its state. In other words, it is applicable when the objects require changes in behavior depending on the state’s changes.

The state pattern is commonly used in user interface (UI) development. React2 provides a state built-in object that allows to dynamically reflect changes in state into the displayed UI.

Guideline

Pay close attention to when you actually use it. The number of states might exponentially grow, hence impacting the complexity of the code. It is also worth noticing that storing data that does not change in a state object is not considered a good practice due to its impact on readability: it does not necessarily affect performances, yet storing data elsewhere might increase how easy to use it would be.

Back to our react example: if the state does not change (e.g., all we want the user to see is a blank page with the title “Hello World!”), there is no real need to increase the complexity of the code for a page which is not dynamic.

Chain of Responsibility

The chain of responsibility pattern fosters decoupling between the sender of a request and the receiver.

How

More objects within a pipeline are given a chance to handle an incoming request. In particular, the request is passed sequentially through the pipeline until an object is actually able to handle it. See Figure 5-15.
../images/485619_1_En_5_Chapter/485619_1_En_5_Fig15_HTML.jpg
Figure 5-15

Chain of responsibility. Micromanaging is never the case

The following is a simple implementation of the chain of responsibility design pattern:
# Handler Interface
class Handler():
    def __init__(self,request=None, successor=None):
        self._successor = successor
    def handle_request(self):
        pass
# Concrete Handler
class IntegerHandler(Handler):
    def handle_request(self):
        if request.get_type() is int:
            print (self.__class__.__name__)
        elif self._successor is not None:
            self._successor.handle_request()
# Another Concrete Handler
class StringHandler(Handler):
    def handle_request(self):
        if request.get_type() is str:
            print (self.__class__.__name__)
        elif self._successor is not None:
            self._successor.handle_request()
# Simple Request object
class Request():
    def __init__(self):
        self._el = 'I am a string'
    def get_type(self):
        return type(self._el)
# Testing out
if __name__ == "__main__":
    request = Request()
    string_handler = StringHandler(request=request)
    int_handler = IntegerHandler(request=request,successor=string_handler)
    int_handler.handle_request()

When

In cases when you want to abstract the processing pipeline by allowing a request to travel until it finds a handler able to take charge of it. It is a pretty handy pattern because you can decide which and in which order handlers are added to the chain.

Guideline

Back to nonfunctional requirements. Keep an eye on the required performances. Too many handlers (executed sequentially, in worst case skipping up to the very last handler in the chain) might impact code performances. Also bear in mind that debugging this pattern could be fairly difficult.

Summary

In this chapter, we provided guidance on the most common design patterns: how do they work, when to use them, and issues to consider for each of them.

Main takeaways
  • Design patterns are meant to provide solutions for common problems. When choosing or reviewing design patterns in the codebase, consider if the problem you are trying to solve is very similar to the specific goal of the design pattern you want to use in your implementation.

  • As always, keep it simple.

In the next chapter, we will go another step higher from the nitty-gritty details of code alone by providing guidelines from a design perspective.

Further Reading

Design patterns are widely used and discussed given their importance, ranging from basic design patterns explored in Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma and colleagues (Addison-Wesley Professional, 1994) to more advanced enterprise patterns in Patterns of Enterprise Application Architecture by Martin Fowler (Addison-Wesley Professional, 2002). The latter is probably one of my absolutely preferred books on the topic; give it a try if you are serious about design patterns.

Code Review Checklist

  1. 1.

    Are you using design patterns properly?

     
  2. 2.

    Are the patterns the optimal choice based on requirements?

     
  3. 3.

    Are performances taken into account when choosing the design pattern you are inspecting?

     
  4. 4.

    Is the decorator a first lady?

     
  5. 5.

    Does the pattern hinder performance requirements?

     
  6. 6.

    Is your singleton behaving like a first lady?

     
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.60.192