面向对象

类与对象

“类”和“对象”是面向对象编程中最基本的概念,从语言的角度来讲,“类”是用户自定义的具有一定行为的数据类型,“对象”则是“类”这种数据类型的变量。通俗的讲,“类”是具有相同或相似行为的事物的抽象,“对象”是“类”的实例,是是一组具有相关性的代码和数据的组合体,是有一定责任的实体。

类本身还可以进一步抽象为类型,类型是一种更高层次上的抽象,它只用来描述接口,比如抽象类和接口就是一种类型。当一个类型的接口包含另外一个类型的接口时,我们就可以说它是此类型的子类型。类型是用来标识特定接口的,如果一个对象接受某个接口定义的所有行为,那么我们就可以说该对象具有该类型。一个对象同时拥有多种类型。

面向对象编程的特性

面向对象编程有三个特性:封装,继承,多态。这三个特性从低级到高级描述了面向对象的特征。一种语言只有同时具备这三种特性才能被称为面向对象的语言。VB中也有类,它的类也支持封装和简单的继承,但是它不支持所有的继承语义和多态,因此VB只能被称为基于对象的语言。

封装是所有抽象数据类型(ADT)的特性,很多刚刚接触面向对象的人认为封装就是就是面向对象。将程序按照一定的逻辑分成多个互相协作的部分,并将对外界有用的稳定的部分暴露出来,而将会发生的改变隐藏起来,外界只能通过暴露的部分向这个对象发送操作请求从而享受对象提供的服务,而不必管对象内部是如何运行的,这就是封装。理解封装是理解面向对象的第一个步骤,40%的程序员对面向对象的理解仅停留在封装这个层次。

继承也称为派生,继承关系中,被继承的称为基类,从基类继承而得的被称为派生类或者子类。继承是保持对象差异性的同时共享对象相似性的复用。能够被继承的类总是含有并只含有它所抽象的那一类事务的共同特点。继承提供了实现复用,只要从一个类继承,我们就拥有了这个类的所有行为。理解继承是理解面向对象的第二个步骤,50%的程序员对面向对象的理解仅停留在继承这个层次。语义上的“继承”表示“是一种(is-a)”的关系。很多人体会到了继承在代码重用方面的优点,而忽视了继承的语义特征。于是很多滥用继承的情况就发生了,关于这一点我们将会在后边介绍。

多态是“允许用户将父对象设置成为一个或更多的它的子对象相等的技术,赋值后,基类对象就可以根据当前赋值给它的派生类对象的特性以不同的方式运作”(Charlie Calvert)。多态扩大了对象的适应性,改变了对象单一继承的关系。多态是行为的抽象,它使得同名方法可以有不同的响应方式,我们可以通过名字调用某一方法而无需知道哪种实现将被执行,甚至无需知道执行这个实现的对象类型。多态是面向对象编程的核心概念,只有理解了多态,才能明白什么是真正的面向对象,才能真正发挥面向对象的最大能力。不过可惜的是,只有极少数程序员能真正理解多态。

对象之间的关系

对象之间有两种最基本的关系:继承关系,组合关系。

继承关系

继承关系可以分为两种:一种是类对接口的继承,被称为接口继承;另一种是类对类的继承,被称为实现继承。继承关系是一种“泛化/特化”关系,基类代表一般,而派生类代表特殊。

组合关系。

组合是由已有的对象组合而成新对象的行为,组合只是重复运用既有程序的功能,而非重用其形式。组合与继承的不同点在于它表示了整体和部分的关系。比如电脑是由CPU、内存、显示器、硬盘等组成的,这些部件使得电脑有了计算、存储、显示图形的能力,但是不能说电脑是由CPU继承而来的。

1.2

对象之间有两种最基本的关系:继承关系,组合关系。通过这两种关系的不断迭代组合最终组成了可用的程序。但是需要注意的就是要合理使用这两种关系。

派生类是基类的一个特殊种类,而不是基类的一个角色。语义上的“继承”表示“is-a”(是一种)的关系,派生类“is-a”基类,这是使用继承关系的最基本前提。如果类A是类B的基类,那么类B应该可以在任何A出现的地方取代A,这就是“Liskov代换法则(LSP)”。如果类B不能在类A出现的地方取代类A的话,就不要把类B设计为类A的派生类。

举例来说,“苹果”是“水果”的派生类,所以“水果是植物的果实”这句话中的“水果”可以用“苹果”来代替:“苹果是植物的果实”;而“苹果”不是“香蕉”的派生类,因为“香蕉是一种种子退化的了的植物果实”不能被“苹果”替换为“苹果是一种种子退化的了的植物果实”。

举这个例子好像有点多余,不过现实的开发中却经常发生“苹果”从“香蕉”继承的事情。

某企业中有一套信息系统,其中有一个“客户(Customer)”基础资料,里边记录了客户的名称、地址、email等信息。后来系统要进行升级,增加一个“供应商(Supplier)”基础资料,开发人员发现“供应商”中有“客户”中的所有属性,只是多了一个“银行帐号”属性,所以就把“供应商”设置成“客户”客户的子类。

图 2.1

