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

Chapter 9 Design Patterns and Idioms

In practice, dependency injection frameworks are available as commercial and open source solutions.

Adapter

I’m sure the Adapter (Wrapper) is one of the most commonly used design patterns. The reason for this is that the adaptation of incompatible interfaces is certainly a case that’s often necessary in software development, such as when a module developed by another team has to be integrated, or when using third-party libraries.

Here is the mission statement of the Adapter pattern:

“Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.”

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

Let’s further develop the example from the previous section about dependency injection. Let’s assume that we want to use BoostLog v2 (see www.boost.org) for logging purposes, but we want to keep a usage of this third-party library exchangeable with other logging approaches and technologies.

The solution is simple: we just have to provide another implementation of the LoggingFacility interface, which adapts the interface of BoostLog to the interface that we want, as depicted in Figure 9-6.

Figure 9-6.  An adapter for a boost logging solution

394

Chapter 9 Design Patterns and Idioms

In source code, the additional implementation of the LoggingFacility interface

BoostTrivialLogAdapter is shown in Listing 9-13.

Listing 9-13.  The Adapter for BoostLog Is Just Another Implementation of LoggingFacility

#include "LoggingFacility.h" #include <boost/log/trivial.hpp>

class BoostTrivialLogAdapter : public LoggingFacility { public:

void writeInfoEntry(std::string_view entry) override { BOOST_LOG_TRIVIAL(info) << entry;

}

void writeWarnEntry(std::string_view entry) override { BOOST_LOG_TRIVIAL(warn) << entry;

}

void writeErrorEntry(std::string_view entry) override { BOOST_LOG_TRIVIAL(error) << entry;

}

};

The advantages are obvious: through the Adapter pattern, there is now exactly one class in the entire software system that has a dependency to the third-party logging solution. This also means that the code is not contaminated with proprietary logging statements, like BOOST_LOG_TRIVIAL(). And because this Adapter class is just another implementation of the LoggingFacility interface, we can also use dependency injection (see the previous section) to inject instances—or just exactly the same instance—of this class into all client objects that want to use it.

Adapters can facilitate a broad range of adaptation and conversion possibilities for incompatible interfaces. This ranges from simple adaptations, such as operations names and data type conversions, right up to supporting an entirely different set of operations. In our case, a call of a member function with a string parameter is converted into a call of the insertion operator for streams.

395

Chapter 9 Design Patterns and Idioms

Interface adaptations are of course easier if the interfaces to be adapted are similar. If the interfaces are very different, an adapter can also become a very complex piece of code.

Strategy

If you remember the open-closed principle (OCP) described in Chapter 6 as a guideline for an extensible object-oriented design, the Strategy design pattern can be considered as the “celebrity gig” of this important principle. Here is the mission statement of this pattern:

“Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.”

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

Doing tings in different ways is a common requirement in software design. Just think of sorting algorithms for lists. There are various sorting algorithms that have different characteristics regarding the time complexity (number of operations required) and the space complexity (additional required storage space in addition to the input list). Examples are Bubble-Sort, Quick-Sort, Merge-Sort, Insert-Sort, and Heap-Sort.

For instance, Bubble-Sort is the least complex one and it is very efficient regarding memory consumption, but also one of the slowest sorting algorithms. In contrast, Quick-Sort is a fast and efficient sorting algorithm that is easy to implement through its recursive structure and does not require additional memory, but it is very inefficient with presorted and inverted lists. With the help of the Strategy pattern, a simple exchange of the sorting algorithm can be implemented, for example, depending on the properties of the list to be sorted.

Let’s consider another example. Assume that we want to have a textual representation of an instance of a Customer class in an arbitrary business IT system. A stakeholder requirement states that the textual representation should be formatted in various output formats: as plain text, as XML (Extensible Markup Language), and as JSON (JavaScript Object Notation).

First of all, let’s introduce an abstraction for our various formatting strategies, the abstract class Formatter. See Listing 9-14.

396

Chapter 9 Design Patterns and Idioms

Listing 9-14.  The Abstract Formatter Class Contains Everything That All Specific Formatter Classes Have in Common

#include <memory> #include <string>

#include <string_view>

