[Перевод] Объектно-ориентированный антипаттерн
Для тех, кто читает не далее второго абзаца: я не говорю, что любое ООП — это плохо! ООП, особенно классическое полиморфное ООП, заслуженно имеет своё место в реальных проектах. Ниже речь поидёт про частный случай (анти)паттерна, который я периодически встречаю: использование классов там, где могли быть простые свободные функции.
Начало
Довольно часто у студентов, изучающих C++ в определённых учебных кругах, складывается мировоззрение о том, что всё должно быть объектами. Попросите их написать программу, которая считает некоторое значение — и они начнут с создания объекта ValueComputer
и метода vc.computeResult()
.
Например: дана задача с помощью динамического программирования посчитать количество способов замостить костяшками домино прямоугольник . Студент пишет:
int main()
{
DominoTilingCounter tc(4, 7); // in a 4x7 rectangle
std::cout << tc.count() << '\n';
}
Обозначив задачу, студент далее приступает к написанию класса DominoTilingCounter
. А если он смышлёный, то также добавит мемоизацию, чтобы метод count()
не считал каждый раз всё заново:
class DominoTilingCounter {
int height, width;
bool done = false;
int tilingCount;
int computeCount(int h, int w, std::string_view prevRow, int rowIdx) {
[...recursive solution omitted...]
}
public:
explicit DominoTilingCounter(int h, int w) : height(h), width(w) {
if (h == 0 || w == 0 || (w*h) % 2 != 0) {
tilingCount = 0;
done = true;
}
}
int count() {
if (!done) {
tilingCount = computeCount(height, width, "", 0);
done = true;
}
return tilingCount;
}
};
(Если вам от этого кода захотелось фыркнуть — хорошо. Так и задумано.)
Здесь можно было бы придраться к тому, что метод count()
по смыслу должен быть константным, но его таковым сделать нельзя, потому что он меняет внутренние поля класса. Давайте это исправим.
Продолжение, или рассуждаем логически
Смотрите: ты конструируешь объект DominoTilingCounter tc
, только чтобы вычислить tc.count()
, правильно? И больше объект ни для чего не используется?
Ещё раз: ты конструируешь объект DominoTilingCounter tc
, только чтобы вычислить tc.count()
.
Так перенеси вычисление в конструктор!
class DominoTilingCounter {
int height, width;
int tilingCount;
int computeCount(int h, int w, std::string_view prevRow, int rowIdx) {
[...recursive solution omitted...]
}
public:
explicit DominoTilingCounter(int h, int w) : height(h), width(w) {
if (h == 0 || w == 0 || (w*h) % 2 != 0) {
tilingCount = 0;
} else {
tilingCount = computeCount(height, width, "", 0);
}
}
int count() const {
return tilingCount;
}
};
Вот что изменилось после рефакторинга:
метод
count()
стал константным,объект больше не может находиться в «невычисленном» состоянии,
убрали поле
done
, которое использовалось, только чтобы отличать это «невычисленное» состояние (кстати, в C++17 можно было использоватьstd::optional
для тех же целей, но нам теперь и не надо).
Более того, поля width
и height
тоже больше не нужны. Они использовались, чтобы передать данные из конструктора в метод count()
, а теперь все вычисления происходят в конструкторе, ничего никуда передавать не нужно. Код значительно сократился.
Конец
Так получилось, что с самого начала метод computeCount()
не обращался к полям height
и width
класса, а получал данные через параметры w
и h
. По счастливому случаю метод computeCount()
не обращается вообще ни к каким полям класса, значит, его можно пометить как static
. Теперь наш код выглядит как-то так:
class DominoTilingCounter {
int tilingCount;
static int computeCount(int h, int w, std::string_view prevRow, int rowIdx) {
if (h == 0 || w == 0 || (w*h) % 2 != 0) {
return 0;
}
[...recursive solution omitted...]
}
public:
explicit DominoTilingCounter(int h, int w) {
tilingCount = computeCount(h, w, "", 0);
}
int count() const {
return tilingCount;
}
};
Заметим, что, по сути, теперь класс просто оборачивает int
!
Избегайте классов, по своей сути равнозначных
int
.
(А что насчёт float
или double
? — прим. перев.)
Что нам нужно было написать, это простую свободную функцию countDominoTilings(h, w)
:
int countDominoTilingsImpl(int h, int w, std::string_view prevRow, int rowIdx) {
if (h == 0 || w == 0 || (w*h) % 2 != 0) {
return 0;
}
[...recursive solution omitted...]
}
int countDominoTilings(int h, int w) {
return countDominoTilingsImpl(h, w, "", 0);
}
int main() {
int tc = countDominoTilings(4, 7); // in a 4x7 rectangle
std::cout << tc << '\n';
}
Никаких классов. Больше не нужно беспокоиться о константности методов. Можно забыть про мемоизацию (теперь это забота вызывающего кода). Первоначальный вариант с классом был потоко-небезопасен, и об этом тоже можно больше не переживать. А ещё наш код стал на десяток строк короче.
На всякий случай, ещё раз: не все классы — это плохо! Но если вам нужно что-то посчитать, не пишите класс ВычисляторЗначения
. Напишите функцию вычислить_значение()
.