OOP中的文档应避免指定“ getter”是否执行任何计算?


39

我学校的CS程序避免了任何有关面向对象编程的问题,所以我一直在自己做一些阅读来补充它-特别是Bertrand Meyer的面向对象软件构造

Meyer反复指出,类应隐藏尽可能多的有关其实现的信息,这是有道理的。特别是,他反复指出,属性(即类的静态,非计算属性)和例程(与函数/过程调用相对应的类的属性)应该是无法区分的。

例如,如果一个类Person具有属性age,他断言,它应该是不可能告诉,从符号,是否Person.age相当于国内喜欢的东西return current_year - self.birth_date或干脆return self.age,其中self.age已被定义为一个常量属性。这对我来说很有意义。但是,他继续声称以下内容:

将设计类的标准客户端文档(称为类的简称),以免透露给定功能是属性还是函数(在可能的情况下)。

即,他声称即使是该类的文档也应避免指定“ getter”是否执行任何计算。

这,我不明白。难道文档不是将这种区别告知用户的重要场所吗?如果我要设计一个包含Person对象的数据库,那么知道是否Person.age是一个昂贵的调用就不重要了,那么我可以决定是否为其实现某种缓存?我是否误解了他的意思,还是他只是OOP设计哲学的一个极端例子?


1
有趣的问题。我最近问了一个非常相似的问题:如何设计一个接口,以便清楚地知道哪些属性可以更改其值,哪些属性可以保持不变?。我得到了一个很好的答案,指向文档,即Bertrand Meyer似乎反对的东西。
stakx

我还没看过这本书。迈耶(Meyer)是否提供他所推荐的文档样式的任何示例?我很难想象您描述的适用于任何语言的内容。
user16764 2013年

1
@PatrickCollins我建议您阅读“在名词王国中的执行”,并在此处理解动词和名词的概念。其次,OOP与吸气剂和
二传手无关

@AndreasScheinert-您指的是这个吗?我嘲笑“只想要马蹄铁”,但这似乎是关于面向对象编程的弊端。
帕特里克·柯林斯

1
@PatrickCollins是的:steve-yegge.blogspot.com/2006/03/…!它提供了一些需要深思的要点,其他要点是:您应该使用(s)setter将对象变成数据结构。
AndreasScheinert

Answers:


58

我不认为Meyer的意思是,当您进行昂贵的手术时,您不应该告诉用户。如果您的函数要访问数据库或向Web服务器发出请求,并花费数小时的计算时间,则其他代码将需要知道这一点。

但是使用您的类的编码人员不需要知道您是否已实现:

return currentAge;

要么:

return getCurrentYear() - yearBorn;

这两种方法之间的性能特征是如此之小,无所谓。使用您的课程的编码人员实际上不必理会您拥有的课程。那是迈耶的观点。

但这并非总是如此,例如,假设您在容器上有一个size方法。可以实现:

return size;

要么

return end_pointer - start_pointer;

或者可能是:

count = 0
for(Node * node = firstNode; node; node = node->next)
{
    count++
}
return count

前两者之间的差异确实不重要。但是最后一个可能会对性能产生严重影响。这就是为什么STL,例如说,.size()O(1)。它没有确切记录尺寸的计算方法,但确实提供了性能特征。

所以:文件性能问题。不要记录实施细节。我不在乎std :: sort如何对我的东西进行排序,只要它能正确有效地进行排序即可。您的班级也不应记录其如何计算事物,但是如果事物具有意外的性能特征,则应记录该事物。


4
此外:首先记录时间/空间复杂性,然后说明为什么函数具有这些属性。例如:// O(n) Traverses the entire user list.
乔恩·普迪

