Week 6 - Behavioral Patterns: Observer and Strategy Patterns

Lecture recording here.

Lab recording here.

Introduction

We start our study of behavioural patterns. The observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. The strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

Videos

The Observer Design PatternObserver Design Pattern
The Observer Pattern Explained and Implemented in Java
The Strategy Design PatternWhat is the Strategy Pattern? (Software Design Patterns)
The Strategy Pattern Explained and Implemented in Java

Assignment(s)

Assignment 3 - Investigation of Design Patterns

The Observer Design Pattern

The Rationale

The Observer software design pattern is used when you want to establish a one-to-many relationship between objects, so that when one object changes state, all its dependents are notified and updated automatically. The observer design pattern promotes loose coupling between objects, which means that the objects don't have to know much about each other, and can be easily modified without affecting other parts of the system.

By using the Observer pattern, the subject object (the one that is being observed) and the observer objects (the ones that are notified of changes) can be developed and tested independently of each other. This makes the code easier to maintain, extend, and reuse. Another benefit of this pattern is that it allows for more flexibility in designing systems, as it enables the dynamic addition and removal of observers at runtime. This means that new functionalities can be easily added without changing the core code, and unwanted functionality can be removed without affecting the rest of the system.

The UML

Here is the UML diagram for the Observer pattern:
The Observer Pattern
Here are the components of the Observer design pattern:

  1. Subject: This component maintains the state of the object being observed, and notifies observers of any changes in the state.
  2. Observer: This component is an abstract class that defines the interface for updating the observer objects.
  3. Concrete Observer(s): The ConcreteObserver(s) is/are derived from Observer and is/are notified of changes to the Subject.
  4. registerObserver, unregisterObserver, notifyObservers: These functions manage the list of observers and notify them of any changes.

Code Example - Observer

In the following code example, the class Subject is used to add and remove observers, as well as notify them. The class Observer is the abstract class that is the base class for ConcreteObserver:
C++: Observer.cpp.
C#: Observer.cs.
Java: ObserverMain.java.
Python: Python.py.

Common Usage

The following are some common usages of the observer pattern:

  1. User interface design: The Observer pattern is often used in user interface design to enable views to automatically update when the data they are presenting changes. This can be useful in applications that involve real-time data feeds or frequent updates.
  2. Event-driven systems: The Observer pattern is used extensively in event-driven systems, where events are generated by one object and consumed by others. For example, in a financial trading system, the observer pattern could be used to notify interested parties of changes in market prices.
  3. Reactive programming: The Observer pattern is also used in reactive programming, where changes in data are propagated automatically to dependent computations. Reactive programming is commonly used in web development, where it is used to build responsive and interactive user interfaces.
  4. Logging and debugging: The Observer pattern can be used to log or debug system events, by having observers that listen for events and log them to a file or display them in real-time.

Code Problem - Random Number Generator

We have code that implements several types of random numbers. The number of random numbers is not known at compile time. The user will be prompted for the number of random numbers. Once this has been received, all random number generators have to be notified. The following example generates a series of low and high definition random numbers. A thread receives user input for how many random numbers should be generated. Once the input is received, all registered objects are notified, each generating a number of random numbers:
Observer.h, the abstract base class for all kinds of observers,
LowDefinitionObserver.h, a concrete observer,
HighDefinitionObserver.h, a concrete observer,
Subject.h, abstract class for all kinds of subjects (not necessary),
IntegerSubject.h, a concrete subject,
RandomNumber.cpp, the main function.

Code Problem - Library Statistics

The following code adds book and DVD items to a library. A book observer and DVD observer keep track of the number of book and DVDs respectively and their types. So, each time an item is added to the library, an update function is called in the observers to update statistics. The observers can also print statistics. For instance the book observer prints how many books and also the percentages of each type of book. Likewise the DVD observer prints how many DVDs and the percentage of each type of DVD. The code can be seen below.
Observer.h, the abstract class for all observers,
BookObserver.h, the concrete book observer,
DVDObserver.h, the concrete DVD observer,
Item.h, an interface class for all subjects (a library item),
Book.h, the book concrete subject,
DVD.h, the DVD concrete subject,
Library.h, the library,
LibraryMain.cpp, the main function.

The Strategy Design Pattern

The Rationale

The Strategy software design pattern is used to define a family of algorithms, encapsulate each one, and make them interchangeable. This pattern therefore provides a flexible way to switch between different algorithms or behaviors at runtime, without changing the overall structure of the system.

By using the Strategy pattern, you can separate the implementation of an algorithm from its context, which makes the code more modular, easier to understand, and easier to maintain. This also promotes the Open/Closed principle, which states that software entities should be open for extension but closed for modification. Another benefit of this pattern is that it enables better code reuse, as you can create a library of interchangeable algorithms that can be used across multiple projects or applications.

The UML

Here is the UML diagram for the ... pattern:
The Strategy Pattern
Here are the components of the Observer design pattern:

  1. Context: The Context class implements one of the concrete strategies through the Strategy interface.
  2. Strategy: This component defines the algorithm interface that is implemented by the various ConcreteStrategy classes.
  3. ConcreteStrategy: These components contain specific algorithmic implementations of the Strategy interface.

Code Example - Concrete Strategy

In this example, the class Context sets and executes a concrete strategy. The class Strategy is the interface class from which ConcreteStrategyA and ConcreteStrategyB are derived:
C++: Strategy.cpp.
C#: Strategy.cs.
Java: StrategyMain.java.
Python: Strategy.py.

Common Usage

