Global Data
全局数据
全局数据可以在程序中任意一个位置访问。这一概念有时被延伸到作用域比局部变量更广的变量——例如可以在一个类内部任意位置进行访问的类变量。但是,在一个类内部的任意位置可访问,并不意味该变量是全局的。
大多数有经验的程序员已经得出结论:使用全局数据的风险比使用局部数据大。极富经验的程序员还认为通过一些子程序来访问数据很有帮助。
不过,即便全局数据并不总是引发错误,也很难将其作为最佳的解决办法。本节的剩余部分会对有关问题展开全面讨论。
一、Common Problems with Global Data
与全局数据有关的常见问题
如果你随意使用全局数据,或者认为不能随心所欲地使用它们是一种约束,那么你可能还没有充分理解信息隐藏和模块化的意义。模块化、信息隐藏并结合使用设计良好的类可能还算不上是绝对真理,但是它们能极大地提升大型程序的可理解性和可维护性。一旦明白了这一点,你就会努力去写出与全局变量和外界联系尽可能少的子程序和类来。
人们指出了使用全局数据的许多问题,实际上这些问题都可以归结到下面几种情况。
1、无意间修改了全局数据
你可能会无意间在某处修改了一个全局变量的值,然后错误地认为它在其他的位置还是保持不变的。这种问题称为“副作用”(side effects)。
2、与全局数据有关的奇异的和令人激动的别名问题
”别名“(aliasing)指的是两个或更多不同名字,说的是同一个变量。当一个全局变量被传递给一个子程序,然后该子程序将它既用作全局变量又用作参数使用的情况,就会出现这种情况。
3、与全局数据有关的代码重入(re-entrant)问题
可以由一个以上的线程访问的代码正变得越来越常见。多线程代码造成了这样一种可能性,那就是全局数据将不但在多个子程序间共享,而且也将在同一个程序的不同拷贝之间共享。在这种环境下,你必须确保即使一个程序的多个拷贝同时运行,全局数据也会保持其意义。这是一个重大问题。你可以使用本节后面将要建议的技巧来加以避免。
4、全局数据阻碍代码重用
要把一个程序里的代码应用于另一个程序,你必须能够把它从第一个程序里取出,然后插入到另一个程序里面。在理想状况下,你可以把一个单一子程序或者类取出来,把它插入到另一个程序里面去,然后高兴地继续下去。
全局数据使这件事变得复杂了。如果你想重用的类读或写了全局数据,那么你就无法简单地把它插入到新的程序里。你将不得不修改新的程序或者旧类,以便让它们相容。如果选择上策,你应该去修改旧类,让它不再使用全局数据。如果你真的这么做了,那么下次你再需要重用这个类的时候,就可以把它插入到新程序中而无须花费额外的力气。如果选择下策,那就去修改新的程序,以创建旧有的类所需要使用的全局数据。这样做就像传播病毒:该全局数据不但影响了原来的程序,还传染到使用旧程序中的类的新程序里面。
5、与全局数据有关的非确定的初始化顺序事宜
有些语言,特别是C++,没有定义不同”转译单元“(文件)里的数据初始化的顺序。如果在初始化一个文件中的全局变量的时候使用了在另一个不同文件中初始化的全局变量,那么除非你用明确的手段来确保这两个变量能按照正确的顺序初始化,否则请不要对第二个变量的取值下任何赌注。
6、全局数据破坏了模块化和智力上的可管理性
创建超过几百行代码的程序的核心便是管理复杂度。你能够在智力上管理一个大型程序的唯一方法就是把它拆分成几部分,从而可以在同一时间只考虑一部分。模块化就是你手中可以使用的把程序拆分成几部分的最强大工具。
全局数据使得你的模块化能力大打折扣。如果你用了全局数据,你能够在同一时间只关注一个子程序吗?不能。你不得不关注一个子程序,以及使用了同样全局数据的其它所有子程序。尽管全局数据并没有完全破坏程序的模块化,但是却削弱了它,而这已经是很充分的理由,要求你去寻找问题的更好解决方案了。
二、Reasons to Use Global Data
使用全局数据的理由
数据纯化论者有时争论说,程序员应该绝不使用全局数据,但是按照“全局数据”这一术语的广义解释,大多数程序都使用了它。存在于数据库中的数据是全局数据,存在于配置文件如Windows注册表中的数据也是。具名常量也是全局数据,只不过不是全局变量而已罢了。
如果遵循使用的原则,那么全局变量在一些场合下也是有用的。
1、保存全局数值
有时候你会有一些在概念上用于整个程序的数据。这可能是一个用于表示程序状态的变量——例如,交互式模式或者命令模式、正常模式或者错误恢复模式标识。也可能是在整个程序里面要用到的信息——例如,程序中的每一个子程序都会用到的数据表。
2、模拟具名常量
尽管C++、Java、Visual Basic和多数现代语言都支持具名常量,但是Python、Perl、Awk以及UNIX shell脚本等语言却不支持。当你的语言不支持具名常量的时候,你可以用全局变量代替它们。例如,你可以用取值分别为1和0的全局变量TRUE和FALSE来代替字面量1和0,或者用LINSES_PER_PAGE = 66来代表每页行数的66。一旦采用了这种方法,那么日后再修改代码就会更容易了,而且这样的代码会更方便阅读。贯彻这种对全局数据的使用原则是在一种语言上编程(programming in a language)和深入一种语言去编程(programming into a language)之间差异的一个主要示例。
3、模拟枚举类型
你还可以在Python等不直接支持枚举类型的语言里用全局变量和模拟枚举类型。
4、简化对极其常用的数据的使用
有的时候你会大量地引用一个变量,以致于它几乎出现在你所编写的每一个子程序的参数列表里。与其将它包含在每一个参数列表里面,不如把它设置成全局变量。不过,事实上很少会出现代码到处访问某一个变量的情形。通常该变量是由为数不多的一组子程序来访问的,你可以把这些子程序以及它们所用到的数据整合进一个类里面。下面会就这一问题展开讨论。
5、消除流浪数据
有的时候你把数据传递给一个子程序或者类,仅仅是因为想把它传递给另一个子程序或者类。例如,你可能有一个在每个子程序里都使用的错误处理对象。当调用链中间的子程序并不使用这一对象的时候,这一对象就被称为“流浪数据(tramp data)”。使用全局变量可以消除流浪数据。
三、Use Global Data Only as a Last Resort
只有万不得已时才使用全局数据
在你选择使用全局数据之前,请考虑下面这些替换方案。
1、首先把每一个变量设置为局部的,仅当需要时才把变量设置为全局的
开始的时候先把所有的变量都设置为单一子程序内部的局部变量。如果你发现还需要在其他位置用到它们,那么在一举把它们转变成全局变量之前,先把它们转变为类里的private或者protected变量。如果你最终发现必须要把它们转变成全局变量,那么就转变它们。不过请先确定除此之外别无选择。如果你一开始就把变量设置为全局的,那么你将永远不可能把它转变成局部的;反之,如果你开始时把变量设置为局部的,那么你可能永远也不需要把它转变成全局的。
2、区分全局变量和类变量
有些变量由于要被整个程序访问,因此是真正的全局变量。其他只在一组特定的子程序里被频繁使用的实际是类变量。在频繁使用某个类变量的子程序组里,你可以采用任何希望的方式来访问它。如果类外部的子程序需要使用该变量,那么就用访问器子程序来提供对该变量的访问。不要直接访问类变量————好像它们是全局变量一样————即便你的编程语言允许你这么做。这一建议等价于高呼:“模块化!模块化!模块化!”
3、使用访问器子程序
创建访问器子程序是避免产生与全局数据相关问题的主要方法。下一节会对些做更多的讨论。
四、Using Access Routines Instead of Global Data
用访问器子程序来取代全局数据
你用全局数据能做的任何事情,都可以用访问器子程序做得更好。使用访问器子程序是实现抽象数据类型和信息隐藏的一种核心方法。即使你不希望使用装备齐全的抽象数据类型,你仍然可以用访问器子程序来集中控制你的数据,并保护你免受变化的困扰。
1、Advantages of Access Routines
访问器子程序的优势
使用访问器子程序可以带来很多的好处。
1)你获得了对数据的集中控制。如果你日后发现了一种更合适的实现该结构的方法,那么你无须到处修改引用该数据的代码。所需做的修改不会波及整个程序。它被限制在访问器子程序的内部。
你可以确保对变量的所有引用都得到了保护。如果你用stack.array[stack.top] = newElement这样的语句向栈中压入元素,你会很容易就忘记检查栈溢出,从而犯下严重的错误。如果你使用了访问器子程序————例如 PushStack(newElement)————你就可以把栈溢出检测写到PushStack()子程序里。这一检测会在每次调用该子程序的时候自动执行,你可以忘记它。
你可以自动获得信息隐藏的普遍益处。访问器子程序是信息隐藏的一个例子,哪怕你并不是出于这一理由才设计它们的。你可以修改一个访问器子程序的内部代码而无须涉及程序的其余部分。访问器子程序允许你在不改变你房子外表的情况下重新装修内部,而你的朋友们还是可以认出它来。
2)访问器子程序可以很容易地转变为抽象数据类型。访问器子程序的一项优点是,让你可以创建一个很难用全局数据来直接创建的抽象层。例如,与其写if lineCount > MAX_LINES,访问器子程序让你能采用if PageFull()。这样一种小修改说明了这个if lineCount 检测的用意,代码也实现了所表示的用途。这是对可读性的一点小小改进,但是如果能坚持重视这些细节,就能写出同那些东拼西凑(hack)到一起的代码迥然不同的精致程序了。
2、How to Use Access Routines
如何使用访问子程序
下面是有关访问器子程序的理论和实践的总结:把数据隐藏到类里面。用static关键字或者它的等价物来声明该数据,以确保只存在该数据的单一实例。写出让你可以查看并且修改该数据的子程序来。要求类外部的代码使用该访问器子程序来访问该数据,而不是直接操作它。
举例来说,如果你有一个全局的状态变量g_globalStatus,用于描述这个程序整体状态,你可以创建两个访问器子程序:globalStatus.Get()和globalStatus.Set(),获得原有全局变量所能提供的所有好处。
如果你的语言不支持类,你仍然可以创建访问子程序来操纵全局数据,但必须制定严格的代码编写标准,限制对全局数据的使用,以代替编程语言内置的约束。
下面是你的语言没有内置对类的支持的状况下,使用访问器子程序来隐藏全局变量的一些详细的指导原则。
1)要求所有的代码通过访问器子程序来存取数据
一个好习惯是要求所有的全局数据都冠以g_前缀,并且除了该变量的访问器子程序以外,所有的代码都不可以访问具有g_前缀的变量。其他全部代码都通过访问器子程序来存取该数据。
2)不要把你所有的全局数据都扔在一处
如果把所有的全局数据都堆到一起,然后为它编写一些访问器子程序,你可以消灭所有与全局数据有关的问题,但这也使代码丧失了信息隐藏和抽象数据类型所带来的好处。既然已经在编写访问器子程序,就请花些时间考虑每一个全局数据属于哪个类,然后把该数据和它的访问器子程序以及其他的数据和子程序打包放入那个类里面。
3)用锁定来控制对全局变量的访问
与一个多用户数据库环境中的并发控制相类似,锁定要求在使用或者更新一个全局变量值之前,该变量必须被签出(check out)。在用完这一变量之后再把它签入(check in)回去。在使用期间(已check out),如果程序的其余部分尝试要将它check out,那么锁定/解锁子程序就会显示一条错误消息,或者触发一个断言。
关于锁定的这种描述忽略了很多通过编写代码来充分支持并发操作的微妙之处。基于这种原因,像这样的简化的锁定方式最适用于开发阶段。除非很好地设计它,否则它可能不足以可靠到放入产品环境里面去。当把程序投入产品环境里时,这些代码就要进行修改,以执行一些比显示错误消息更安全和优雅的操作。例如,当代码检测到程序的多个组成部分都在试图锁定同一个全局变量的时候,它可能会在文件里记录下一条错误消息。
当你使用访问器子程序来取代全局数据的时候,这种开发阶段的防范措施就会相当容易实现,然而如果你直接使用全局数据,那么实现起来就会十分不便。
4)在你的访问器子程序里构建一个抽象层
要在问题域这一层次上构建访问器子程序,而不是在细节实现层次上。这种方法会为你的代码带来更好的可读性,同时防止在代码编写过程中不小心修改到实现细节。
5)使得对一项数据的所有访问都发生在同一个抽象层上
如果你用一个访问器子程序对一个结构体执行了某种操作,那么在对此结构体执行任何其他操作时,你同样也应该使用一个访问器子程序。如果你用某个访问器子程序读取该结构体,那么就用另一个访问器子程序写入该结构体。如果你调用InitStack()来初始化栈,就应该调用PushStack()来往栈上压值,这样你就为该数据创建了一个一致的视角。但如果通过value = array[stack.top]来从栈中弹出数据,你所创建的对该数据的操作就不一致。这种不一致性会使得其它人很难理解该代码。应该创建一个PopStack()子程序来代替value = array[stack.top]。
尽管你可能认为这些指导原则只适用于大型程序,但实践证明,访问器子程序可以成为一种能避免同全局数据相关问题的有效解决方案。除此之外,它会使得代码更具可读性,并且增加了代码的灵活度。
五、How to Reduce the Risks of Using Global Data
如何降低使用全局数据的风险
在许多情况下,全局数据事实上就是没有设计好或没有实现的类中的数据。在少数情况下,一些数据的确需要作为全局数据,但是可以使用访问器子程序对其进行封装,从而最大限度地减少发生问题的可能性。在剩余的极少情况下,你真的需要使用全局数据。这时,你可以把下面的原则看做是在出游陌生国度前注射的疫苗,它们在某种程度上或许会带来些痛苦,但可以让你在旅行中更加健康。
1、创建一种命名规则来突出全局变量
在对全局变量进行操作时,为全局变量命以更醒目的名字可以让你少犯错误。如果你正在把全局变量用于多种用途(比如说,用做变量以及其具名常量的替代品),那么就要确保你的命名规则能够区分开这些不同的看法。
2、为全部的全局变量创建一份注释良好的清单
一旦你的命名规则表明了某个变量是全局的,那么指出该变量的具体功能将会大有好处。一份全局变量的清单是在你的程序上工作的人所能获得的最有用的工具之一。
3、不要用全局变量来存放中间结果
如果你需要为一个全局变量计算新值,那么应该在计算结束后再把最终结果赋给该全局变量,而不要用它来保存计算的中间结果。
4、不要把所有的数据都放在一个大对象中并到处传递,以说明你没有使用全局变量。
把所有一切都放在一个大对象里可能会满足不使用全局变量的要求,但是这样做纯粹是一种负担,它也无法真正带来封装所能带来的那些好处。如果你要使用全局数据,那么就公开地用。不要试图通过使用大对象来掩盖这一点。
有不同意见的,先自己稍息一会儿,读上几遍先消化消化,实在是掉进井里出不来的,别着急,回头我再套用PLC中的东西给你们翻译翻译。