简单的数据结构,避免类的手动伪封装

保持简单的数据结构简单!当您只有一堆数据时,没有必要进行人工伪封装。

#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;
}

如果我们查看 gettersetter,我们会发现它们只是一堆样板文件。关于面向对象程序设计的书常常长篇大论地谈论封装。它们鼓励我们为每个数据成员使用 getter setter
但是,封装意味着应该保护某些数据不受自由访问的影响。通常,这是因为有一些逻辑将一些数据绑定在一起。在这种情况下,访问函数执行检查,并且某些数据可能只能一起更改。
但是 C++并不是一种纯粹的面向对象语言。在某些情况下,我们的结构仅仅是一组简单的数据,仅此而已。最好不要在伪类后面隐藏这个事实,而是通过使用带有公共数据成员的结构来使其显而易见。结果是一样的: 每个人都可以无限制地访问任何东西。

有时候,像这样的类似乎只是普通的数据容器,逻辑隐藏在其他地方。在域对象的情况下,这称为贫血域模型,通常被认为是反模式。通常的解决方案是重构代码,将逻辑移动到要与数据共存的类中。
无论我们这样做还是将逻辑与数据分离,这都应该是一个有意识的决定。如果我们决定把数据和逻辑分开,我们可能应该把这个决定写下来。在这种情况下,我们回到了之前的结论: 不使用类,而是使用带有公共数据的结构。
即使我们决定将逻辑移动到类中,也存在实际封装在类外部提供的罕见情况。一个例子是“ pimpl 惯用语”中的细节类; 除了包含类和pimpl本身之外,没有人能够访问它们,所以添加所有这些gettersetter 没有意义。
通常需要构造函数来创建处于一致状态的对象并建立不变量。在普通数据结构的情况下,不存在可以维护的不变量和一致性。上面示例中的构造函数只需要不必默认构造一个对象,然后立即通过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
};

参考

[1] simple-data-structures
[2] C++ Core Guidelines C.2

猜你喜欢

转载自blog.csdn.net/MMTS_yang/article/details/127102237