Пример 5. Декомпозиция проекта при работе с множествами объектов
Исходная задача
Предположим, что у нас есть класс Rectangle
, в котором описаны некоторые его свойства и методы взаимодействия с ними:
#include <iostream>
class Rectangle {
private:
int a_, b_;
public:
Rectangle() {}
Rectangle(int a, int b) {
this->a_ = a;
this->b_ = b;
}
int getA() {
return a_;
}
int getB() {
return b_;
}
void setA(int a) {
this->a_ = a;
}
void setB(int b) {
this->b_ = b;
}
void set(int a, int b) {
setA(a);
setB(b);
}
};
int main() {
Rectangle *rec = new Rectangle(1, 3);
std::cout << "I know about the rectangle. It has: "
<< std::endl;
std::cout << " a=" << rec->getA() << std::endl;
std::cout << " b=" << rec->getB() << std::endl;
delete rec;
}
Обратите внимание, что названия private
переменных имеют постфикс _
, чтобы отличать их от общедоступных. Также названий переменных и методов используется верблюжий (CamelCase
) стиль.
О лучших стилистических практиках рассказано здесь.
Предположим, что нам поставили следующую задачу: обрабатывать в программе множество объектов класса Rectangle
.
Подход 1: хранение множества объектов в месте его использования
Объявление множества в функции main
, а его обработчиков - в main.cpp
Первое предположение о решении задачи, которое может прийти на ум, может быть желанием хранить нужное нам множество там, где мы с ним будем работать. В нашем случае делать это в функции main
. В том же файле, где содержится функция main
(main.cpp
), расположить функции работы с нашим множеством. Итоговый результат будет таким:
#include <iostream>
class Rectangle {
private:
int a_, b_;
public:
Rectangle() {}
Rectangle(int a, int b) {
this->a_ = a;
this->b_ = b;
}
int getA() {
return a_;
}
int getB() {
return b_;
}
void setA(int a) {
this->a_ = a;
}
void setB(int b) {
this->b_ = b;
}
void set(int a, int b) {
setA(a);
setB(b);
}
};
void showRectangles(Rectangle *arr, int n) {
for (int i = 0; i < n; i++) {
std::cout << "I know about the rectangle # " << i
<< ". It has: " << std::endl;
std::cout << " a=" << arr[i].getA() << std::endl;
std::cout << " b=" << arr[i].getB() << std::endl;
}
}
int calculateAreas(Rectangle *arr, int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i].getA() * arr[i].getB();
}
return sum;
}
int main() {
int n = 2;
Rectangle *arr = new Rectangle[n];
arr[0].set(1, 3);
arr[1].set(4, 6);
showRectangles(arr, n);
std::cout << std::endl;
std::cout << "Sum of the areas of all rectangles: "
<< calculateAreas(arr, n) << std::endl;
delete[] arr;
}
Функция showRectangles
у нас показывает информацию о всех прямоугольниках в множества, calculateAreas
- суммирует площади прямоугольников из множества.
А что делать, если в нашей программе мы будем работать не только с объектами класса Rectangle
, но и с Circle
, Triangle
, Square
? Тогда в обработчики множеств объектов соответствующих классов нам также придется размещать в main.cpp
. Чем это грозит?
-
Нам придется придумывать длинные названия функций, которые выполняют одни и те же действия, но для разных объектов. Например, для расчеты суммы площадей множеств нам придется использовать названия
calculateAreasRectangle
,calculateAreasTriangle
и т.д. Но в C++ имеется механизм перегрузки функций, который позволит нам использовать имя функцииcalculateAreas
для разных множеств.Перегрузка функций — это механизм С++, благодаря которому функции с разным количеством или типами параметров могут иметь одинаковое имя (идентификатор).
-
Даже если мы применили механизм перегрузки функций, мы сталкиваемся с ростом объема
main.cpp
. В лабораторной работе №1 мы познакомились о том, что для декомпозиции проекта можно использовать механизм статических библиотек классов. Значит мы можем унести наши обр аботчики множеств на уровень статических библиотек и разместить их по соседству с объявлением соответствующих классов или в самих классах.
Объявление множества в функции main
, а его обработчики сделать методами объектов класса Rectangle
Остановимся на том, что функции работы с множествами мы будем размещать в самих классах. Для примера мы продолжим использовать файл main.cpp
. Для класса Rectangle
результат наших преобразований будет выглядеть следующим образом:
#include <iostream>
class Rectangle {
private:
int a_, b_;
public:
Rectangle() {}
Rectangle(int a, int b) {
this->a_ = a;
this->b_ = b;
}
int getA() {
return a_;
}
int getB() {
return b_;
}
void setA(int a) {
this->a_ = a;
}
void setB(int b) {
this->b_ = b;
}
void set(int a, int b) {
setA(a);
setB(b);
}
void showRectangles(Rectangle *arr, int n) {
for (int i = 0; i < n; i++) {
std::cout << "I know about the rectangle # " << i
<< ". It has: " << std::endl;
std::cout << " a=" << arr[i].getA() << std::endl;
std::cout << " b=" << arr[i].getB() << std::endl;
}
}
int calculateAreas(Rectangle *arr, int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i].getA() * arr[i].getB();
}
return sum;
}
};
int main() {
int n = 2;
Rectangle *arr = new Rectangle[n];
arr[0].set(1, 3);
arr[1].set(4, 6);
arr[0].showRectangles(arr, n);
std::cout << std::endl;
std::cout << "Sum of the areas of all rectangles: "
<< arr[0].calculateAreas(arr, n) << std::endl;
delete[] arr;
}
В этой реализации мы сталкиваемся с несколькими проблемами:
- Первая проблема данных методов, о которой мы можем увидеть, это то, что вызов методов работы с массивами происходит через конкретный объект. Иными словами, нам для вызова метода работы с массивом нужно обратиться к какому-то объекту из этого массива и вызвать у него требуемый метод.
- Вторая вытекающая проблема заключается в том, что методы работы с массивами зачем-то имеют доступу к конкретному экземпляра класса, через который вызываются данные методы. А это может повлечь проблему безопасной работы с конкретным объектом в случае внесения каких-то изменений по неосторожности. Пример, где вы можете столкнуться с проблемами безопасного управления данными: сортировка множества.
Решить эти проблемы можно достаточно просто: сделать эти методы статическими методами класса!
Демонстрация использования статических методов представлена в примере 3.