本节书摘来自华章出版社《面向对象的思考过程(原书第4版)》一书中的第1章,第1.3节,[美] 马特·魏斯费尔德(Matt Weisfeld) 著黄博文 译更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1.3 过程式编程与面向对象编程
在我们深入了解面向对象开发的优势之前,先考虑一个更基本的问题:究竟什么是对象?这既是一个复杂的问题,也是一个简单的问题。它复杂是因为学习任何一种软件开发方法论都非易事。它简单是因为人们已经在按对象的方式进行思考。
例如,当你看到一个人,你会把他看作一个对象。一个对象由两部分组成:属性及行为。一个人具有属性,比如眼睛颜色、年龄、身高等。一个人也有行为,比如行走、讲话、呼吸等。对象的基本定义是一个包含了数据和行为的实体。
单词和(both)是面向对象编程与其他编程范式的核心区别。例如在过程式编程中,代码放置在完全不同的函数或程序中。如图1-1所示的理想情况下,程序即为“黑盒”(black box),接收输入,然后输出。数据放置在单独的结构中,通过函数或程序进行操作。
面向对象与过程式编程的不同之处
在面向对象的设计中,属性及行为包含在单个对象中,而在过程式或结构式设计中,属性和行为通常是分开的。
虽然面向对象设计越来越流行,但有个现实问题最初阻碍了面向对象设计的推广,那就是很多非面向对象的系统还在正常工作。因此,没有任何商业动力来将其改造为面向对象的系统。任何熟悉计算机系统的人都知道不管对系统的修改有多小,都极可能引发灾难性的后果。
同样的情形也阻碍了采纳面向对象的数据库。随着面向对象开发方式的发展,貌似面向对象的数据库将替代关系型数据库。然而,这绝不会发生。关系型数据库得到了大量的商业投资,而现有的关系型数据库可以正常工作,这一重要因素阻碍了向面向对象的数据库的转换。将关系型数据库转换为面向对象的数据库花销巨大又充满风险,但却没有一个足够令人信服的理由来支持转换。
事实上,企业已经找到了一个折中方案。当今很多软件开发实践糅和了几种开发方法论,比如面向对象和面向过程方法论。
如图1-2所示,结构化编程中数据往往与程序分离,而且数据是全局的,所以在你的代码作用域之外依然可以很容易修改数据。这意味着对数据的访问是失控的,并且不可预期(因为很多功能都可以访问全局数据)。而且,由于你无法控制谁能访问数据,那么测试和调试将变得更加困难。对象通过将数据和行为组合到一个完整的包中从而解决了这些问题。
恰当的设计
如果设计是恰当的,那么在面向对象模型中则不会有诸如全局数据的元素。事实上,在面向对象系统中具有很高的数据完整性。
对象并不会完全替代其他的软件开发范式,它是一种进化。结构化的程序有复杂的数据结构,比如数组等。C++有结构体,结构体具有对象(类)的很多特性。
然而,对象比数据结构体及原始数据类型(比如整型和字符串)更丰富。对象包含整型和字符串之类的实体,用于表示属性,对象也包含方法,用于表示行为。在对象中,方法用于操作数据及其他行为。更重要的是,你可以控制对对象中成员(包括属性及方法)的访问。这意味着某些成员(属性和方法)可以对其他对象隐藏起来。例如,名为Math的对象包含两个整数,叫作myInt1和myInt2。同时,Math对象也包括了必要的方法存取myInt1和myInt2的值。它也包括一个叫作sum()的方法对这两个整数求和。
数据隐藏
在面向对象的术语中,数据表现为属性,行为表现为方法。限制访问具体属性和(或)方法的行为叫作数据隐藏。
将属性及方法合并到同一个实体中,在面向对象中将这种方式叫作封装(encapsulation)。我们可以控制对Math对象的数据的访问。比如将这些整数定义为禁止对外访问,其他函数无法操作整数myInt1和myInt2,只有Math对象才行。
合理的类设计指导
请注意完全有可能创建一个设计差劲的面向对象的类,该类没有对自身属性的访问进行任何限制。即使你使用面向对象设计也可能会设计出烂代码,使用其他的编程方法论都会有这样的可能性。请坚持合理的类设计指导(详见第5章)。
另一个名为myObject的对象如何获取myInt1和myInt2的总和?它会询问Math对象,即myObject会给Math对象发送一个消息。图1-3展示了这两个对象如何通过方法进行通信。该消息其实就是调用Math对象的sum方法,sum方法把值返回给了myObject。巧妙之处在于,myObject无需知道总和是如何计算出来的(尽管我认为它能猜到)。使用这种设计方法,你可以修改Math对象计算总和的方式而无需对myObject做任何修改(这意味着获取总和的方式无需改变)。你需要的是总和,而不用关心它是如何计算出来的。
我们可以通过一个简单的计算器示例来演示该概念。当使用计算器求和时,你只需使用计算器提供的接口,即键盘和LED显示器。计算器有一个求和方法,当你按下正确的按键序列就会调用它。你会得到正确的返回结果,然而你无需知道结果是如何计算出来的,你既不用关心电路控制,也不用关心算法。
求和不是myObject的责任,它是Math对象的责任。由于myObject可以访问Math对象,它可以发送相应的信息并获取正确的结果。通常,对象不应当操作其他对象的内部数据(即myObject不应当直接修改myInt1和myInt2的值)。而且随着持续开发,最好多构建一些具有明确任务的小对象,而不应该构建包含很多方法的大对象。