是否应隐藏所有数据成员

Should all data member be hidden?

本文关键字:数据成员 隐藏所 是否      更新时间:2023-10-16

我知道OOP的"数据隐藏"概念,但在实际开发中,当规范发生变化时,它总是受到挑战。

例如:

class role
{
std::string name;
int level;
public:
const std::string& get_name() { return name; }
void set_name(const std::string& value) { name = value; }
void set_level(int value) { level = value; }
int get_level() const { return level; }
}

当然,这个代码没有什么问题。但是namelevel根本没有封装,我想。

我的意见如下:

  1. 用户可以通过setter函数修改数据
  2. setter/getter公开这些数据成员的数据类型。如果这些数据成员必须更改其类型,那么setter/getter函数成员也必须更改其接口
  3. 如果我需要对level成员进行另一个操作,那么添加更多的操作成员(例如add_level(int value)sub_level(int value)等)函数是唯一的方法
  4. 只有一件事是好的。如果get/set必须添加一些判断,这些接口可以很好地工作

那么,我应该封装什么样的数据成员?我根本无法预测那些数据成员的规模和使用情况。如果我直接公开它们,对它们的操作将非常简单、清晰且有意义。如果我封装它们,我会为它们创建很多操作成员,如果有一天它们的类型被spec(int->class,class->int)更改,我的操作成员必须更改它们的接口或直接杀死它们(因为我会一次性将它们发送到公共区域!)。

我一直在阅读许多不同的语言,尤其是函数式语言,我慢慢地开始质疑setters的想法。

假设我有一个Person类,这是我几年前会写的:

class Person {
public:
std::string const& name() const { return _name; }
void name(std::string const& n) { _name = n; }
unsigned age() const { return _age; }
void age(unsigned a) { _age = a; }
private:
std::string _name;
unsigned _age;
};

你会注意到它和你自己的课有多么相似。

  • Person的名称在对象的生命周期内会更改吗
  • Person的年龄在对象的生命周期内会发生变化吗?我们能做些什么来避免这种情况
  • 关于_name的类型,我已经把自己束缚住了:改变它会破坏name()getter

现在,一个替代实现,也就是我今天要写的:

class Person {
public:
Person(std::string name, Time birth): _name(name), _birth(birth) {}
std::string name() const { return _name; }
Duration age(Time now) { return now - _birth; }
private:
std::string _name;
Time _birth;
};

此类不再具有任何setter。这个类不再向其内部返回任何句柄(因此,我将支付副本的价格,可能不会有太多)。

然而,最值得注意的一点是,我改变了记忆信息的方式:age是一个从出生日期和当前日期得出的波动值。因此,为什么要记住副产品而不是来源?

我想更改值吗?井:

Time const now = Time::Now();
person = Person("John R. Smith", now - person.age(now));

效果很好。至少我只写过一次不变量(在构造函数中)。

显然,这并不一定适用于所有地方。如果你的类只有几个字段,尽管它运行良好;当您的类获得更多字段时,也许是时候将其中一些字段提取到自己的类中了。

其思想是,该类定义了一个接口,这是其作者和任何使用它的开发人员之间的合同。该接口应尽可能保持不变,因为对接口的任何更改都可能需要对使用它的代码进行更改。

编写setter和getter意味着您可以验证对私有成员的访问,并且可以在不影响任何客户端代码的情况下更改底层实现(例如,通过更改内部数据类型)。

例如,假设您已经编写了一个time类。setter可以防止调用者在hoursminutesseconds成员中存储无效值。假设这些值是从实时时钟中获取的。如果RTC不可访问或损坏,getter可以查询RTC并返回错误代码。

作为另一个例子,假设您定义了一个返回32位整数的getter。稍后您会发现一个不准确的地方,可以通过将实现更改为使用64位整数来修复。(也许它在做某种计算,四舍五入到32位。)getter封装了实现,所以你可以在内部自由更新它,使其使用64位,但仍然返回一个(现在更准确)32位的值,所以你的新类是旧类的替代品。

Getter和setter通常只有在您执行的操作不仅仅是分配成员变量时才有用。

class A {
public:
int get_value () const;
void set_value (int v);
private:
int _value;
};

如果您1)从未打算更改实现(基本上您没有针对接口进行编码),2)没有要执行的验证,那么以上内容并不能为您带来任何好处。

当你的类由不共享不变量的数据组成时(基本上类只是收集不同的值,但成员之间没有规则),你可以争论getter和setter是否有用。

例如,在Date类的情况下,您应该真正使用setter和getter,因为成员(天、月和年)共享一个不变量(例如,当月增加到12以上时,年也应该增加)。

但是,如果Point类的X和Y不共享不变量,则可以跳过getter和setter,除非您真的想将其作为一个接口并使用多态性。

此外,我不同意OOP具有挑战性,因为规范会发生变化(无论如何,它们总是会发生变化),但正确使用OOP是一种技术(与其他技术相结合),可以帮助你应对变化,并将程序的各个部分相互隔离,这样变化就不会渗透到你的整个代码库中。我不会说你可以遵循任何简单的规则,所以这只是一个经验问题。

  1. 使用set/get修改类外部的值(比如另一个类中的值)
  2. 隐藏不需要在类外部公开的函数(使用私有函数)。例如,我有一个类,它具有使用各种方法计算积分的函数。但所有这些算法都依赖于一个名为sumIt()的函数,其他类不应该显式使用该函数
  3. 如果您想避免每次手动编写自己的getter/setter,可以使用模板轻松地扩展C++以允许类似"属性"的修饰符。这取决于您的类/应用程序的设计