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

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

#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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值