保持简单的数据结构简单!当您只有一堆数据时,没有必要进行人工伪封装。
#include <iostream>
using namespace std;
class Unit {
public:
Unit(std::string name_, unsigned points_, int x_, int y_)
: name{name_}, points{points_}, x{x_}, y{y_} {}
Unit(std::string name_) : name{name_}, points{0}, x{0}, y{0} {}
Unit() : name{""}, points{0}, x{0}, y{0} {}
void setName(std::string const& n) { name = n; }
std::string const& getName() const { return name; }
void setPoints(unsigned p) { points = p; }
unsigned getPoints() const { return points; }
void setX(int x_) { x = x_; }
int getX() const { return x; }
void setY(int y_) { y = y_; }
int getY() const { return x; }
private:
std::string name;
unsigned points;
int x;
int y;
};
int main(){
return 0;
}
如果我们查看 getter
和 setter
,我们会发现它们只是一堆样板文件。关于面向对象程序设计的书常常长篇大论地谈论封装。它们鼓励我们为每个数据成员使用 getter
和 setter
。
但是,封装意味着应该保护某些数据不受自由访问的影响。通常,这是因为有一些逻辑将一些数据绑定在一起。在这种情况下,访问函数执行检查,并且某些数据可能只能一起更改。
但是 C++
并不是一种纯粹的面向对象语言。在某些情况下,我们的结构仅仅是一组简单的数据,仅此而已。最好不要在伪类后面隐藏这个事实,而是通过使用带有公共数据成员的结构来使其显而易见。结果是一样的: 每个人都可以无限制地访问任何东西。
有时候,像这样的类似乎只是普通的数据容器,逻辑隐藏在其他地方。在域对象的情况下,这称为贫血域模型,通常被认为是反模式。通常的解决方案是重构代码,将逻辑移动到要与数据共存的类中。
无论我们这样做还是将逻辑与数据分离,这都应该是一个有意识的决定。如果我们决定把数据和逻辑分开,我们可能应该把这个决定写下来。在这种情况下,我们回到了之前的结论: 不使用类,而是使用带有公共数据的结构。
即使我们决定将逻辑移动到类中,也存在实际封装在类外部提供的罕见情况。一个例子是“ pimpl
惯用语”中的细节类; 除了包含类和pimpl
本身之外,没有人能够访问它们,所以添加所有这些getter
和 setter
没有意义。
通常需要构造函数来创建处于一致状态的对象并建立不变量。在普通数据结构的情况下,不存在可以维护的不变量和一致性。上面示例中的构造函数只需要不必默认构造一个对象,然后立即通过setter
设置每个成员。
如果仔细观察,甚至可能会发现其中存在 bug
:任何std::string
都可以隐式转换为 Unit
,因为单个参数构造函数不是显式的。这样的事情可以带来很多调试乐趣和令人头疼的问题。
从 C++11
开始,我们就有了类内初始化器的特性。在这种情况下,可以使用它们来代替构造函数。上面的所有构造函数都包含在这种方法中。这样,示例中的53行代码可以归结为6行:
struct Unit {
std::string name{ "" };
unsigned points{ 0 };
int x{ 0 };
int y{ 0 };
};
如果你使用统一初始化,初始化看起来就像以前一样:
Unit a{"Alice"};
Unit b{"Bob", 43, 1, 2};
Unit c;
名称可能不应该是空字符串或包含特殊字符。这是不是意味着我们必须把它全部扔掉,然后重新把这个单元变成一个合适的班级呢?也许不会。我们经常在一个地方使用逻辑来验证和清除字符串和类似的东西。进入程序或库的数据必须通过这个点,然后我们假设数据是有效的。
如果这太接近贫血领域模型,我们仍然不必再次封装 Unit 类中的所有内容。相反,我们可以使用包含逻辑的自定义类型来代替 std::string
。毕竟,std::string
是一组任意的字符。如果我们需要不同的东西,std::string
可能很方便,但它是错误的选择。我们的自定义类型可能有一个合适的构造函数,所以它不能默认构造为空字符串。
如果我们再看一下这个类,我们可以假设 x
和 y
是某种坐标。它们可能属于一起,所以我们不应该有一个方法将两者集合在一起吗?也许构造函数是有意义的,因为它们允许同时设置两个或者不设置?
不,这不是解决办法。它可能会纠正一些症状,但我们仍然会有“数据块”代码的味道。这两个变量属于一起,因此它们应该有自己的结构或类。
struct Unit {
PlayerName name;
unsigned points{ 0 };
Point location{ {0,0} };
};
如果类有不变量,则使用class
;如果数据成员可以独立变化,则使用struct
。
示例1:
struct Pair { // 成员可以独立变化
string name;
int volume;
};
示例2:
class Date {
public:
// 验证{yy, mm, dd}是有效的日期并初始化
Date(int yy, Month mm, char dd);
// ...
private:
int y;
Month m;
char d; // day
};