class Formatter { public:

virtual ~Formatter() = default;

Formatter& withCustomerId(std::string_view customerId) { this->customerId = customerId;

return *this;

}

Formatter& withForename(std::string_view forename) { this->forename = forename;

return *this;

}

Formatter& withSurname(std::string_view surname) { this->surname = surname;

return *this;

}

Formatter& withStreet(std::string_view street) { this->street = street;

return *this;

}

Formatter& withZipCode(std::string_view zipCode) { this->zipCode = zipCode;

return *this;

}

Formatter& withCity(std::string_view city) { this->city = city;

return *this;

}

397

Chapter 9 Design Patterns and Idioms

virtual std::string format() const = 0; protected:

std::string customerId { "000000" }; std::string forename { "n/a" }; std::string surname { "n/a" }; std::string street { "n/a" }; std::string zipCode { "n/a" }; std::string city { "n/a" };

};

using FormatterPtr = std::unique_ptr<Formatter>;

The three specific formatters that provide the formatting styles that are requested by the stakeholders are shown in Listing 9-15.

Listing 9-15.  The Three Specific Formatters Override the Pure Virtual format() Member Function of Formatter

#include "Formatter.h" #include <sstream>

class PlainTextFormatter : public Formatter { public:

std::string format() const override { std::stringstream formattedString { }; formattedString << "[" << customerId << "]: "

<<forename << " " << surname << ", "

<<street << ", " << zipCode << " "

<<city << ".";

return formattedString.str();

}

};

class XmlFormatter : public Formatter { public:

std::string format() const override { std::stringstream formattedString { }; formattedString <<

"<customer id=\"" << customerId << "\">\n" <<

398

Chapter 9 Design Patterns and Idioms

"<forename>" << forename << "</forename>\n" <<

"<surname>" << surname << "</surname>\n" <<

"<street>" << street << "</street>\n" <<

"<zipcode>" << zipCode << "</zipcode>\n" <<

"<city>" << city << "</city>\n" << "</customer>\n";

return formattedString.str();

}

};

class JsonFormatter : public Formatter { public:

std::string format() const override { std::stringstream formattedString { }; formattedString <<

"{\n" <<

"\"CustomerId : \"" << customerId << END_OF_PROPERTY <<

"\"Forename: \"" << forename << END_OF_PROPERTY <<

"\"Surname: \"" << surname << END_OF_PROPERTY <<

"\"Street: \"" << street << END_OF_PROPERTY <<

"\"ZIP code: \"" << zipCode << END_OF_PROPERTY <<

"\"City: \"" << city << "\"\n" <<

"}\n";

return formattedString.str();

}

private:

static constexpr const char* const END_OF_PROPERTY { "\",\n" };

};

As can be seen clearly here, the OCP is particularly well supported. As soon as a new output format is required, another specialization of the abstract class Formatter has to be implemented. Modifications to the already existing formatters are not required. See Listing 9-16.

399

Chapter 9 Design Patterns and Idioms

Listing 9-16.  How the Passed-In Formatter Object Is Used Inside the Member Function getAsFormattedString()

#include "Address.h" #include "CustomerId.h" #include "Formatter.h"

class Customer { public:

// ...

std::string getAsFormattedString(Formatter& formatter) const { return formatter.

withCustomerId(customerId.toString()). withForename(forename). withSurname(surname). withStreet(address.getStreet()). withZipCode(address.getZipCodeAsString()). withCity(address.getCity()).

format();

}

// ...

private:

CustomerId customerId; std::string forename; std::string surname; Address address;

};

The Customer::getAsFormattedString() member function has a parameter that expects a non-const reference to a formatter object. This parameter can be used to control the format of the string that can be retrieved through this member function, or in other words, the member function Customer::getAsFormattedString() can be supplied with a formatting strategy.

Perhaps you’ve noticed the special design of the public interface of the Formatter with its numerous chained with...() member functions. Here also another design pattern has been used, which is called Fluent Interface. In object-oriented programming,

400

Chapter 9 Design Patterns and Idioms

a fluent interface is a style to design APIs in a way that the readability of the code is close to that of ordinary written prose. In Chapter 8, we saw such an interface. That chapter introduced a custom assertion (see the section entitled “More Sophisticated Tests with a Custom Assertion”) to write more elegant and better readable tests. In this case here, the trick is that every with...() member function is self-referential, that is, the new context for calling a member function on the formatter is equivalent to the previous context, unless when the final format() function is called.

A graphical visualization of the class structure of our code example (a UML class diagram) is shown in Figure 9-7.

Figure 9-7.  An abstract formatting strategy and its three concrete formatting strategies

As you can see, the Strategy pattern in this example ensures that the caller of the Cust omer::getAsFormattedString() member function can configure the output format as it wants. You want to support another output format? No problem: thanks to the excellent support of the open-closed principle, another concrete formatting strategy can be easily added. The other formatting strategies, as well as the Customer class, remain completely unaffected by this extension.

401