The following are some common usages of the strategy pattern:

  1. Sorting algorithms: The Strategy pattern is commonly used in sorting algorithms, where different sorting algorithms can be implemented as separate strategies that can be swapped in and out at runtime.
  2. Compression algorithms: The Strategy pattern can be used to implement different compression algorithms, such as gzip, zlib, or lz4. Each algorithm can be implemented as a separate strategy, which can be selected at runtime based on the requirements of the application.
  3. Payment processing: The Strategy pattern can be used in payment processing systems, where different payment methods (such as credit card, PayPal, or Bitcoin) can be implemented as separate strategies that can be selected at runtime based on customer preferences.
  4. User interface design: The Strategy pattern can be used in user interface design to implement different layouts, styles, or behavior for different devices (such as desktop, mobile, or tablet). Each device can be implemented as a separate strategy, which can be selected at runtime based on the user's device.
  5. Database access: The Strategy pattern can be used to implement different database access strategies, such as using different ORM (Object-Relational Mapping) frameworks or database engines. Each strategy can be implemented as a separate module, which can be selected at runtime based on the needs of the application.

Code Problem - Sorting Strategy

The Standard Template Library provides us with a few sorting algorithms. Which algorithm we use depends on what we want sorted. The following code uses one of three sorting strategies. Each strategy is based on an STL algorithm:
SortingStrategy.h, abstract base class for sorting strategies,
StdSortStrategy.h, concrete sorting strategy using STL::sort,
StdStableSortStrategy.h, concrete sorting strategy using STL::stable_sort,
StdPartialSortStrategy.h, concrete sorting strategy using STL::partial_sort,
Sorter.h, context that uses a sorting strategy to sort data,
SorterMain.cpp, main function for sorting.

Code Problem - Custom Sorting Strategy

The following code performs sorting without using STL's sorting algorithm. The bubble sort and quick sort algorithms are coded.
SortingStrategy.h, abstract base class for sorting strategies,
BubbleSort.h, the bubble sort concrete class,
QuickSort.h, the quick sort concrete class,
Sorter.h, context for sorting data,
SortMain.cpp, the main function for the sorter.

Code Problem - Combined Sorting Strategy

Since the interfaces used in the above two examples are the same, it is quite easy to add the sorting strategies QuickSort and BubbleSort to the strategies of the first example:
SortingStrategy.h, abstract base class for sorting strategies,
BubbleSort.h, concrete sorting strategy using BubbleSort,
QuickSort.h, concrete sorting strategy using QuickSort,
StdSortStrategy.h, concrete sorting strategy using STL::sort,
StdStableSortStrategy.h, concrete sorting strategy using STL::stable_sort,
StdPartialSortStrategy.h, concrete sorting strategy using STL::partial_sort,
Sorter.h, context that uses a sorting strategy to sort data,
SorterMain.cpp, main function for sorting.

Workshop Custom Design Patterns

Streaming Applications

Software developers typically build streaming applications using multimedia streaming frameworks like DirectShow, GStreamer, and Symbian MMF. Although a significant amount of work has been done on architectural and design patterns in software engineering, there is a limited notion of patterns in the development of multimedia streaming software.

Since it is costly and time-consuming to build multimedia streaming software from scratch, multimedia frameworks enable streaming applications to be assembled by integrating pluggable media components. In addition, multimedia frameworks isolate applications from a variety of complex tasks such as handling of the complex multimedia acceleration hardware, data transport, and synchronization between various tasks.

One of the broadly recognized approaches in the development of the multimedia streaming software is to structure the streaming software as Pipes and Filters. The Pipes and Filters architectural pattern divides a complex functionality into several sequential processing subfunctionalities forming a streaming graph.

An activity diagram for the pipes and filters pattern is shown below:
Pipes and Filters Pattern

The following is an example of the Pipes and Filters pattern: Filter.h, DoubleFilter.h, EvenFilter.h, SumFilter.h, Pipe.h and PipesAndFilters.cpp.
If you look at the code carefully, you will see this to be a variation of the observer pattern.

An article on design patterns for multimedia can be seen at Architectural and Design Patterns in Multimedia Streaming Software, Dajsuren and van den Brand, 2014.

Event Driven Architecture - Publish/Subscribe

Event-driven architecture is a model of software design that uses the flow of events through a system to drive the system's responses. Request/response systems - the traditional form of software architecture - instead require application components to request updates and wait for a response. In many contexts, request/response systems work well enough. But when enterprises require greater scalability, resiliency, or usage of realtime data, event-driven architecture can offer an outsized advantage over other architecture designs.

See Event-driven architecture patterns and when to use them for more details.

We will look at the publish/subscribe pattern. In the Publish/Subscribe pattern, publishers and subscribers are decoupled, and communication between the two is asynchronous. Publisher and subscriber components are separated and a broker facilitates the processing of events and the passing of events from publishers to subscribers in realtime.

Key to the Publish/Subscribe pattern is the shift of architectural complexity from client to publisher. With a Pub/Sub pattern, even complex applications can become as smooth and seamless as smaller, simpler applications. The shift of complexity means the client isn't stuck polling and waiting and polling again. Events drive processes and with brokers handling message routing, components are free to keep running to support the larger application.

Below is a simple implementation of the publish/subscribe design pattern in C++. As you can see, this is effectively the observer design pattern once again.
PubSub.cpp.

A more complex example of the publish/subscribe design pattern is given below. In this example, the mouse subscriber subscribes to mouse events and the key subscriber to key events. This is also a variation of the observer design pattern:
Event.h, KeyEvent.h, MouseEvent.h, EventManager.h, Subscriber.h, KeySubscriber.h, MouseSubscriber.h, MouseKeyMain.cpp.