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

Chapter 9 Design Patterns and Idioms

In addition to the positive feature of loose coupling (the concrete subject knows nothing about the Observers), this pattern also supports the open-closed principle very well. New concrete observers (in our case, new views) can be added very easily since nothing needs to be adjusted or changed in the existing classes.

Factories

According to the separation of concerns (SoC) principle, object creation or procurement should be separated from the domain-specific tasks that an object has. The dependency injection pattern follows this principle in a straightforward way, because the whole object creation and dependency resolution process is centralized in an infrastructure element, and the objects themselves do not have to worry about it.

But what should we do if an object must be dynamically created at some point at runtime? Well, this task can then be taken over by an object factory.

The Factory design pattern is basically relatively simple and appears in code bases in many different forms and varieties. In addition to the SoC principle, information hiding (see Chapter 3) is also greatly supported, because the creation process of an instance should be concealed from its users.

As stated, factories can be found in countless forms and variants. We discuss only a simple variant.

Simple Factory

The simplest implementation of a Factory probably looks like Listing 9-36 (we take up the Logging example from the DI section).

Listing 9-36.  Probably the Simplest Imaginable Object Factory

#include "LoggingFacility.h" #include "StandardOutputLogger.h"

class LoggerFactory { public:

static Logger create() {

return std::make_shared<StandardOutputLogger>();

}

};

422

Chapter 9 Design Patterns and Idioms

Usage of this very simple factory looks like Listing 9-37.

Listing 9-37.  Using the LoggerFactory to Create a Logger Instance

#include "LoggerFactory.h"

int main() {

Logger logger = LoggerFactory::create(); // ...log something...

return 0;

}

Maybe you’ll ask now, whether it is at all worth it to spend an extra class for such a puny task. Well, maybe not. It’s more sensible, if the factory were able to create various loggers, and decides which type it will be. This can be done, for example, by reading and evaluating a configuration file, or a certain key is read out from the Windows Registry database. It is also imaginable that the type of the generated object is made dependent on the time of the day. The possibilities are endless. It is important that this should be completely transparent to the client class. Listing 9-38 shows a slightly more sophisticated LoggerFactory that reads a configuration file (e.g., from hard disk) and decides which specific Logger is created based on the current configuration.

Listing 9-38.  A More Sophisticated Factory That Reads and Evaluates a Configuration File

#include "LoggingFacility.h" #include "StandardOutputLogger.h" #include "FilesystemLogger.h"

#include <fstream>

#include <string>

#include <string_view>

class LoggerFactory { private:

enum class OutputTarget : int { STDOUT,

FILE

};

423

Chapter 9 Design Patterns and Idioms

public:

explicit LoggerFactory(std::string_view configurationFileName) : configurationFileName { configurationFileName } { }

Logger create() const {

const std::string configurationFileContent = readConfigurationFile(); OutputTarget outputTarget = evaluateConfiguration(configurationFileContent); return createLogger(outputTarget);

}

private:

std::string readConfigurationFile() const { std::ifstream filestream(configurationFileName);

return std::string(std::istreambuf_iterator<char>(filestream), std::istreambuf_iterator<char>()); }

OutputTarget evaluateConfiguration(std::string_view configurationFileContent) const {

// Evaluate the content of the configuration file...

return OutputTarget::STDOUT;

}

Logger createLogger(OutputTarget outputTarget) const { switch (outputTarget) {

case OutputTarget::FILE:

return std::make_shared<FilesystemLogger>(); case OutputTarget::STDOUT:

default:

return std::make_shared<StandardOutputLogger>();

}

}

const std::string configurationFileName;

};

The UML class diagram in Figure 9-12 depicts the structure that we basically know from the section about dependency injection (Figure 9-5), but now with our simple LoggerFactory instead of an assembler.

424