-- 作者:卷积内核
-- 发布时间:5/12/2006 2:27:00 PM
-- C++从零开始(十二)
C++从零开始(十二) ——何谓面向对象编程思想 前面已经说明了C++中最重要的概念——类,并且介绍了大部分和类相关的知识,至此,已经可以开始做些编程方面比较高级的应用——设计程序,而不再只是将算法变成代码。要说明如何设计程序,有必要先了解何谓编程思想。 编程思想 编程,即编写程序,而之前已经说过,程序就是方法的描述,那么编程就是编写方法的描述。我知道如何到人民公园,然后我就编写了到人民公园的方法的描述——从市中心开始向东走400米再向右转走200米就是。接着另一个人也知道如何去,但他编的程序却是——从市中心沿人民东路走过两个交叉口,在第三个交叉口处右转,直走就能在右手方看到。很明显,两个程序不同,但最后走的路线是相同的,即大家的方法相同,但描述不同。 所谓的编程思想,就是如何编程,即编写程序的方法。那么之前在《C++从零开始(八)》中说的编程的三个步骤其实就是一种编程思想。这也是为什么不同的人对同一算法编写出的程序不同(指程序逻辑,不是简单的变量或函数名不同),不同的人的编程思想不同。 如果多编或多看一些程序,就会发现编程思想是很重要的。好的编程思想编出的程序条理分明,可维护性高;差的编程思想编出的程序晦涩难懂,可维护性低。注意,这里是从程序的易读性来比较的,实际出于效率,是会使用不符合人脑思维习惯的编程思想,进而导致代码的难于维护,但为了效率还是会经常在程序的瓶颈位置使用被优化了的代码(这种代码一般使用汇编语言编写,算法则很大程度上是数学上的优化,丢弃了大部分其在人类世界中的意义)。 本系列一直坚持并推荐这么一个编程思想——一切均按照语义来编写。而语义是语言的意义,之前说它是代码在人类世界中的意义。比如桌子,映射成一个结构,有桌脚数、颜色等成员变量,那么为什么没有质量、材料、价格、生产日期等成员?对此有必要说明一下“人类世界”的含义。 世界 我们生活在一个四维的客观物理世界中,游戏中的怪物生活在游戏定义的游戏世界中,白雪公主生活在一个童话世界中。什么叫世界?世界即规则的集合。比如客观世界中,力可作用于有质量的物体上,并进而按照运动学定律改变物体的速度;电荷异性相吸同性相斥;能量守恒等,这些都是对客观世界这个规则的集合中的某些规则的描述。注意它们都只是规则的描述,不是规则,就好像程序是方法的描述,但不是方法。即方法和规则都是抽象的逻辑概念,各自通过程序和论调来表现。程序就是我们要编写的,而论调就是一门理论,如概率论、运动学、流体力学等。而前面所说的游戏世界,是因为游戏也是一系列规则,关于这点,我在我写的另一篇文章《游戏论》中做了详细的阐述,如果还未理解世界的概念,《游戏论》中关于何谓游戏的阐述希望能有所帮助。同样,童话世界也是由一系列的规则组成。如白雪公主能吃东西,能睡觉,并且能因为吃了毒苹果而中毒;魔镜能回答问题等。 那么就算了解了世界这个概念又怎样?有什么用?前面说了本系列是推荐按照语义来编写程序,即知道了算法后按照《C++从零开始(八)》中说的三步来编写程序。而算法是基于某些规则的,如给出1到100求和的算法是(1+100)*100/2,这里就暗示已经有那么些规则说明什么是加减乘除,什么是求和。即一个算法一定是就一个世界来说的,它在另一个世界可能毫无意义。因为算法就是方法,是由之前说的命令和被操作的资源组成,而命令和资源就是由世界来定义的。 前面说根据算法写代码,其实是先制订了一个世界来做为算法展示的平台。如之前的商人过河,其就在如下的一个规则集上表现的:有一只能坐两人的船可以载人过河;有三个商人和三个仆人在河的一边;河的任意一边仆人数多过商人数商人就会被杀。这是对过河问题所基于的世界的严重不准确的描述,但在这过于抽象并没什么好处,只用注意:上面的商人和仆人不是现实世界中的商人和仆人,他们不能吃饭不能睡觉不能讲话,甚至连走路都不会,唯一会的是通过坐船过河来改变自身的位置。当某一位置(即河的某一岸)的仆人的实例多于商人的实例时(且商人的实例至少有一个),则称商人被杀。上面的描述暂且称为商人仆人论,它是对过河问题所基于的世界的一个描述。 另一个人却不像上面那样看待问题。河有两个岸,每个河岸总对应着两个数字——商人数和仆人数。有一个途径能按照某个规则改变河岸对应的两个数字(就是坐只能坐两人的船过河),而当任何一个河岸所对应的仆人数多于商人数时(且商人数不为零),则称商人被杀。此人没有定义商人和仆人这么两个概念,而只定义了一个概念——河岸,此概念具有两个属性——商人数和仆人数。这是另一个论调,暂且称为河岸论。 什么意思?上面就是对商人过河问题所基于的世界的两个不同论调。注意上面论调不同,但描述的都是同一个世界,就好像动力学和量子力学,都是对客观世界物体之间作用规则的描述,但大相径庭。算法总是基于一个世界,但更准确点的说法应是算法总是基于一个世界的描述,而所谓的设计程序就是编写算法所基于的世界描述,即论调,而论调其实就是问题的描述。 现在考虑前面说的商人仆人论和河岸论,它们都是同一世界的描述,但前者提出两个名词性概念——商人和仆人,各自具有“位置”这个状态和“坐船”这么一个功能以及“商人被杀”这个动词性概念;后者提出一个名词性概念——河岸,具有“商人数”和“仆人数”两个属性和“商人被杀”及“坐船”两个动词性概念。在此,说后者比前者好,因为后者定义的名词性概念更少(即名词性概念比动词性概念更容易增加架构的复杂性,因为其代表了世界中东西的种类,种类越繁多则世界越复杂,越难以实现),虽说不一定更容易理解,但结构更简单。 易发现,所有的论调都可以只由“名词性概念”和“动词性概念”组成,其中前者在数学中就是数、实数、复数等,后者是加减乘除、求导等,它们都被称作定义。在《游戏论》中,我将前者称为类,而类的实例就是方法中被操作的的资源,后者称为命令。而在方法中,前者是资源的类型,后者是操作的类型。一个论调,提出的概念越少,结构就越简单,也就越好。但应注意,就电脑编程来说,由于电脑并不是抽象的概念,而是存在效率因素的,因此基于前述的好的论调的算法而编出的代码的运行效率并不一定高。 因此,所谓的程序设计,就是设计算法所基于的论调,而好的程序设计,就是相应的论调设计得好。但前面说了,效率并不一定高,对此,一般仅在代码的瓶颈位置另外设计,而程序的整体架构依旧按照之前的设计。随着程序的日趋庞大,清晰简明的程序架构越显重要,而要保持程序架构的简明,就应设计好的论调;要保持架构的清晰,就应按照语义来编写代码。下面,介绍如此风靡的面向对象编程思想来帮助设计程序。 何谓对象 要说明面向对象,首先应了解何谓对象。对象就是前述的“名词性概念”的实现,即一个实例。如商人仆人论中有商人和仆人两个“名词性概念”,其有三个商人和三个仆人,则称有六个对象,分别是三个商人的实例和三个仆人的实例。应注意对象和实例的区别,其实它们没有区别,如果非要说区别,可以认为对象能够没有状态,但实例一定有状态。 那么什么叫状态?还是先来看看什么叫属性。桌子有个属性叫颜色,这张桌子是红色的而那张是绿的。人有个状态叫脸色,这个人的脸色红润而那个的惨白。都是颜色,但一个是属性一个是状态,什么区别?如果把桌子和人都映射成类,那么桌子的颜色和人的脸色都应映射成相应类的成员变量,而两者的区别就是桌子的实例在主要运作过程中颜色都不变化,其主要用于读;人的脸色在人的实例的主要运作过程中可能变化,其主要用于写。什么叫运作过程?类映射的是资源,资源可以具有功能,即成员函数,当一个实例的功能执行时,就是这个实例的运作过程。 桌子有个功能是“放东西”,当调用这个成员函数时,其中会读取颜色这个属性的值以判断放在桌子上的东西的颜色是否和桌子的颜色搭配协调。人有个功能是“泡澡”,其可以使相应实例的脸色从惨白向红润转变。但桌子也有个功能是“改变颜色”,调用它可以改变桌子的颜色。按照前面所说,颜色是属性,应该被读,但这里却在实例的运作过程中对它进行了写操作。注意前面说的是“主要运作过程”,即桌子的目的是用来“放东西”,不是“改变颜色”。如果桌子这个概念在其相应世界中主要是用来改变其颜色而不是放东西,此时桌子只不过是一个能记录颜色值的容器,而这时桌子的颜色就是状态,不是属性了。 有何意义?属性和状态都映射为成员变量,从代码上是看不出它们的区别的,但它们的语义是有严重区别的。属性是用来配置实例而状态是用来表现实例。在面向对象编程思想中,只是简单地说对象是具有属性和功能(也被称作方法)的实例,这在编写的程序所基于的世界比较复杂时显得非常地孱弱,而且就是对“属性”的错误理解,再加上“封装”这个词汇的席卷,导致出现大量的荒谬代码,后面说明。 属性和状态的差别导致出现所谓的无状态对象(在MTS——Microsoft Transaction Server中提出,称作Stateless Component,无状态组件),这正是对象和实例的差别——对象是实现,因此可以是一个抽象概念的实现;实例是实际存在,不能是抽象概念的实现。这在C++代码上就表现为没有成员变量的类和有成员变量的类。如下: struct Search { virtual int search( int*, int, int ); }; Search a, b; int c[3] = { 10, 20, 5 }; a.search( c, 3, 20 ); 这里就生成两个对象a和b,它们都是抽象概念——搜索功能的对象。注意结构Search没有成员变量,因为不需要,那么a和b的长度是多少?由于可能出现下面的情况,一般的编译器都将上面的a和b的长度定为一个字节,进而&a就不等于&b。 struct BSearch : public Search { int search( int*, int, int ); }; Search *p; BSearch d; p = &a; p = &b; p = &d; p->search( c, 3, 5 ); 注意从代码上依旧可以称上面生成了Search的两个实例a和b,BSearch的一个实例d(即使实际上它们根本不存在,逻辑上大小为零),这也就是为什么之前说它和对象没有区别,仅仅有概念上的微小差别。 应注意前面提到的无状态对象并不是说没有成员变量的类的实例,只是没状态,并不代表没有属性。如前面的BSearch可能有个属性m_MaxSearchTimes以表示折半搜索时如果搜索m_MaxSearchTimes那么多次仍没找到,则BSearch::search返回没找到。虽然这里BSearch有了成员变量,但就逻辑上它还是一个抽象概念。由于属性和状态的实现相同(都通过成员变量),因此要实现无状态对象需要一些特殊手段,由于与本系列无关,在此不表。 前面的a和b有区别吗?为什么要有两个实例?“搜索功能”按照之前说的语义不是更应该映射为函数?为什么要映射成没有成员变量的类?上面的用法在STL(Standard Template Library——标准模版库)中被使用,做了一些变形,称作函数类,是一种编程技巧。但的确有这种语义——查找功能有三个参数:查找条件、查找位置(即欲搜索的容器或集合)、查找前排序容器或集合的方法。这里传递函数指针不是刚刚好吗(实际并不刚刚好,指针的语义是引用,在这并不很准确)?这就是所谓的面向对象编程思想。 面向对象编程思想 前面已说明设计程序就是编写程序欲解决的问题的描述,也就是编写论调。而论调可以只用“名词性概念”和“动词性概念”表现出来,对象又正好是“名词性概念”的实现,而利用前面说的没有成员变量的类来映射“动词性概念”就可以将其转换为对象。因此,一个世界,可以完全由对象组成,而将算法所基于的世界只用对象表现出来,再进行后续代码的编写,这种编程方法就被称作面向对象的编程思想。 注意,先设计算法应基于的世界,再全部用对象将其表述出来,然后再设计算法,最后映射为代码。但前面在编写商人过河问题时是直接给出算法的,并没有设计世界啊?其实由于那个问题的过于简单,我直接下意识地设计了世界,并且用前面所说的河岸论来描述它。应注意世界的设计完全依赖于问题,而准确地说,前面我并没有设计世界,而是设计了河岸论来描述问题。 接着,由于对象就是实例,因此以对象来描述世界在C++中就是设计类,通过类的实例来组合表现世界。但应注意,面向对象是以对象来描述世界,但也描述算法,因为算法也会提出一些需要被映射的概念,如前面商人过河问题的算法中的过河方案。但切记,当描述算法时操作了描述世界时定义的类,则一定要保持那个类的设计,不要因为算法中对那个类的实例的操作过于复杂而将那部分算法映射为这个类的一个成员函数,因为这严重遮蔽了算法的实现,破坏了程序的架构。如一个算法是让汽车原地不停打转,需要复杂的操作,那么难道给汽车加一个功能,让它能原地不停地打转?!这是在设计类的时候经常犯的错误,也由于这个原因,一个面向对象编写的代码并不是想象的只由类组成,其也可能由于将算法中的某些操作映射成函数而有大量的全局函数。请记住:设计类时,如果是映射世界里的概念,不要考虑算法,只以这个世界为边界来设计它,不要因为算法里的某个需要而给它加上错误的成员。 因此,将“名词性概念”映射成类,“名词性概念”的属性和状态映射为成员变量,“名词性概念”的功能映射为成员函数。那么“动词性概念”怎么办?映射成没有成员变量的类?前面也看见,由于过于别扭,实际中这种做法并不常见(STL中也只是将其作为一种技巧),故经常是将它映射为函数,虽然这有背于面向对象的思想,但要易于理解得多,进而程序的架构要简明得多。 随着面向对象编程思想的问世,一种全新的设计方式诞生了。由于它是如此的好以至于广为流传,但理解的错误导致错误的思想遍地而生,更糟糕的就是本末倒置,将这个设计方式称作面向对象的编程思想,它的名字就是封装。 封装 先来看现在在各类VC教程中关于对象的讲解中经常能看见的如下的一个类的设计。 class Person { private: char m_Name[20]; unsigned long m_Age; bool m_Sex; public: const char* GetName() const; void SetName( const char* ); unsigned long GetAge() const; void SetAge( unsigned long ); bool GetSex() const; void SetSex( bool ); }; 上面将成员变量全部定义为private,然后又提供三对Get/Set函数来存取上面的三个成员变量(因为它们是private,外界不能直接存取),这三对函数都是public的,为什么要这样?那些教材将此称作封装,是对类Person的内部内存布局的封装,这样外界就不知道其在内存上是如何布局的并进而可以保证内存的有效性(只由类自身操作其实例)。 首先要确认上面设计的荒谬性,它是正宗的“有门没锁”毫无意义。接着再看所谓的对内存布局的封装。回想在《C++从零开始(十)》中说的为什么每个要使用类的源文件的开头要包含相应的头文件。假设上面是在Person.h中的声明,然后在b.cpp中要使用类Person,本来要#include "Person.h",现在替换成下面: class Person { public: char m_Name[20]; unsigned long m_Age; bool m_Sex; public: const char* GetName() const; void SetName( const char* ); unsigned long GetAge() const; void SetAge( unsigned long ); bool GetSex() const; void SetSex( bool ); }; 然后在b.cpp中照常使用类Person,如下: Person a, b; a.m_Age = 20; b.GetSex(); 这里就直接使用了Person::m_Age了,就算不做这样蹩脚的动作,依旧#include "Person.h",如下: struct PERSON { char m_Name[20]; unsigned long m_Age; bool m_Sex; }; Person a, b; PERSON *pP = ( PERSON* )&a; pP->m_Age = 40; 上面依旧直接修改了Person的实例a的成员Person::m_Age,如何能隐藏内存布局?!请回想声明的作用,类的内存布局是编译器生成对象时必须的,根本不能对任何使用对象的代码隐藏有关对象实现的任何东西,否则编译器无法编译相应的代码。 那么从语义上来看。Person映射的不是真实世界中的人的概念,应该是存放某个数据库中的某个记录人员信息的表中的记录的缓冲区,那么缓冲区应该具备那三对Get/Set所代表的功能吗?缓冲区是缓冲数据用的,缓冲后被其它操作使用,就好像箱子,只是放东西用。故上面的三对Get/Set没有存在的必要,而三个成员变量则不能是private。当然,如果Person映射的并不是缓冲区,而在其它的世界中具备像上面那样表现的语义,则像上面那样定义就没有问题,但如果是因为对内存布局的封装而那样定义类则是大错特错的。 上面错误的根本在于没有理解何谓封装。为了说明封装,先看下MFC(Microsoft Foundation Class Library——微软功能类库,一个定义了许多类的库文件,其中的绝大部分类是封装设计。关于库文件在说明SDK时阐述)中的类CFile的定义。从名字就可看出它映射的是操作系统中文件的概念,但它却有这样的成员函数——CFile::Open、CFile::Close、CFile::Read、CFile::Write,有什么问题?这四个成员函数映射的都是对文件的操作而不是文件所具备的功能,分别为打开文件、关闭文件、从文件读数据、向文件写数据。这不是和前面说的成员函数的语义相背吗?上面四个操作有个共性,都是施加于文件这个资源上的操作,可以将它们叫做“被功能”,如文件具有“被打开”的功能,具有“被读取”的功能,但应注意它们实际并不是文件的功能。 按照原来的说法,应该将文件映射为一个结构,如FILE,然后上面的四个操作应映射成四个函数,再利用名字空间的功能,如下: namespace OFILE { bool Open( FILE&, … ); bool Close( FILE&, … ); bool Read( FILE&, … ); bool Write( FILE&, … ); } 上面的名字空间OFILE表示里面的四个函数都是对文件的操作,但四个函数都带有一个FILE&的参数。回想非静态成员函数都有个隐藏的参数this,因此,一个了不起的想法诞生了。 将所有对某种资源的操作的集合看成是一种资源,把它映射成一个类,则这个类的对象就是对某个对象的操作,此法被称作封装,而那个类被称作包装类或封装类。很明显,包装类映射的是“对某种资源的操作”,是一抽象概念,即包装类的对象都是无状态对象(指逻辑上应该是无状态对象,但如果多个操作间有联系,则还是可能有状态的,但此时它的语义也相应地有些变化。如多一个CFile::Flush成员函数,用于刷新缓冲区内容,则此时就至少有一个状态——缓冲区,还可有一个状态记录是否已经调用过CFile::Write,没有则不用刷新)。 现在应能了解封装的含义了。将对某种资源的操作封装成一个类,此包装类映射的不是世界中定义的某一“名词性概念”,而是世界的“动词性概念”或算法中“对某一概念的操作”这个人为定出来的抽象概念。由于包装类是对某种资源的操作的封装,则包装类对象一定有个属性指明被操作的对象,对于MFC中的CFile,就是CFile::m_hFile成员变量(类型为HANDLE),其在包装类对象的主要运作过程(前面的CFile::Read和CFile::Write)中被读。 有什么好处?封装提供了一种手段以将世界中的部分“动词性概念”转换成对象,使得程序的架构更加简单(多条“动词性概念”变成一个“名词性概念”,减少了“动词性概念”的数量),更趋于面向对象的编程思想。 但应区别开包装类对象和被包装的对象。包装类对象只是个外壳,而被包装的对象一定是个具有状态的对象,因为操作就是改变资源的状态。对于CFile,CFile的实例是包装类对象,其保持着一个对被包装对象——文件内核对象(Windows操作系统中定义的一种资源,用HANDLE的实例表征)——的引用,放在CFile::m_hFile中。因此,包装类对象是独立于被包装对象的。即CFile a;,此时a.m_hFile的值为0或-1,表示其引用的对象是无效的,因此如果a.Read( … );将失败,因为操作施加的资源是无效的。对此,就应先调用a.Open( … );以将a和一特定的文件内核对象绑定起来,而调用a.Close( … );将解除绑定。注意CFile::Close调用后只是解除了绑定,并不代表a已经被销毁了,因为a映射的并不是文件内核对象,而是对文件内核对象操作的包装类对象。 如果仔细想想,就会发现,老虎能够吃兔子,兔子能够被吃,那这里应该是老虎有个功能是“吃兔子”还是多个兔子的包装类来封装“吃兔子”的操作?这其实不存在任何问题,“老虎吃兔子”和“兔子被吃”完全是两个不同的操作,前者涉及两种资源,后者只涉及一种资源,因此可以同时实现两者,具体应视各自在相应世界中的语义。如果对于真实世界,则可以简略地说老虎有个“吃”的功能,可以吃“肉”,而动物从“肉”和“自主能动性”多重继承,兔子再从动物继承。这里有个类叫“自主能动性”,指动物具有意识,能够自己动作,这在C++中的表现就是有成员函数的类,表示有功能可以被操作,但收音机也具有调台等功能,难道说收音机也能自己动?!这就是世界的意义——运转。 方法——世界的驱动方式 算法就是方法,前面已说过其由操作和被操作的资源组成,即资源的类型和操作的类型。方法指出如何使用世界中定义出的各种操作,但并不执行。由前面的阐述,世界可以只由对象组成,当对象产生后,世界中所有对象的状态和属性,即成员变量,的一份拷贝,称作世界的状态的一份快照,而世界的状态的变化称作世界的运转。世界的状态就是世界中所有对象的状态和属性,要改变它,就是要执行世界定义的操作,但只能通过方法指出如何执行它以改变世界的状态,进而驱动世界,即使世界定义的操作被执行才能驱动世界。 上面越说越远了,感觉虚得很,有什么意义?考虑为什么要提出世界这个概念。世界是我们欲编程解决的问题所基于的规则集合体,而设计程序就是设计描述世界的论调,然后在这个论调上设计算法,编写出代码,执行代码,得到结果。“得到结果”?!什么是结果?即世界最终状态中的某一部分,如求圆周率的值。这其实是目的,但值得注意的是目的不止这种。代码执行的过程往往是另一种目的,如将数据保存到某个文件中;将文件打开编辑再保存等,这种目的并不关心世界的状态最后如何。而世界的状态的变化过程,也就是世界的运转则是另一种非常流行的目的——电子游戏。 不管什么样的目的,都需要改变世界的状态,即要驱动世界运转,也就必须使世界定义的操作被执行,而这只能通过方法来实现。因此在设计算法时,也就决定了驱动世界的方式。 对于上面的第一种目的,由于是要看世界的最终状态,因此一直连续执行操作到最后。在《C++从零开始(八)》中给出的商人过河的例子就在通过算法得到结果后直接调用printf打印出结果并结束。对于第二种目的,由于要的是执行的过程,因此也可直接连续执行操作到完。但这种目的往往要求由用户决定何时执行且执行不止一段代码,如文件打开后,直到用户给出命令(通过键盘或鼠标或其它输入方式)后才进行编辑操作,且用户可能随机地执行不同的编辑操作,最后也由用户决定是否保存文件。这种世界的运转完全由用户控制的世界驱动方式称为用户驱动方式。这里的算法仅仅是如何打开、保存文件,如何编辑数据,但由于决定是用户驱动方式,则算法就必须修改以实现这种驱动方式。再看第三种目的,要的是世界的状态的变化,则前面两种都可以。但很明显,第一种变化过程不能持续,连续执行完就完;第二种由用户驱动,则太麻烦。因此往往都会有个循环,在游戏编程中一般称其为主循环,每次循环都按照一定的规则改变世界中部分对象的状态,此称作循环驱动方式。每次循环,都会被改变状态的对象就被称作具有自主能动性,如前面提到的动物的实例。除此以外的就不称作具有自主能动性,如前面提到的收音机的实例。同样,游戏中的算法依旧不会涉及到上面提到的循环驱动方式,因此必须修改算法以实现循环驱动方式。 上面将那么多只为了说明一点,已经不能再如《C++从零开始(八)》中说的那几步来编写程序了,下面给出一个方法。 1. 当得到一个问题,应同时得到这个问题的算法(程序员并不是科学家),或由客户给出或由于过于简单而直接得出。 2. 由问题抽象设计出它的描述,即前面说的论调,也就是所谓的程序设计。 3. 将之前给出的算法用刚设计出的论调进行描述,并完善这个论调(因为算法可能带入一些原来世界中并不存在的概念)。 4. 由需要决定使用何种世界驱动方式,并实现以完善算法和论调(世界的驱动方式也可能带入一些原来并不存在的概念)。 5. 继续《C++从零开始(八)》中提出的三步。 这里给出的步骤只是一般性的,当程序对应的世界过于复杂时,上面的第2步将还需细分。先设计程序的大框架;再设计接口;最后决定接口的具体运用。关于接口会在《C++从零开始(十八)》中说明,而这里提到的关于接口的设计方式并不用管它,它只是使设计简单化,并不是必须的。由于其与本系列无关,在此不表。 本文提出了多个抽象的概念,例子较少,且只说了以面向对象思想设计类时应注意的一些问题,提到世界定义的概念越少越好,但并没说如何决定有哪些概念。对此,其实从前面关于老虎吃兔子的问题中就可看出,因为问题是现实世界中的问题,因此只需简单地映射现实世界中的概念,并去掉或简化一些次要概念(其实这就是语义在另一方面的体现)。在下篇,将针对本文提出的那五个程序编写步骤给出一个基于对象设计的简单样例以大致说明如何编写面向对象的程序。
|