第六章--可以工作的类

第六章--可以工作的类


6.1--类的基础:抽象数据类型(ADTs)


一、抽象数据类型ADTs

1:抽象数据类型是指一些数据以及对这些数据进行的操作的集合。

     不懂ADT的程序员开发出来的类只不过是把一些稍微有点关系的数据和子程序堆在一起!!!在理解ADT之后,才可以写出很容易实现,也易于维护的类来。


2.使用ADT的益处

    (1)可以隐藏实现细节:如,把设计字体数据类型的信息隐藏起来,如果以后改变该数据类型,也不会对其他程序造成影响。

    (2)改动不会影响到整个程序:如,要添加操作,只要在程序的某一处改动即可。

    (3)让接口提供更多信息:如currentFont.size = 16, 16的单位不够明确,如果把这些相似操作隐藏在ADT中,则可以基于磅数或像素来定义整个接口(在ADT中添加表示单位的数据项)。

    (4)更容易提高性能:通过修改ADT的子程序提高操作性能,而不要改整个程序。

    (5)让程序的正确性更显而易见:如可将currentFont.attribute = current-Font.attribute or 0x02 改为currentFont.SetBoldOn(), 这样就直接调用函数即可,不要担心写错or 或 0x02等细节。

    (6)程序更具自我说明性:currentFont.attribute = current-Font.attribute or 0x02 改为currentFont.SetBoldOn(), 更具有可读性。

    (7)无须在程序内到处传递数据:ADT里面的数据只有其子程序可以访问,ADT之外的则不能。把对字体的操作都隔离到一组子程序里,这样为其他要操作字体的程序提供了更好的抽象层,同时也可以在针对字体的操作发生变化时提供一层保护(无须对程序其他地方进行修改)。


3. 创建ADT的一些建议

(1)把常见的底层数据类型创建为ADT并使用这些ADT,而不再使用底层数据类型。如栈、列表、队列等。

(2)把像文件这样的常用对象当成ADT。可根据需要对ADT进行分层。

(3)简单的事物也可以当做ADT。如把“灯”和与之相关的操作“开”“关”放到一个ADT中,可以提高代码的自我说明能力,让代码更易于修改,还能把改动可能引起的后果封闭在子程序中,并减少了需要到处传递的数据的项数。

(4)不要让ADT依赖于其存储介质。如RateFile.Read()这样的命名就暴露了费率信息是存在文件中的事实,也就是依赖于存储介质。如果要存储到磁盘上,那么把它当做文件的代码将会不正确,容易产生误导。所以,可以命名为rates.Read()。


二、在非面向对象环境中用ADT处理多份数据实例

因为在面向对象的环境中,对ADT的创建和删除非常容易实现,无须考虑。

但在C语言这样的非面向对象的过程中,就必须添加一些创建和删除实例的服务操作。

对此,有下面三种方法作参考。

    1:每次使用ADT服务子程序时都明确地指明实例。如,Font ADT服务子程序负责跟踪所有底层数据,调用方给子程序传递一个参数fontId来区分多份实例。这样就需要为ADT的每个子程序都传递参数FontId。

    2:明确地向ADT服务子程序提供所要用到的数据。在要调用ADT服务的子程序中声明一个Font数据类型,并将它传到ADT的所有子程序中。这种方法的优点是ADT中的服务子程序不需要根据调用方传过来的fontId来查询字体信息。缺点则是向调用者暴露了字体内部的数据。调用方可能利用这种ADT的内部实现细节。

    3:使用隐含实例(要十分小心)。设计一个子程序,专门让它根据调用者传递过来的参数fontId来设定某一特定字体为当前字体实例。设定后,所有服务子程序都使用这一字体实例。优点是只有一个函数需要传递fontId,其他的子程序则不需要。缺点是对状态的依赖性高,必须在用到字体操作的所有代码中跟踪当前字体实例,复杂度高。



6.2--良好的类接口


一、好的抽象

良好的类接口应该提供一组明显相关的子程序,接口中的每个子程序都朝着一个目标而工作。不好的抽象的类可能会包含大量混杂的函数。

下面是创建类的抽象接口的一些指导建议:

(1)类的接口应该展现一致的抽象层次。!!!

        每一个类应该实现一个ADT,并且仅实现这个ADT。如果一个类中实现了多个ADT,或者你自己都不知道实现了何种ADT,那么抽象层次不一致,要将其修改或者拆分成多个明确的ADT。

(2)一定要理解类所实现的抽象是什么。

        一些类非常像,你必须非常仔细地理解类的接口应该捕捉的抽象到底是哪一个。

(3)提供成对的服务。

        如开灯、关灯。添加项目、删除项目。

(4)把不相关的信息转移到其他类中

        不要把两个类混在一起使用

(5)尽可能让接口可编程,而不是表达语义。

        每个接口都有一个可编程部分和一个语义部分组成。可编程部分由接口中的数据类型和其他属性组成,可在编译时检查错误。语义部分则又“本接口将会被怎样使用”的假定组成,无法通过编译器检查错误。

        要想办法将语义接口的元素转换为编程接口的元素,如使用Assert(断言)或其他技术。

(6)谨防在修改时破坏接口的抽象

        扩充类的功能时,要注意保持抽象的一致性,不然类会变成一锅大杂烩!

(7)不要添加与接口抽象不一致的公用成员。

