类的不变式- -

最近看到的:
http://www.royaloo.com/bjarne/interviews/bs_artima1.htm

类应该强制执行不变式

 

Bjarne Stroustrup:根据我的经验,当且仅当你确定你的类中有一个不变式时,你应该设计一个具有接口的类以及一个隐藏的表示。

 

Bill Venners:您所说的不变式是什么意思?

 

Bjarne Stroustrup:是什么让一个对象成为一个有效的对象?不变式允许你说出一个对象的表示何时良好何时不好。以vector作为一个非常简单的例子,一个vector知道它容纳了n个元素,它有一个指向这n个元素的指针。这儿不变式就是指指针指向一块内存区域,而这块内存区域可以容纳n个元素。如果它容纳了n-1或者n+1个元素,那就出现了bug。如果指针为0,那也是bug,因为该指针并未指向任何东西,这就表示了它违背了一个不变式。所以你必须分辨出哪些对象有意义,哪些是好的,哪些是坏的。这样你就可以提炼出维护不变式的接口。这是检查成员函数合理性的一种途径,同时也是判断一个操作是否应该成为成员函数的一种方式。那些不需要与内部表示混在一起的操作最好被安排到类外。这样一来,你就可以得到一个整洁、小巧而容易理解和维护的接口。

 

Bill Venners:这就是说,不变式表示一个类存在的正当性,因为类本身承担起了维护该不变式的责任?

 

Bjarne Stroustrup:没错。

 

Bill Venners:这样说来,不变式就是类中各个不同的数据成员之间的一种关系?

 

Bjarne Stroustrup:是这样的。如果每个数据成员都可以被赋予任何值,那就没什么太大必要做成一个类。以一个简单的“名字—地址”数据结构为例,如果任何字符串都是合法的名字,任何字符串都是合法的地址,那么它本质上就是一个结构,而不是一个类,请使用struct关键字来声明它。千万别把名字和地址作为私有数据成员隐藏起来,然后再提供类似于get_address、set_address、get_name以及set_name这样的成员函数来存取它们。或者更糟糕地,提供一个拥有get_name和set_name之类的虚函数的抽象基类,然后在一个派生类中 重写它们,这种做法纯粹是挖空心思而已,绝无必要。

 

Bill Venners:您的意思是因为那些类有且仅有一种具体的实现,所以把它们声明为class是不必要的 。可是有一种辩解认为:如果你将数据成员存取操作封装为函数,那么你就可以灵活地改变这个类的具体实现方式了。

 

Bjarne Stroustrup:大部分情况下是这样的,但有些实现是你不会去改变的:你并不会经常去改变一个整数、或者一个点、或者一个复数等类的实现。如果真的 需要改变它们的话,你应该在某个地方做好设计决策。

 

下一层次,当你要从原始的数据结构转移到真正的类的时候,让我们再次以名字-地址为例:你应该不会将这个类命名为name_and_address吧?也许你可以把它称为personnel_record或者mailing_address。在这个层次上,你认为名字和地址都不仅仅只是字符串而已。也许你想把名字拆开为first name、middle name和last name来分别存储,或者你决定采用一种由你自己确定语义的字符串来存储这三个部分,你也可以决定是否判断地址的有效性,并根据这些性质将字符串分为first address、second address、城市、洲、国家以及邮编 之类的东西。



当开始进行这样的分解工作时,你就应该开始考虑更改这个类的具体实现的可能性了。这时,你要开始做出决定:是真的往类中加入一个私有数据成员,并使用继承 ,还是仅仅只使用一个平凡的类,并固定其表现形式,或者你希望为数据提供一个抽象接口,这样它们就可以拥有不同的表现形式。这里的重点不是“如何决定”,而是“做出决定”。你不能毫无章法可言地将一些类和函数堆砌在一起 ,如果你决定采用私有数据成员的话,你需要先定义一些确切的语义。

 

整个事情的思路是:在构造函数中,建立好成员函数进行操作时所必需的环境。换句话来说,构造函数建立了不变式。而为了建立不变式,你通常需要分配一些资源。在析构函数中,你可以清理环境并切释放资源 ,这些资源可以是内存、文件、同步锁以及socket连接等,凡是你能想到的,都可以在析构函数中被释放。

 

设计简单的接口

 

Bill Venners:您刚才提到不变式可以帮助我们确定什么东西是应该放到接口 中的,可以解释得更详细一点吗?我现在试着复述一遍您刚才所说的概念,看看我是否已经理解了它。第一点,所有和不变式相关或能够操作不变式的功能都应该放到类中。

 

Bjarne Stroustrup:对!

 

Bill Venners:任何仅仅使用数据 而不会对不变式产生影响的功能就不需要放到类中。

 

Bjarne Stroustrup:我来给个例子具体说明一下 ,有些操作是你一定要与具体实现进行直接的交互才能完成的。如果一个操作会改变一个vector的大小,那你最好也让 其同时改变vector内容纳的元素的数量。如果你仅仅需要读出这个大小变量的话,那么肯定应该存在这样的一个成员函数。但除 了这些需要直接与不变式打交道的基本函数外,还存在许多以这些函数为基础的其他函数,比如对vector的高效存取、查找和搜索操作等 ,这些函数就最好不要被设计为成员函数。

 

作为另一个例子,让我们再来看看日期类。所有能够改变年、月、日的操作应该被作为该类的成员函数,而那些诸如搜索下一个周末、周日的函数则可以建构在基本成员函数之上。我曾经 看到过一个拥有60到70个成员函数的日期类,那个类的设计者把所有操作都放到类里面去了,甚至包括find_next_Sunday这样的函数也不例外,实际上这些函数在逻辑上与这个类没有任何关系,如果你将这个函数作为该类的成员变量,那么这些函数就可以接触到类中的具体数据成员,这就意味着如果你想改变日期的表现形式的话,你需要 修改近60个函数,在近60个地方修改 (译注:可能还不止:))。

 

作为一种替代,如果你为这个日期类建立一个具有相互联系的简单接口的话,那么出于逻辑相关或者性能等方面的考虑,这个类中应该只会有5到10个左右的成员函数 — 虽然我想不出日期类有什么性能问题可言,不过它的确是思考问题时一个重要的焦点。然后以这5到10个基本操作为基础,你再把另外的50个操作放到一个支撑库中。 最近这种思维方式被越来越多的人所接受。甚至在Java中,你也可以在拥有一个容器的同时拥有一个由静态方法所构成的支撑库。

 

可惜的是,尽管我已经为这种思想作了近20年的宣传工作,人们仍然倾向于把所有东西都放到类和继承体系中去。关于上面提到的日期问题,我还见过这样的解决方案:提供一个基类,该基类有一些基本的操作和被声明为protected的数据成员。日后当你需要 添加一些新的工具函数时,你需要从这个基类派生出一个新类,然后再在新类中加上新的工具函数。相信我,你的系统就是被这些东西弄得一团糟的,把这些工具函数放到派生类中毫无道理可言。将这些工具函数分别独立实现可以让我们自由 地组合使用这些工具函数。由于你写的函数和我写的函数是完全独立的,所以可以自由组合它们。如果我和你都从那个日期基类派生出新类型,然后通过往新类型中添加新函数的方法来实现各自的工具函数,那么第三人将很难同时使用我们的函数库,在这里,类继承体系 即被滥用了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值