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

Chapter 3 Be Principled

add unnecessary, homemade complexity to this intrinsic complexity. Therefore, it is advisable not to use every fancy feature of your language or cool design patterns just because you can. On the other hand, do not overplay simplicity. If 10 decisions are necessary in a switch-case statement and there is no better, alternative solution, that’s just how it is.

Keep your code as simple as you can! Of course, if there are high prioritized quality requirements about flexibility and extensibility, you have to add complexity to fulfill these requirements. For instance, you can use the well-known strategy pattern (see Chapter 9 about design patterns) to introduce a flexible variation point into your code when requirements demand it. But be careful and add only the amount of complexity that makes such things easier.

“Focusing on simplicity is probably one of the most difficult things for a programmer to do. And it is a life long learning experience.”

—Adrian Bolboaca (@adibolb), April 3, 2014, on Twitter

YAGNI

“Always implement things when you actually need them, never when you just foresee that you need them.”

—Ron Jeffries, You’re NOT gonna need it! [Jeffries98]

This principle is tightly coupled to the previously discussed KISS principle. YAGNI is an acronym for “you aren’t gonna need it!” or is sometimes translated to “you ain’t gonna need it!”. YAGNI is the declaration of war against speculative generalization and over-­ engineering. It states that you should not write code that is not necessary at the moment, but might be in the future.

Probably every developer knows these kinds of tempting impulses in their daily work: “Maybe we could use it later…”, or “We’re going to need…” No, we aren’t gonna need it! We should under all circumstances avoid producing anything today for an uncertain and speculative future. In most cases, this code is simply not needed. But if we have implemented that unnecessary thing, we’ve wasted our precious time and the code gets more complicated than it should be! And of course, we also violate the previously discussed KISS principle. Even worse, these code pieces could be buggy and could cause serious problems!

43

Chapter 3 Be Principled

My advice is this: Trust in the power of refactoring and build things only when you know that they are actually necessary, not before.

DRY

“Copy and paste is a design error.”

—David L. Parnas

Although this principle is one of the most important, I’m quite sure that it is often violated, unintentionally or intentionally. DRY is an acronym for “don’t repeat yourself!” and states that we should avoid duplication, because duplication is evil. Sometimes this principle is also referred to as “once and only once” (OAOO).

The reason that duplication is very dangerous is obvious: when one piece is changed, its copies must be changed accordingly. And don’t have high hopes. It is a safe bet that change will occur. I think it’s unnecessary to mention that any copied piece will be forgotten sooner or later and we can say hello to bugs.

Okay, that’s it—nothing more to say? Wait, there is still something and we need to go deeper. In fact, I believe that the DRY principle is often misunderstood and also construed too pedantically by many developers! Thus, we should refresh our understanding of this principle.

It’s About Knowledge!

“Don’t Repeat Yourself (or DRY) is probably one of the most misunderstood parts of the book.”

—Dave Thomas, Orthogonality and the DRY Principle, 2003

In their brilliant book, The Pragmatic Programmer [Hunt99], Dave Thomas and Andy Hunt state that applying the DRY principle means that we have to ensure that “every piece of knowledge must have a single, unambiguous, authoritative representation within a system.” It is noticeable that Dave and Andy did not explicitly mention the code, but they talk about the knowledge.

First of all, a system’s knowledge is far broader than just its code. For instance, the DRY principle is also valid for business processes, requirements, database schemes,

44

Chapter 3 Be Principled

documentation, project plans, test plans, or the system’s configuration data. DRY affects everything! Perhaps you can imagine that strict compliance with this principle is not as easy as it might seem at first sight.

Building Abstractions Is Sometimes Hard

Moreover, an exaggerated application of the DRY principle at all costs in a code base can lead to some fiddly problems. The reason is that creating an adequate common abstraction from duplicated code pieces can quickly become a tricky task, sometimes deteriorating the readability and comprehensibility of the code.

The annoyance becomes really big if there are requirement changes or functional enhancements that affect only one locus of usage of a multiple used abstraction, as the following example demonstrates.

Let’s look at the following two (simplified) classes (Listings 3-1 and 3-2) from software for an online mail order business.

Listing 3-1.  The Class for the Shopping Cart

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

#include <vector>

class ShoppingCart { public:

void addProduct(const Product& product) { goods.push_back(product);

}

void removeProduct(const Product& product) { std::erase(goods, product);

}

private: std::vector<Product> goods;

};

45

Chapter 3 Be Principled

Listing 3-2.  The Class Used to Ship the Ordered Products

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

#include <vector>

class Shipment { public:

void addProduct(const Product& product) { goods.push_back(product);

}

void removeProduct(const Product& product) { std::erase(goods, product);

}

private: std::vector<Product> goods;

};

I’m pretty sure you would agree that these two classes are duplicated code and that they therefore violate the DRY principle. The only difference is the class name; all other lines of code are identical.

THE ERASE-REMOVE IDIOM (UNTIL C++20)

Before C++20, if developers wanted to eliminate elements from a container, such as a std::vector, they often applied the so-called Erase-Remove idiom on that container.

In this idiom, two steps were successively applied to the container. First, the algorithm std::remove was used to move those elements that did not match the removal criteria, to the front of the container. The name of this function is misleading, as no elements are actually removed by std::remove, but are shifted to the front of the container.

After that, std::remove returns an iterator pointing to the first element of the tail elements in the container. This iterator, as well as the container’s end iterator, have then been passed to the std::vector::erase member function of the container to physically remove the tail elements. Applied to an arbitrary vector named vec, it looked like this:

46

Chapter 3 Be Principled

// Removing all elements that match 'value' from a vector before C++20: vec.erase(std::remove(begin(vec), end(vec), value), end(vec));

Since C++20, the Remove-Erase idiom is no longer necessary for this purpose. Instead, the two template functions std::erase and std::erase_if, both defined in header

<vector>, can do the job. These functions not only physically delete the elements that match the deletion criteria, but can also be used easier because it is not necessary anymore to pass two iterators. Instead, the entire container can be passed, like this:

// Removing all elements that match 'value' from a vector since C++20: std::erase(vec, value);

A suitable solution to get rid of the duplicated code seems to be to refactor the code and create a common abstraction, for instance by using inheritance, as shown in Listing 3-3.

Listing 3-3.  The Base Class ProductContainer, from which ShoppingCart and Shipment Is Derived

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

#include <vector>

class ProductContainer { public:

void addProduct(const Product& product) { products.push_back(product);

}

void removeProduct(const Product& product) { std::erase(goods, product);

}

private:

std::vector<Product> products;

};

class ShoppingCart : public ProductContainer { }; class Shipment : public ProductContainer { };

47