(8)同时考虑抽象性和内聚性

        一个呈现出很好的抽象的类接口通常也有很高的内聚性。


   实践心得:

   在实现资源包打包工具时,接口应该就是简单的packFile(),而获取目录下所有文件列表getAllFileNamesFromDir()则是该类的私用函数,就不应该把它作为接口,也不应该作为成员函数,因为它不是打包工具有得功能,而只是为了实现打包工具的成员函数而用得私用函数。

   在实现从资源包中读取文件的API时,实现打开文件,读取文件和获取文件大小的三个接口就OK了,而不需要参照C语言读取文件相关函数实现所有API。


二、良好的封装

封装是一个比抽象更强的概念。没有封装时,抽象往往很容易被打破。比如,如果一个类暴露了他的成员数据,那么调用方就可以自由的使用这些数据,而这个类甚至连这些数据什么时候被改动过都不知道。良好的抽象应该是类的成员数据只能通过类的接口来进行访问。

良好封装的建议:

(1)尽可能限制类和成员的可访问性。

        采用最严格且可行的访问级别。

(2)不要公开暴露成员数据

        暴露成员数据会破坏封装性,从而限制你对这个抽象的控制能力。

(3)避免私用的实现细节放入类的接口中

        如,把private数据成员string m_Name放在头文件中,其实就是暴露了m_Name是string类型这一实现细节。解决方法是把类的接口和实现隔离开来,并在类的声明中包含一个指针,指向类的实现,但不能包含任何其他细节。

(4)不要对类的使用做出任何假设

(5)避免使用友元类,因为会破坏封装性。

(6)不要因为一个子程序里仅仅使用公用子程序,就把它归入公开接口。

(7)让阅读代码比编写代码更方便。

(8)要格外警惕从语义上破坏封装性。

        如果调用方代码不是依赖于类的公开接口,而是依赖于类的私用实现,那么你就不是在针对接口编程了,而是在透过接口针对内部实现编程。

(9)留意过于紧密的耦合关系

        尽量限制类和成员的可访问性

        避免使用友元类,因为他们之间是紧密耦合的。


6.3--有关设计和实现的问题

一、包含(有一个...)的关系

相比于继承,包含才是面向对象编程中得主力技术。

(1)通过包含来实现“有一个”的关系。

(2)在万不得已的时候通过private继承来实现“有一个”的关系。

(3)警惕有超过约7个数据成员的类

        研究表明,人们在做其他事情时,能记住的离散项目的个数是5~9个。

        

二、继承(是一个...)的关系

当使用继承是时,你必须考虑以下几个方面:

(1)对于每个成员函数,他应该对派生类可见吗?他应该有默认的实现吗?这一默认的实现能被覆盖吗?

(2)对于每一个数据成员,他应该对派生类可见吗?


使用继承的几点建议:

(1)用public继承来实现“是一个”的关系。

 如果派生类不准备用public完全遵守由基类定义的同一个接口契约,继承就不是正确的实现技术了,此时请考虑包含的关系,或修改继承体系。

(2)要么使用继承并进行详细说明,那么就不要用它。

(3)遵循Liskov替换原则:除非派生类真的“是一个”更特殊的基类,否则不应该从基类继承。

(4)确保只继承需要继承的部分。

(5)不要“覆盖”一个不可覆盖的成员函数。

 派生类中得成员函数不要与积累中不可覆盖的成员函数的重名。

(6)把共用的接口、数据及操作放到继承树中尽可能高的位置

(7)只有一个实例的类是值得怀疑的,单件模式是一个特例。

(8)只有一个派生类的基类也值得怀疑。

(9)派生后覆盖了某个子程序,但在其中没做任何操作,这种情况也值得怀疑。

(10)避免让继承体系过深。2到3层。

(11)尽量使用多态,避免大量的类型检查。

 如,在适当的时候用多态,代替case。

(12)让所有的数据都是private(而非protected)


三、多重继承

多重继承的用途主要是定义“混合体”。比如为对象添加Displayable(可显示)、Sortable(可排序)等类。这些类几乎总是抽象的,也不打算独立于其他对象而被单独实例化的。

 多重继承要避免菱形继承。


四、为什么有那么多继承规则

下面总结了何时可以使用继承,何时又该使用包含:

(1)如果多个类共享数据而非行为,则用包含。

(2)如果多个类共享行为而非数据,则用继承。

(3)如果机共享行为又共享数据,则用继承。

(4)如果想用基类控制接口,则用继承。如果想自己控制接口,则用包含。

五、成员函数和数据成员

下面就有效实现成员函数和数据成员提供一些建议:

(1)让类中子程序的数量尽可能少。

(2)禁止饮食地产生你不需要的成员函数和运算符。

(3)减少类所调用的不同子程序的数量。减少用到其他类的数量。

(4)对其他类的子程序的间接调用要尽可能少。

(5)一般来说,应该尽量减少类和类之间相互合作的范围

六、构造函数

(1)如果可能,应该在所有的构造函数中初始化所有的数据成员(有写数据成员只能由构造函数初始化)。

(2)用私用(private)构造函数来强制实现单件属性。

(3)优先使用深拷贝,除非论证可行,才使用浅拷贝。

七、创建类的原因

(1)为现实世界中得对象建模;

(2)为抽象的对象建模,如Shape(形状)这一抽象的对象。

(3)降低复杂度。

 创建类的最重要的理由就是降低程序的复杂度。

(4)隔离复杂度

(5)隐藏实现细节

(6)限制变动的影响范围

(7)隐藏全局数据:将数据隐藏到接口中

(8)让参数传递更顺畅

(9)建立中心控制点:如专门对数据库连接的类,文件操作的类,打印机相关的类。

(10)让代码更易于重用

(11)为程序族做计划

(12)把相关操作包装在一起

(13)实现某种特定的重构



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值