2
=(像Python len这样琐碎的事情无法做到这一点...(至少在某些情况下,这是O(n)我们在大学的一个项目中学到的,当时我建议存储长度而不是每次循环迭代都重新计算长度)
Izkata

@Izkata,很好奇。您还记得什么结构O(n)吗?
2013年

@WinstonEwert不幸的是没有。那是4年前的一个Data Mining项目,我只是直觉地建议给我的朋友,因为我一直在和C一起上另一堂课
。。– Izkata

1
@JonPurdy我要补充一点,在普通的业务代码中,指定big-O复杂性可能没有意义。例如,O(1)数据库访问很可能比O(n)内存列表遍历慢得多,因此请记录下重要的事情。但是,在某些情况下,记录复杂性非常重要(集合或其他算法繁重的代码)。
svick

16

从学者或CS纯粹主义者的角度来看,在文档中描述功能实现内部的任何内容当然都是失败的。这是因为类的用户最好不要对类的内部实现做任何假设。如果实现发生更改,理想情况下没有用户会注意到这一点-该功能创建了一个抽象,内部应该完全隐藏。

然而,现实中大多数计划乔尔Spolsky`s患“漏抽象法”,其中说:

“在某种程度上,所有非平凡的抽象都是泄漏的。”

这意味着,几乎不可能创建复杂功能的完整黑盒抽象。性能问题是这种情况的典型症状。因此,对于现实世界的程序而言,哪些调用成本高昂而哪些不昂贵,就变得非常重要,一个好的文档应该包括该信息(或者应该说明允许类的用户对性能进行假设,而在何处不对性能进行假设) )。

因此,我的建议是:如果您为一个真实程序编写文档,则包括有关潜在昂贵通话的信息​​,而对于仅出于CS课程教育目的而编写的程序,则将其排除在外,并考虑到所有性能方面的考虑故意超出范围。


+1,以及创建的大多数文档都是供下一个程序员维护您的项目的,而不是由下一个程序员使用的
jmoreno

12

您可以编写给定的电话是否昂贵。更好的方法是使用命名约定,例如getAge快速访问和/ loadAgefetchAge昂贵的查找。您绝对想通知用户该方法是否正在执行任何IO。

您在文档中提供的每个细节都像合同,必须由班级履行。它应该告知重要的行为。通常,您会看到带有大O表示法的复杂度指示。但是您通常想要简短而切合实际。


1
+1表示文档是类合同及其接口的一部分。
巴特·范·英根·谢瑙

我支持这个。进一步地,一般而言,通过提供具有行为的方法来尝试最小化对吸气剂的需求。
sevenforce 2013年

9

如果我要设计一个包含Person对象的数据库,那么知道Person.age是否是一个昂贵的电话就变得不重要了吗?

是。

这就是为什么我有时使用Find()函数来指示调用它可能需要一段时间的原因。这比其他任何事情都更符合惯例。它需要一个函数或属性返回的时间,使该方案无明显差异(尽管它可能给用户),但程序员之间存在一种期望,如果它被声明为一个属性,成本称呼它应该是低。

无论如何,在代码本身中应该有足够的信息来推断某个东西是函数还是属性,因此我在文档中并没有真正说出这一点。


4
+1:该约定在很多地方都是惯用的。此外,文档应该在接口级别完成-那时您还不知道 Person.Age是如何实现的。
Telastyn

@Telastyn:我从来没有想过以这种方式来编写文档。也就是说,它应该在接口级别完成。现在看来很明显。对该有价值的评论+1。
stakx

我很喜欢这个答案。如果Person是从RESTful服务检索到的实体,则描述性能与程序本身无关的一个完美示例。GET是固有的,但尚不清楚它是便宜还是昂贵。当然,这不一定是面向对象的,但重点是相同的。
maple_shaft

+1用于Get在属性上使用方法来指示更重的操作。我已经看到足够多的代码,其中开发人员认为属性只是一个访问器,并多次使用它,而不是将值保存到局部变量中,因此多次执行非常复杂的算法。如果没有约定不实现这样的属性,并且文档中没有暗示复杂性,那么我希望有谁必须维护这种应用程序。
enzi

这个约定从何而来?从Java的角度来看,我希望它与之相反:get方法等同于属性访问,因此并不昂贵。
sevenforce 2013年

3

值得注意的是,本书的第一版于1988年OOP成立之初编写。这些人正在使用当今广泛使用的更加纯粹的面向对象的语言。当今我们最受欢迎的OO语言-C ++,C#和Java-与早期(更纯粹的OO)语言的工作方式有一些相当大的差异。

在C ++和Java之类的语言中,必须区分访问属性和方法调用。instance.getter_method和之间存在很大的差异instance.getter_method()。一个实际上获得了您的价值,而另一个则没有。

当使用Smalltalk或Ruby的更纯粹的OO语言(在本书中使用的Eiffel语言)时,它成为完全有效的建议。这些语言将为您隐式调用方法。instance.attribute和之间没有区别instance.getter_method

我不会冒犯这一点,也不会教条地接受它。目的是好的-您不希望您的类的用户担心无关的实现细节-但它不能完全转换为许多现代语言的语法。


1
关于考虑提出建议的年份的非常重要的一点。尼特:Smalltalk和Simula的历史可以追溯到60年代和70年代,因此88几乎不是“早期”。
luser droog 2013年

2

作为用户,您不需要知道如何实现。

如果性能是一个问题,则必须在类实现内部做一些事情,而不是围绕它。因此,正确的措施是修复类实现或将错误提交给维护人员。


3
但是,是否总是会出现一种计算昂贵的方法是一个错误的情况?举一个简单的例子,假设我关心的是对字符串数组的长度求和。在内部,我不知道我的语言中的字符串是Pascal风格还是C风格。在前一种情况下,由于字符串“知道”它们的长度,因此我可以期望我的length-summing-loop根据字符串数采用线性时间。我还应该知道,更改字符串长度的操作将具有与之相关的开销,因为string.length每次更改时都会重新计算。
帕特里克·柯林斯

3
在后一种情况下,由于字符串不“知道”其长度,因此我可以期望我的length-summing-loop花费二次时间(这取决于字符串的数量及其长度),但是会改变长度的字符串会更便宜。这些实现都不是错误的,也不值得提交错误报告,但是它们要求略有不同的编码样式,以避免意外的打ic。如果用户至少有一个模糊的想法,这会不会更容易?
帕特里克·柯林斯

因此,如果您知道字符串类实现了C样式,则将考虑到这一事实,选择一种编码方式。但是,如果字符串类的下一个版本实现新的Foo样式表示呢?您会相应地更改代码还是接受由于错误的假设而导致的性能损失?
mouviciel 2013年

我懂了。因此,面向对象的响应是“如何依靠特定的实现从代码中榨取一些额外的性能?” 是“你不能。” 响应“我的代码比我预期的要慢,为什么?” 是“需要重写。” 是或多或少的想法?
帕特里克·柯林斯

2
@PatrickCollins OO响应依赖于接口而不是实现。不要使用不包含性能保证作为接口定义一部分的接口(例如C ++ 11 List.size被保证为O(1)的示例)。它不需要在接口定义中包括实现细节。如果您的代码慢于您希望的速度,那么除了将其更改为更快(在分析确定瓶颈之后)之外,还有其他答案吗?
stonemetal

2

任何未能告知程序员例程/方法的复杂性代价的面向程序员的文档都是有缺陷的。

  • 我们正在寻求产生无副作用的方法。

  • 如果一个方法的执行已运行时间复杂度和/或存储器比其它复杂性O(1)在存储-或时间受限的环境中它可以被认为是具有副作用

  • 如果某个方法执行了完全出乎意料的操作(在这种情况下,是占用内存或浪费CPU时间),则会违反“最小惊喜原则


1

我认为您正确理解了他,但我也认为您有一个很好的观点。如果Person.age是通过昂贵的计算实现的,那么我想我也希望在文档中看到这一点。重复调用(如果是便宜的操作)或一次调用和缓存值(如果很昂贵)之间可能会有所不同。我不确定,但是我认为在这种情况下Meyer 可能同意在文档中包含警告。

解决此问题的另一种方法可能是引入一个新名称,该名称的名称暗示可能要进行冗长的计算(例如Person.ageCalculatedFromDB),然后Person.age返回缓存在该类中的值,但这可能并不总是合适的,而且似乎过于复杂我认为这件事。


3
也可能会提出这样的论据:如果您需要了解的agePerson则无论如何都要调用该方法来获取它。如果呼叫者开始做过于精明的事情,以致于不得不进行计算,那么他们就有可能由​​于跨越生日边界而导致其实现无法正常工作。该类中的昂贵实现将表现为性能问题,这些问题可以通过分析来根除,并且可以在类中完成诸如缓存之类的改进,所有调用者都将看到其收益(以及正确的结果)。
Blrfl 2013年

1
@Blrfl:是的,缓存应该Person该类中完成,但是我认为这个问题的目的是更笼统,这Person.age只是一个例子。在某些情况下,让呼叫者选择更有意义-也许被呼叫者有两种不同的算法来计算相同的值:一种快速但不准确,一种速度较慢但更准确(将3D渲染集中在一个地方可能会发生的情况),并且文档中应提及这一点。
FrustratedWithFormsDesigner

与每次您期望相同的答案相比,提供不同结果的两种方法是不同的用例。
Blrfl 2013年

0

面向对象类的文档通常需要权衡取舍,既要使类的维护者可以灵活地更改其设计,又要允许类的使用者充分利用其潜力。如果一个不可变的类将有许多属性,这将有一定的精确相互关系(例如LeftRightWidth整数坐标网格对齐矩形的属性),可以设计该类以存储两个属性的任意组合并计算第三个属性,或者可以将其设计为存储所有三个属性。如果关于接口的任何事情都不能弄清楚存储了哪些属性,则该类的程序员可能会由于某种原因而证明这样做有帮助的情况下更改设计。相比之下,例如,如果其中两个属性作为final字段公开而第三个属性没有公开,则该类的未来版本将始终必须使用与作为“基础”相同的两个属性。

如果属性没有确切的关系(例如,因为它们不是floatdouble而不是int),则可能有必要记录哪些属性“定义”了类的值。例如,即使Left加号Width应该相等Right,浮点数学也经常是不精确的。例如,假设Rectangle使用给定的as 和as 构造使用类型Floataccepts LeftWidthas的构造函数参数的a 。总和的最佳表示形式是1234568.125 [可能显示为1234568.13];下一个较小的将是1234568.0。如果该类实际存储并Left1234567fWidth1.1ffloatfloatLeftWidth,它可能会报告指定的宽度值。但是,如果构造函数是Right根据传入的LeftWidth计算Width的,后来又根据Left和构造的Right,则它将报告宽度为1.25f而不是传入的宽度1.1f

使用可变的类,事情可能会变得更加有趣,因为对一个相互关联的值的更改将暗示对至少另一个的更改,但是可能并不总是清楚哪个是一个。在某些情况下,它可能是最好的避免方法其中“集”一个单一的财产本身,而是要么方法,例如SetLeftAndWidthSetLeftAndRight什么性质被指定和正在发生变化,或者明确(例如MoveRightEdgeToSetWidthChangeWidthToSetLeftEdgeMoveShapeToSetRightEdge) 。

有时,使用一个类来跟踪已指定的属性值和已从其他属性计算出的值可能很有用。例如,“时刻”类可能包括绝对时间,本地时间和时区偏移。与许多此类类型一样,给定任何两条信息,一个就可以计算出第三条。知道哪个计算一条信息,但是有时可能很重要。例如,假设一个事件记录为已发生在“ UTC 17:00,时区-5,本地时间12:00 pm”,并且后来发现该时区应为-6。如果知道UTC是在服务器之外记录的,则应将记录更正为“ UTC 18:00,时区-6,本地时间12:00 pm”;如果有人在当地时间以外的时间键入时间,则应为“ UTC 17:00,时区-6,当地时间11:00 am”。但是,如果不知道应该将全球时间还是本地时间视为“更可信”,就无法知道应该采用哪种更正方法。但是,如果记录跟踪指定了哪个时间,则对时区的更改可能会使一个时区保持不变,而更改另一个时区。


0

所有这些关于如何在类中隐藏信息的规则在需要防止类用户中有人错误地创建对内部实现的依赖的假设下都是很有意义的。

如果班级有这样的听众,则可以建立这样的保护措施。但是,当用户在您的课程中编写对某个函数的调用时,他们将通过执行时银行帐户信任您。

这是我经常看到的事情:

  1. 对象有一个“修改过的”位,表示从某种意义上说它们是否已过时。足够简单,但是它们拥有下级对象,因此直接让“ modified”成为对所有下级对象求和的函数就很简单了。然后,如果存在多个从属对象层(有时多次共享同一对象),则“ modified”属性的简单“ Get”操作可能会占用相当一部分执行时间。

  2. 当以某种方式修改对象时,假定散布在软件周围的其他对象需要“通知”。这可以发生在由不同程序员编写的多层数据结构,窗口等上,有时还会重复执行需要防止的无限递归。即使这些通知处理程序的所有编写者都非常谨慎地避免浪费时间,整个复合交互最终仍会使用无法预测且痛苦的很大一部分执行时间来结束,并且巧妙地假设了它是“必需的”。

因此,我喜欢看到类向外界提供了一个干净的抽象接口,但是我只是想了解它们是如何工作的,即使只是为了了解它们正在为我节省些什么。但是除此之外,我倾向于觉得“少即是多”。人们如此迷恋数据结构,以至于他们认为更好的是更好的,而当我进行性能调整时,性能问题的普遍原因是人们过分地坚持以膨胀的数据结构来构建人们的教学方式。

所以去图。


0

添加诸如“计算或不计算”或“性能信息”之类的实现细节,将使代码和文档保持同步变得更加困难

例:

如果您有一个“性能昂贵”的方法,是否还要对使用该方法的所有类都记录“昂贵”的文档?如果您将实现更改为不再昂贵该怎么办。您是否也要向所有消费者更新此信息?

当然,对于代码维护者来说,从代码文档中获取所有重要信息是一件好事,但是我不喜欢声明不再有效的文档(与代码不同步)


0

得出的结论是:

因此:文档性能问题。

并且自记录的代码被认为比文档更好,其结果是方法名称说明任何异常的性能结果。

所以仍然Person.age适用,return current_year - self.birth_date但是如果该方法使用循环来计算年龄(是):Person.calculateAge()

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.