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

Chapter 3 Be Principled

Alternative solutions would be to use C++ templates, or to use composition instead of inheritance, i.e., ShoppingCart and Shipment use ProductContainer for their implementation (see the section entitled “Favor Composition over Inheritance” in Chapter 6).

So, the code for the shopping cart and for the shipment of goods has been identical, and we have removed the duplication now … but wait! Maybe we should stop and ask ourselves the question: Why was the code identical?!

From the perspective of the business stakeholders, there may be very good reasons for making a very clear distinction between the two domain-specific concepts of a shopping basket and the product shipment. It is therefore highly recommended to ask the business people what they think of our idea to map the shopping basket and product shipping to the same piece of code. They might say, “ Well, yes, on first sight a nice idea, but remember that customers can order certain products by any number, but for safety reasons we have to make sure that we never ship more than a certain number of these products with the same delivery.”

By sharing the same code for two (or more) different domain concepts, we have coupled them very closely together. Often there are additional requirements to fulfill, which only affect one of both usages. In such a case, exceptions and special case handlings must be implemented for the several uses of the ProductContainer class. This can become a very tedious task, the readability of the code can suffer, and the initial advantage of the shared abstraction is quickly lost.

The conclusion is this: Reusing code is not basically a bad thing. But overzealous de-duplication of code creates the risk that we reuse code that only “accidentally” or “superficially” behaves the same, but that in fact has different meanings in the different places it is used. Mapping different domain concepts to the same piece of code is dangerous, because there are different reasons that this code needs to be changed.

The DRY principle is only marginally about code. In fact, it’s about knowledge.

Information Hiding

Information hiding is a long-known and fundamental principle in software development. It was first documented in the seminal paper “On the Criteria to Be Used in Decomposing Systems Into Modules,” [Parnas72] written by David L. Parnas in 1972.

48

Chapter 3 Be Principled

The principle states that one piece of code that calls another piece of code should not “know” the internals about that other piece of code. This makes it possible to change internal parts of the called piece of code without being forced to change the calling piece of code accordingly.

David L. Parnas describes information hiding as the basic principle for decomposing systems into modules. Parnas argued that system modularization should concern the hiding of difficult design decisions or design decisions that are likely to change. The fewer internals a software unit (e.g., a class or component) exposes to its environment, the lesser is the coupling between the implementation of the unit and its clients. As a result, changes in the internal implementation of a software unit will not be propagated to its environment.

There are numerous advantages of information hiding:

•\

Limitation of the consequences of changes in modules

•\

Minimal influence on other modules if a bug fix is necessary

•\

Significantly increasing the reusability of modules

•\

Better testability of modules

Information hiding is often confused with encapsulation, but it’s not the same. I know that both terms have been used in many noted books synonymously, but I don’t agree. Information hiding is a design principle for aiding developers in finding good modules. The principle works at multiple levels of abstraction and unfolds its positive effect, especially in large systems.

Encapsulation is often a programming-language dependent technique for restricting access to the innards of a module. For instance, in C++ you can precede a list of class members with the private keyword to ensure that they cannot be accessed from outside the class. But just because we use these guards for access control, we are still far away from getting information hiding automatically. Encapsulation facilitates, but does not guarantee, information hiding.

The code example in Listing 3-4 shows an encapsulated class with poor information hiding.

49

Chapter 3 Be Principled

Listing 3-4.  A Class for Automatic Door Steering (Excerpt)

class AutomaticDoor { public:

enum class State { closed = 1, opening,

open, closing

};

private:

State state;

// ...more attributes here...

public:

State getState() const;

// ...more member functions here...

};

This is not information hiding, because parts of the internal implementation of the class are exposed to the environment, even if the class looks well encapsulated. Note the type of the return value of getState. The enumeration class State is required by clients using this class, as Listing 3-5 demonstrates.

Listing 3-5.  An Example of How AutomaticDoor Must Be Used to Query the Door’s Current State

#include "AutomaticDoor.h"

int main() {

AutomaticDoor automaticDoor;

AutomaticDoor::State doorsState = automaticDoor.getState(); if (doorsState == AutomaticDoor::State::closed) {

// do something...

}

return 0;

}

50

Chapter 3 Be Principled

ENUMERATION CLASS (STRUCT) [C++11]

With C++11 there has also been an innovation on enumerations types. For downward compatibility to earlier C++ standards, there is still the well-known enumeration with its keyword enum. Since C++11, there are also the enumeration classes.

One problem with those old C++ enumerations is that they export their enumeration literals to the surrounding namespace, causing name clashes, such as in the following example:

const std::string bear;

// ...and elsewhere in the same namespace...

enum Animal { dog, deer, cat, bird, bear }; // error: 'bear' redeclared as different kind of symbol

Furthermore, old C++ enums implicitly convert to int, causing subtle errors when such a conversion is not expected or wanted:

enum Animal { dog, deer, cat, bird, bear }; Animal animal = dog;

int aNumber = animal; // Implicit conversion: works

These problems no longer exist when using enumeration classes, also called “new enums” or “strong enums.” Their enumeration literals are local to the enumeration, and their values do not implicitly convert to other types (like to another enumeration or an int).

const std::string bear;

// ...and elsewhere in the same namespace...

enum class Animal { dog, deer, cat, bird, bear }; // No conflict with the string named 'bear'

Animal animal = Animal::dog;

int aNumber = animal; // Compiler error!

It is strongly recommended to use enumeration classes instead of plain old enums for a modern C++ program, because it makes the code safer. And because enumeration classes are also classes, they can be forward declared.

What will happen if the internal implementation of AutomaticDoor must be changed and the enumeration class State is removed from the class? It is easy to see that this will have a significant impact on the client’s code. It will result in changes everywhere that member function AutomaticDoor::getState() is used.

51

Chapter 3 Be Principled

Listings 3-6 and 3-7 show an encapsulated AutomaticDoor with good information hiding.

Listing 3-6.  A Better Designed Class for Automatic Door Steering

class AutomaticDoor { public:

bool isClosed() const; bool isOpening() const; bool isOpen() const; bool isClosing() const;

// ...more operations here...

private:

enum class State { closed = 1, opening,

open, closing

};

State state;

// ...more attributes here...

};

Listing 3-7.  An Example of How Elegant Class AutomaticDoor Can Be Used After it Was Changed

#include "AutomaticDoor.h"

int main() {

AutomaticDoor automaticDoor; if (automaticDoor.isClosed()) {

// do something...

}

return 0;

}

52