到了年终,老板要求给所有的客户通过Email发送新年祝福,由于“供应商”是一种(is-a)“客户”,所以系统就给“供应商”和“客户”都发送了新年祝福。第二天很多供应商都感动流涕的给老板打电话“谢谢老板呀,我们供应商每次都是求着贵公司买我们的东西,到了年终你们还忘不了我们,真是太感谢了!”。老板很茫然,找来开发人员,开发人员这才意识到问题,于是在发送Email的程序里做了判断“如果是供应商则不发送,否则发送”,一切ok了。到了年初,老板要求给所有很长时间没有购买他们产品的“客户”,打电话进行问候和意见征集。由于“供应商”是一种(is-a)“客户”,所以第二天电话里不断出现这样的回答:“你们搞错了吧,我们是你们的供应商呀!”。老板大发雷霆,开发人员这才意识到问题的严重性,所以在系统的所有涉及到客户的地方都加了判断“如果是供应商则……”,一共修改了60多处,当然由于疏忽遗漏了两处,所以后来又出了一次类似的事故。

我们可以看到错误使用继承的害处了。其实更好的解决方案应该是,从“客户”和“供应商”中抽取一个共同的基类“外部公司”出来:

图 2.2

这样就将“客户”和“供应商”之间的继承关系去除了。

派生类不应大量覆盖基类的行为。派生类具有扩展基类的责任,而不是具有覆盖(override)基类的责任。如果派生类需要大量的覆盖或者替换掉基类的行为,那么就不应该在两个类之间建立继承关系。

让我们再来看一个案例:

一个开发人员要设计一个入库单、一张出库单和一张盘点单,并且这三张单都有登帐的功能,通过阅读客户需求,开发人员发现三张单的登帐逻辑都相同:遍历单据中的所有物品记录,然后逐笔登到台帐上去。所以他就设计出了如下的程序:

图 2.3

把登帐逻辑都写到了“库存业务单据”这个抽象类中,三张单据从这个类继承即可。过了三个月,用户提出了新的需求:盘点单在盘点过程中,如果发现某个货物的盘亏量大于50则停止登帐,并向操作人员报警。所以开发人员在盘点单中重写了“库存业务单据”的“登帐”方法,实现了客户要求的逻辑。又过了半个月,客户要求出库登帐的时候不仅要进行原先的登帐,还要以便登帐一边计算出库成本。所以开发人员在出库单中重写了“库存业务单据”的“登帐”方法,实现了客户要求的逻辑。到了现在“库存业务单据”的“登帐”方法的逻辑只是对“入库单”有用了,因为其他两张单据都“另立门户”了。

这时候就是该我们重新梳理系统设计的时候了,我们把“库存业务单据”的“登帐”方法设置成抽象方法,具体的实现代码由具体子类自己决定:

图 2.4

注意此处的“库存业务单据”中的“登帐”方法是斜体,在UML中表示此方法是一个抽象方法。这个不难理解,每张单据都肯定有登帐行为,但是每张单据的登帐行为都有差异,因此在抽象类中定义类的“登帐”方法为抽象方法以延迟到子类中去实现。

继承具有如下优点:实现新的类非常容易,因为基类的大部分功能都可以通过继承关系自动赋予派生类;修改或者扩展继承来的实现非常容易;只要修改父类,派生的类的行为就同时被修改了。

初学面向对象编程的人会认为继承真是一个好东西,是实现复用的最好手段。但是随着应用的深入就会发现继承有很多缺点:继承破坏封装性。基类的很多内部细节都是对派生类可见的,因此这种复用是“白箱复用”;如果基类的实现发生改变,那么派生类的实现也将随之改变。这样就导致了子类行为的不可预知性;从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。

继承关系有很多缺点,如果合理使用组合则可以有效的避免这些缺点,使用组合关系将系统对变化的适应力从静态提升到动态,而且由于组合将已有对象组合到了新对象中,因此新对象可以调用已有对象的功能。由于组合关系中各个各个对象的内部实现是隐藏的,我们只能通过接口调用,因此我们完全可以在运行期用实现了同样接口的另外一个对象来代替原对象,从而灵活实现运行期的行为控制。而且使用合成关系有助于保持每个类的职责的单一性,这样类的层次体系以及类的规模都不太可能增长为不可控制的庞然大物。因此我们优先使用组合而不是继承。

当然这并不是说继承是不好的,我们可用的类总是不够丰富,而使用继承复用来创建一些实用的类将会不组合来的更快,因此在系统中合理的搭配使用继承和组合将会使你的系统强大而又牢固。

1.3

接口的概念

接口是一种类型,它定义了能被其他类实现的方法,接口不能被实例化,也不能自己实现其中的方法,只能被支持该接口的其他类来提供实现。接口只是一个标识,标识了对象能做什么,至于怎么做则不在其控制之内,它更像一个契约。

任何一个类都可以实现一个接口,这样这个类的实例就可以在任何需要这个接口的地方起作用,这样系统的灵活性就大大增强了。

接口编程的实例

SQL语句在各个不同的数据库之间移植最大的麻烦就是各个数据库支持的语法不尽相同,比如取出表的前10行数据在不同数据库中就有不同的实现。

MSSQLServer:Select top 10 * from T_Table

MySQL:select * from T_Table limit 0,10

Oracle:select * from T_Table where ROWNUM <=10

我们先来看一下最朴素的做法是怎样的:

首先定义一个SQL语句翻译器类:

public class Test1SQLTranslator

{

private int dbType;

public Test1SQLTranslator(int dbType)

{

super();

this.dbType = dbType;

}

public String translateSelectTop(String tableName, int count)

{

switch (dbType) {

case 0:

return "select top " + count + " * from " + tableName;

case 1:

return "select * from " + tableName + " limit 0," + count;

case 2:

return "select * from " + tableName + " where ROWNUM<=" + count;

default:

return null;

}

}

}

然后如下调用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值