Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
roth_stephan_clean_c20_sustainable_software_development_patt.pdf
Скачиваний:
29
Добавлен:
27.03.2023
Размер:
7.26 Mб
Скачать

Chapter 9 Design Patterns and Idioms

Observer

A well-known architecture pattern for the structuring of software systems is Model-­ View-Controller (MVC). With the help of this architecture pattern, which is described in detail in the book Pattern-Oriented Software Architecture [Busch96], the presentation part (User Interface) of an application is usually structured. The principle behind it

is separation of concerns (SoC). Among other things, the data to be displayed, which is held in the model, is separated from the manifold visual representations (so-called views) of these data.

In MVC, the coupling between the views and the model should be as loose as possible. This loose coupling is usually realized with the Observer pattern. The Observer is a behavioral pattern that is described in [Gamma95] and it has the following intent:

“Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.”

—Erich Gamma et al., Design Patterns [Gamma95]

As usual, the pattern can best be explained using an example. Let’s consider a spreadsheet application, which is a natural constituent of many office software suites. In such an application, the data can be displayed in a worksheet, in a pie chart graphic, and in many other presentation forms; the so-called views. Different views on the data can be created and closed again.

First we need an abstract element for the views that is called Observer. See Listing 9-31.

Listing 9-31.  The Observer Abstract

#include <memory>

class Observer { public:

virtual ~Observer() = default;

virtual int getId() const noexcept = 0; virtual void update() = 0;

};

bool operator==(const Observer& lhs, const Observer& rhs) {

416

Chapter 9 Design Patterns and Idioms

return lhs.getId() == rhs.getId();

}

using ObserverPtr = std::shared_ptr<Observer>;

The Observers observe a so-called Subject. For this purpose, they can be registered at the Subject, and they can also be deregistered. See Listing 9-32.

Listing 9-32.  Observers Can Be Added to and Removed From a Subject

#include "Observer.h" #include <algorithm>

#include <vector>

;

class Subject { public:

void addObserver(const ObserverPtr& observerToAdd) { if (isNotYetObservingThisSubject(observerToAdd)) {

observers.push_back(observerToAdd);

}

}

void removeObserver(ObserverPtr& observerToRemove) { std::erase(observers, observerToRemove);

}

protected:

void notifyAllObservers() const {

for (const auto& observer : observers) { observer->update();

}

}

private:

std::vector<ObserverPtr> observers;

};

417

Chapter 9 Design Patterns and Idioms

In addition to the Subject class, a functor named IsEqualTo is also defined (see Chapter 7 about functors), which is used for comparisons when adding and removing observers. The functor compares the IDs of the Observer. It would also be conceivable that it compares the memory addresses of the Observer instances. Then it would even be possible for several observers of the same type to register at the Subject.

The core is the notifyAllObservers() member function. It is protected since it is intended to be called by the concrete subjects that are inherited from this one. This

function iterates over all registered observers and calls their update() member function. Let’s look at a concrete subject, the SpreadsheetModel. See Listing 9-33.

Listing 9-33.  The SpreadsheetModel Is a Concrete Subject

#include "Subject.h" #include <iostream> #include <string_view>

class SpreadsheetModel : public Subject { public:

void changeCellValue(std::string_view column, const int row, const double value) {

std::cout << "Cell [" << column << ", " << row << "] = " << value << std::endl;

// Change value of a spreadsheet cell, and then...

notifyAllObservers();

}

};

This, of course, is only an absolute minimum of a SpreadsheetModel. It just serves to explain the functional principle of the pattern. The only thing you can do here is call a member function that calls the inherited notifyAllObservers() function.

The three concrete observers in our example that implement the update() member function of the Observer interface are the three views TableView, BarChartView, and PieChartView. See Listing 9-34.

418

Chapter 9 Design Patterns and Idioms

Listing 9-34.  Three Concrete Views Implement the Abstract Observer Interface

#include "Observer.h" #include "SpreadsheetModel.h"

class TableView : public Observer { public:

explicit TableView(SpreadsheetModel& theModel) : model { theModel } { }

int getId() const noexcept override { return 1;

}

void update() override {

std::cout << "Update of TableView." << std::endl;

}

private: SpreadsheetModel& model;

};

class BarChartView : public Observer { public:

explicit BarChartView(SpreadsheetModel& theModel) : model { theModel } { }

int getId() const noexcept override { return 2;

}

void update() override {

std::cout << "Update of BarChartView." << std::endl;

}

private: SpreadsheetModel& model;

};

class PieChartView : public Observer { public:

419

Chapter 9 Design Patterns and Idioms

explicit PieChartView(SpreadsheetModel& theModel) : model { theModel } { }

int getId() const noexcept override { return 3;

}

void update() override {

std::cout << "Update of PieChartView." << std::endl;

}

private: SpreadsheetModel& model;

};

I think it is time again to show an overview in the form of a class diagram. Figure 9-11 depicts the structure (classes and dependencies) that have arisen.

Figure 9-11.  When the SpreadsheetModel gets changed, it notifies all its observers

In the main() function, we now use the SpreadsheetModel and the three views, as shown in Listing 9-35.

420

Chapter 9 Design Patterns and Idioms

Listing 9-35.  Our SpreadsheetModel and the Three Views Assembled Together and in Action

#include "SpreadsheetModel.h" #include "Views.h"

int main() {

SpreadsheetModel spreadsheetModel { };

ObserverPtr observer1 = std::make_shared<TableView>(spreadsheetModel); spreadsheetModel.addObserver(observer1);

ObserverPtr observer2 = std::make_shared<BarChartView>(spreadsheetModel); spreadsheetModel.addObserver(observer2);

spreadsheetModel.changeCellValue("A", 1, 42);

spreadsheetModel.removeObserver(observer1);

spreadsheetModel.changeCellValue("B", 2, 23.1);

ObserverPtr observer3 = std::make_shared<PieChartView>(spreadsheetModel); spreadsheetModel.addObserver(observer3);

spreadsheetModel.changeCellValue("C", 3, 3.1415926);

return 0;

}

After compiling and running the program, we see the following on the standard output:

Cell [A, 1] = 42

Update of TableView.

Update of BarChartView.

Cell [B, 2] = 23.1

Update of BarChartView.

Cell [C, 3] = 3.14153

Update of BarChartView.

Update of PieChartView.

421