5.4 SAP ABAP 面向对象概念 - 多态 - 摘自 《SAP ABAP面向对象程序设计:原则、模式及实践》

《SAP ABAP面向对象程序设计:原则、模式及实践》

https://book.douban.com/subject/30317853/

http://www.duokan.com/shop/tbt/book/179473

https://item.jd.com/12423999.html

https://e.jd.com/30429611.html

5.4 多态

5.4.1 多态的概述

多态是一个生物学和化学中的专有名词,被面计算机科学家引用到面向对象的程序设计中。

我们用柯林斯英语词典看一下多态一词的原始意义:

polymorphism:n.    多型现象,多态性; 多机组合形式; 多形性;

1. Biology - the occurrence of more than one form of individual in a single species within an interbreeding population

2. The existence or formation of different types of crystal of the same chemical compound

1. 生物学 - 在单一的种群中出现的多种形式的个体的现象,同一物种形态构造表现为多种不同的个体,如蜜蜂的不同个体,有蜂王,雄蜂和工蜂。

2. 化学 - 同素异形体,同一化合物可以形成的不同类型的形态,如碳的同素异形体有金刚石和石墨等。

多态(Polymorphism)的意义是"一个实体同时具有多种形态"。多态是面向对象程序设计(OOP)的一个重要特征。在面向对象的程序中,多态(Polymorphism)指的是对不同的对象执行同一个操作,程序可以有不同的解释,执行不同的逻辑,返回不同的执行结果。

在面向对象的程序中,多态是基于父类和子类的继承,通过指向父类的对象,调用不同子类对象的方法来完成的。

现实中类似例子,如在Windows系统中,F1键被定义为"显示帮助"按钮,但在不同的软件系统中,帮助的显示和弹出方法又都是不同的。

如果我们正在操作的是WPS文字软件,按F1键会在右侧弹出"帮助与问答界面"。如果正在使用Chrome浏览器,按F1键会打开一个新的页面,显示support.google.com页面。如果使用 iTunes软件,按F1键会弹出一个简洁的帮助对话框。这就是同一个操作在不同对象上不同的反应。

多态可以提升代码的可扩展性,采用了基于多态的一些设计模式,可以让我们在少量修改甚至不修改原有代码的基础上,轻松加入新的功能,使代码更加健壮,易于维护。

实现多态有三个前提条件:

  1. 多态发生在有继承关系的类之间(继承的主要目的也是为了多态)
  2. 子类要对类方法进行重写覆盖(重写覆盖就是功能细分的实现)
  3. 类引用指向子类对象(向上转型Upcasting,就是多态的标志)

我们逐一解释着几个条件:

  1. 多态必须要有继承关系

多态是不同的对象执行同一个方法时,具有多个不同表现形式的能力,使用不同的实例,针对同一个方法执行不同的操作。"不同对象具有同一个方法",最为方便的实现方式就是多个类从一个类中继承,子类们可以自动继承父类所定义的公有和受保护的方法。

  1. 子类要对类方法进行覆盖重定义

覆盖就是子类对父类的方法进行修改和覆盖,执行不同的逻辑。

面向对象语言中(如C++)有覆盖和重载两种概念,在ABAP中只有重定义(Redefine)这个概念,相当于覆盖这个概念。

覆盖(Overloading),是指子类重新定义父类的虚方法的做法,ABAP中叫做重新定义(Redefine)。

重载(Overriding),是指同一个类允许存在多个同名方法,而这些方法的参数表不同(参数个数不同,或参数类型不同),而SAP ABAP的同一个类里面,同名方法是不允许出现的,也就不存在重载。

所以,我们ABAP的多态,也就是覆盖和重定义(Overloading/Redefine)这种实现方式。也就是子类对继承得来的方法进行改造,使得该方法符合子类逻辑的过程。

  1. 子类要向上转型为父类对象,执行多态。

    子类对象赋值给基类对象,把子类对象作为基类对象使用,称为(Upcasting)"向上转型"。向上转型就是将基类类型变量指向子类的对象的过程,使得类可以访问子类方法,但基类对象不能访问子类对象新增加的属性和方法。

向上转型的方法如下:

设定类对象为go_superj,子类对象为go_sub。

  1. 如果类是可创建对象实例的类,或者抽象类和接口,可以按子类类型创建类对象,如:

DATA: go_super TYPE REF TO zcl_super.

CREATE OBJECT go_super TYPE zcl_sub.

  1. 如果类是可创建对象实例的类,分别创建子类和类对象后,采用一般赋值语句"=", 如:go_super = go_sub.
  2. 如果类是可创建对象实例的类,分别创建子类和类对象后,可以使用MOVE TO:MOVE go_sub TO go_super.

举个现实中的例子,我们常见的"水果",可以定义为父类,其子类就有苹果类,梨子类等。

如"苹果"是可以向上转型为"水果"的,因为苹果就是一种水果,只要对象符合"IS-A"的关系(apple is a kind of fruit),就可以向上转型。

比如我拿着一只食堂领的苹果,问同事"看我的水果如何?"(这个问题就是将具体的"苹果"上转型为抽象的概念"水果"了),他们会漫不经心地回答,挺新鲜的。(苹果是一种水果,上转型是有意义的。如果我拿着一只梨子问同样的问题,效果也是一样的,符合IS-A的关系的上转型都是有意义的)。

如果我拿一部iPhone手机,问同事"看我的水果如何?",大家会觉得我在说冷笑话。(有人叫苹果公司的iPhone为水果机,但毕竟iPhone不是水果,不符合IS-A的关系,此时上转型是无意义的,所以会让人无法理解)。并且,试图对非继承关系的类进行强制转型也会引起"The type of "GO_XXX" cannot be converted to the type of "GO_XXX" 的转型错误。

如示例程序5.14所示,向上转型可以实现如下。

"示例程序5.14

REPORT zrep_cls_0151.

"
类水果,从ABAPobject继承
CLASS
zcl_fruit DEFINITION INHERITING FROM object.
ENDCLASS.
"
子类苹果,继承水果类
CLASS
zcl_apple DEFINITION INHERITING FROM zcl_fruit.
ENDCLASS.
"
子类梨子,继承水果类
CLASS
zcl_pear DEFINITION INHERITING FROM zcl_fruit.
ENDCLASS.
"
定义类对象
DATA go_fruit TYPE REF TO zcl_fruit.
DATA go_apple TYPE REF TO zcl_apple.
DATA go_pear TYPE REF TO zcl_pear.

"
1种上转型,按子类类型创建类对象
"
类是可创建对象实例的类,或者抽象类和接口
CREATE OBJECT go_fruit TYPE zcl_apple.
"
或者使用NEW语句
go_fruit = NEW zcl_apple( ).

"
2种上转型,如果类是可创建对象实例的类,
"
分别创建子类和类对象后,采用一般赋值语句
CREATE OBJECT go_fruit.
CREATE OBJECT go_apple.
go_fruit = go_apple.

"
3种上转型,如果类是可创建对象实例的类,
"
分别创建子类和类对象后,采用MOVE TO 语句
CREATE OBJECT go_fruit.
CREATE OBJECT go_apple.
MOVE go_apple TO go_fruit.

 

  

 

此外,还有一个概念是向下转型,就是类对象强制赋值给子类对象,把类对象当做子类对象来使用称为"强制向下转型"(downcasting)。向下转型(downcasting),主要目的是为了能够让父类对象调用子类特定的方法。

向上转型是从子转型到类,而向下转型是父类转型到子类。上转型是任何时候都允许进行的,而向下转型则会有类型检查,如果不匹配会报出类型错误。

 

向下转型的方法如下:

 

设定类对象为go_super,子类对象为go_sub,子类的类为zcl_sub。

  1. 如果子类经过向上转型到类,则类对象可以直接向下转型。

    向下转型,采用 "?="进行赋值,要将类对象赋值给子类对象,这个转换是强制的,lo_sub ?= lo_super.

 

  1. 向下转型的另一种格式是MOVE ?TO,需要在TO之前加一个问号,以示强制转型: MOVE lo_super ?TO lo_sub.

 

  1. 也可采用SAP的关键字CAST进行向下转型,格式为"CAST 子类类型(类对象)",如:CAST zcl_sub( go_super ).

     

  2. 向下转型前,如果类对象没有经过子类的向上转型,直接进行向下转型,会引发错误。

 

可以先向上转型,如果类是不能创建实例的抽象类或接口,可以参照向上转型的第一种方法,将对应的父类对象创建时采用子类类型创建。

如:CREATE OBJECT lo_super TYPE zcl_sub_class.

然后再进行下转型。

 

试图用类型创建出子类型对象的"向下转型"是不可行的,如:CREATE OBJECT go_apple TYPE zcl_fruit.

会报出"The specified type cannot be converted into the target variables."错误。

 

如示例程序5.15所示,下转型是需要类型验证的,如"苹果"和"梨子"上转型为"水果"后,可以下转型为"苹果"或者"梨子"。在ABAP 7.5 版本后,可以使用 IS INSTANCE OF 操作符来判断具体的类对象的类型,对具体类型进行验证。

"示例程序5.15

REPORT zrep_cls_015.
"
定义素食类
CLASS
zcl_vegetarian_food DEFINITION.
ENDCLASS.
"
定义水果类,继承素食类
CLASS
zcl_fruit DEFINITION INHERITING FROM zcl_vegetarian_food.
ENDCLASS.

"
定义苹果类,继承水果类
CLASS
zcl_apple DEFINITION INHERITING FROM zcl_fruit.
ENDCLASS.

"
定义梨子类,继承水果类
CLASS
zcl_pear DEFINITION INHERITING FROM zcl_fruit.
ENDCLASS.
"
定义类变量
DATA go_vegetarian_food TYPE REF TO zcl_vegetarian_food.
DATA go_fruit TYPE REF TO zcl_fruit.
DATA go_apple TYPE REF TO zcl_apple.
DATA go_pear TYPE REF TO zcl_fruit.


"
1种向下转型,如果子类经过向上转型到类,则类对象可以直接向下转型。
"
向下转型,采用 "?="进行赋值

"
先创建对象
CREATE OBJECT go_vegetarian_food TYPE zcl_vegetarian_food .
CREATE OBJECT go_apple TYPE zcl_apple.

"
向上转型
go_vegetarian_food = go_apple.

"
然后用"?="下转型
TRY.
go_apple ?= go_vegetarian_food.
WRITE: / 'go_apple ?= go_vegetarian_food - Correct'.
CATCH cx_sy_move_cast_error.
WRITE: / 'go_apple ?= go_vegetarian_food - Error'.
ENDTRY.

"
2种向下转型,如果子类经过向上转型到类,则类对象可以直接向下转型
"
向下转型,采用 "MOVE TO"进行赋值

"
先创建对象
CREATE OBJECT go_vegetarian_food TYPE zcl_vegetarian_food .
CREATE OBJECT go_pear TYPE zcl_pear.

"
向上转型
go_vegetarian_food = go_pear.

"
然后用"MOVE TO"下转型
TRY.
MOVE go_vegetarian_food ?TO go_pear.
WRITE: / 'MOVE go_vegetarian_food ?TO go_pear - Correct'.
CATCH cx_sy_move_cast_error.
WRITE: / 'MOVE go_vegetarian_food ?TO go_pear - Error'.
ENDTRY.

"
3种向下转型,如果子类经过向上转型到类,则类对象可以直接向下转型
"
向下转型,采用 "CAST"进行赋值

"
先创建对象
CREATE OBJECT go_vegetarian_food TYPE zcl_vegetarian_food.
CREATE OBJECT go_pear TYPE zcl_pear.

"
向上转型
go_vegetarian_food = go_pear.

"
然后用"CAST"下转型
TRY.
CAST zcl_pear( go_vegetarian_food ).
WRITE: / 'CAST zcl_pear( go_vegetarian_food ) - Correct'.
CATCH cx_sy_move_cast_error.
WRITE: / 'CAST zcl_pear( go_vegetarian_food ) - Error'.
ENDTRY.


"
4种向下转型,如果类对象没有经过子类的向上转型,直接进行向下转型,会引发错误。

"
先创建对象
CREATE OBJECT go_vegetarian_food TYPE zcl_vegetarian_food.
CREATE OBJECT go_pear TYPE zcl_pear.

"
没有经过子类的向上转型
"go_vegetarian_food = go_pear.

"
此时"CAST"下转型,会引发错误。
TRY.
CAST zcl_pear( go_vegetarian_food ).
WRITE: / 'CAST zcl_pear( go_vegetarian_food ) - Correct'.
CATCH cx_sy_move_cast_error.
WRITE: / 'CAST zcl_pear( go_vegetarian_food ) - Error'.
ENDTRY.

  

 

如图5-38所示,前三个下转型都可以成功进行,但最后一个不经过上转型而直接进行下转型的例子会返回异常Exception。

 

 

这里总结一下对象的转型,转型是指存在继承关系的对象,进行对象间相互赋值操作来进行类型转换的过程,转型不是任意类型的对象都可以进行的,特指子类和类之间的转换。

对象的转型有:

  1. 子类对象赋值给基类对象,把子类对象作为基类对象使用,称为(upcasting)"向上转型"。

    向上转型(upcasting),基类类型变量指向子类的对象,使得类可以访问子类方法,但基类对象不能访问子类对象新增加的属性和方法。

     

  2. 基类对象强制赋值给子类对象,把基类对象当做子类对象来使用称为(downcasting)。

向下转型有两种类型:

自动向下转型:在多态时,先向上转型,类指向子类,调用类方法时,系统是自动向下转型的,目的是调用子类重定义(Redefine)的方法的逻辑,但并不能访问子类的特有的属性和方法。

向下转型:主要目的是为了能够让父类对象调用子类特定的方法。需要用 ?= 操作符显示的强制转型。

 

上转型就是特殊到一般,将具体的事物转述为抽象(我们可以称"桃子"为"水果");

下转型就是一般到特殊,将抽象的事物变回具体(问是具体是什么水果?回答是桃子);

上转型是为了找共同点,下转型是为了找个体的具体的特点。

 

5.4.2 多态的实现(基于非抽象类)

我们用一个实例来展示基于非抽象类的多态,工厂中的物料有很多种类型,我们要用ABAP OOP编程,为每一物料根据物料类型打印对应的物料检验说明书,物料类型不同,对打印的要求不同。如图5-39所示,不同的物料类型,要求打印不同的物料描述用于化验室使用。

 

 

 

 

如图5-40所示,在我们这个例子中,在这里我们采用多态,类用于定义共有的行为方法,而子类根据自身的物料类型不同,设定子类特定的打印规则,定义一个ZCL_SUPER_MATL非抽象类,然后派生出"原料"子类ZCL_SUB_MATL_RAW,"半成品"子类ZCL_SUB_MATL_SEMI,"成品"子类ZCL_SUB_MATL_FIN。

 

 

如图5-41所示,首先创建父类ZCL_SUPER_MATL,为非抽象Public类。

 

如图5-42所示,为父ZCL_SUPER_MATL设定属性MV_MATERIAL_ID,代表物料号码。

 

 

如图5-43所示,为父ZCL_SUPER_MATL设定方法PRINT_MATERIAL_DESC,用于打印物料信息。

 

 

如图5-44所示,编辑非抽象方法PRINT_MATERIAL_DESC的代码,打印属性物料号码MV_MATERIAL_ID。

 

 

 

如示例程序5.16所示,编辑非抽象方法PRINT_MATERIAL_DESC的代码如下。

"示例程序5.16

METHOD print_material_desc.
WRITE: / '
物料号码 ', mv_material_id.
ENDMETHOD.

 

 

 

 

 

 

如示例程序5.17所示,父类ZCL_SUPER_MATL的代码定义如下。

"示例程序5.17

class ZCL_SUPER_MATL definition
public
create public .

public section.

data MV_MATERIAL_ID type STRING .

methods PRINT_MATERIAL_DESC .
protected section.
private section.
ENDCLASS.

CLASS ZCL_SUPER_MATL IMPLEMENTATION.

METHOD print_material_desc.
WRITE: / '
物料号码 ', mv_material_id.
ENDMETHOD.
ENDCLASS.

 

 

  

 

如图5-45所示,创建"原料"子类ZCL_SUB_MATL_RAW,在继承设置中,设定类为ZCL_SUPER_MATL。

 

 

如图5-46所示,创建"原料"子类ZCL_SUB_MATL_RAW,继承了父类的方法PRINT_MATERIAL_DESC,重新定义该方法。

 

 

如图5-47所示,在重新定义的方法PRINT_MATERIAL_DESC中,调用父类的打印功能(用"super->"来进行调用类方法)打印物料号,添加打印业务对原料所需的特有的打印数据。

这些数据用字符串和当前日期模拟替代,在真实业务中可以从不同的表中获取数据。

 

 

 

 

如示例程序5.18所示,编辑非抽象方法PRINT_MATERIAL_DESC的代码如下。

"示例程序5.18

METHOD print_material_desc.
CALL METHOD super->print_material_desc.
WRITE: /'
供应商号码 ', 'VEN0127'.
WRITE: /'
批次号码 ', 'MF01RAW209'.
WRITE: /'
采购日期 ', sy-datum.
ENDMETHOD.

 

 

 

 

 

 

 

如示例程序5.19所示,子类ZCL_SUB_MATL_RAW的代码定义如下。

"示例程序5.19

CLASS zcl_sub_matl_raw DEFINITION
PUBLIC
INHERITING FROM zcl_super_matl
FINAL
CREATE PUBLIC .

PUBLIC SECTION.

METHODS print_material_desc
REDEFINITION .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.

CLASS zcl_sub_matl_raw IMPLEMENTATION.

METHOD print_material_desc.
CALL METHOD super->print_material_desc.
WRITE: /'
供应商号码 ', 'VEN0127'.
WRITE: /'
批次号码 ', 'MF01RAW209'.
WRITE: /'
采购日期 ', sy-datum.
ENDMETHOD.
ENDCLASS.

 

  

 

如图5-48所示,创建"半成品"子类ZCL_SUB_MATL_SEMI,在继承设置中,设定类为ZCL_SUPER_MATL。

 

 

如图5-49所示,创建"半成品"子类ZCL_SUB_MATL_SEMI,继承了父类的方法PRINT_MATERIAL_DESC,重新定义该方法。

 

 

 

如图5-50所示,重新定义方法PRINT_MATERIAL_DESC中,调用父类的打印功能(用"super->"来进行调用类方法)打印物料号,添加打印业务对半成品所需的特有的打印数据。

这些数据用字符串和当前日期模拟替代,在真实业务中可以从不同的表中获取数据。

 

 

如示例程序5.20所示,编辑非抽象方法PRINT_MATERIAL_DESC的代码如下。

"示例程序5.20

METHOD print_material_desc.
CALL METHOD super->print_material_desc.
WRITE: /'
批次号码 ', 'MF01SEM537'.
WRITE: /'
重检日期 ', sy-datum.
ENDMETHOD.

 

 

 

 

 

 

 

 

如示例程序5.21所示,"半成品"子类ZCL_SUB_MATL_SEMI的代码定义如下。

"示例程序5.21

CLASS zcl_sub_matl_semi DEFINITION
PUBLIC
INHERITING FROM zcl_super_matl
FINAL
CREATE PUBLIC .

PUBLIC SECTION.

METHODS print_material_desc
REDEFINITION .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.

CLASS zcl_sub_matl_semi IMPLEMENTATION.

METHOD print_material_desc.
CALL METHOD super->print_material_desc.
WRITE: /'
批次号码 ', 'MF01SEM537'.
WRITE: /'
重检日期 ', sy-datum.
ENDMETHOD.
ENDCLASS.

 

 

如图5-51所示,创建"成品"子类ZCL_SUB_MATL_FIN,在继承设置中,设定类为ZCL_SUPER_MATL。

 

 

如图5-52所示,"成品"子类ZCL_SUB_MATL_FIN,继承了父类的方法PRINT_MATERIAL_DESC,在子类中重新定义该方法。

 

如图5-53所示,重新定义方法PRINT_MATERIAL_DESC中,调用父类的打印功能(用"super->"来进行调用类方法)打印物料号,添加打印业务对成品所需的特有的打印数据。

这些数据用字符串和当前日期模拟替代,在真实业务中可以从不同的表中获取数据。

 

 

如示例程序5.22所示,编辑非抽象方法PRINT_MATERIAL_DESC的代码如下。

"示例程序5.22

METHOD print_material_desc.
CALL METHOD super->print_material_desc.
WRITE: /'
批次号码 ' , 'MF01FIN001'.
WRITE: /'
生产日期 ', SY-DATUM.
ENDMETHOD.

  

 

如图5-54所示,"成品"子类ZCL_SUB_MATL_FIN,新定义一个新方法PRINT_MATERIAL_TYPE,在子类中定义该方法。

 

 

如图5-55所示,在方法PRINT_MATERIAL_TYPE中,添加打印物料类型的代码。

这些数据用字符串和当前日期模拟替代,在真实业务中可以从不同的表中获取数据。

 

 

 

如示例程序5.23所示,编辑非抽象方法PRINT_MATERIAL_TYPE的代码如下。

"示例程序5.23

METHOD print_material_type.
WRITE: /'
物料类型 ' , 'FIN'.
ENDMETHOD.

  

 

如示例程序5.24所示,"成品"子类ZCL_SUB_MATL_FIN的代码形式定义如下:

"示例程序5.24

CLASS zcl_sub_matl_fin DEFINITION
PUBLIC
INHERITING FROM zcl_super_matl
CREATE PUBLIC .

PUBLIC SECTION.

METHODS print_material_type .

METHODS print_material_desc
REDEFINITION .
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.

CLASS zcl_sub_matl_fin IMPLEMENTATION.

METHOD print_material_desc.
CALL METHOD super->print_material_desc.
WRITE: /'
批次号码 ' , 'MF01FIN001'.
WRITE: /'
生产日期 ', sy-datum.
ENDMETHOD.

METHOD print_material_type.
WRITE: /'
物料类型 ' , 'FIN'.
ENDMETHOD.
ENDCLASS.

  

如示例程序5.25所示,为多态创建测试代码,定义父类和子类,然后创建对象,设定物料号码,然后进行多态的向上转型,测试多态。

"示例程序5.25

REPORT zrep_cls_013.
"
定义类对
DATA go_super_matl TYPE REF TO zcl_super_matl.
DATA go_sub_matl_raw TYPE REF TO zcl_sub_matl_raw.
DATA go_sub_matl_semi TYPE REF TO zcl_sub_matl_semi.
DATA go_sub_matl_fin TYPE REF TO zcl_sub_matl_fin.

"
创建类对象
CREATE OBJECT go_super_matl.
CREATE OBJECT go_sub_matl_raw.
CREATE OBJECT go_sub_matl_semi.
CREATE OBJECT go_sub_matl_fin.

"
为各个类对象设定物料号码
go_super_matl->mv_material_id = 'MATL001'.
go_sub_matl_raw->mv_material_id = 'RAW001'.
go_sub_matl_semi->mv_material_id = 'SEMI001'.
go_sub_matl_fin->mv_material_id = 'FIN001'.

"
测试物料
WRITE: / '
物料:'.
go_super_matl->print_material_desc( ).

"
向上转型,测试多态下的原料
WRITE: /,/ '
原料:'.
go_super_matl = go_sub_matl_raw.
go_super_matl->print_material_desc( ).

"
向上转型,测试多态下的半成品类
WRITE: /,/ '
半成品:'.
go_super_matl = go_sub_matl_semi.
go_super_matl->print_material_desc( ).

"
向上转型,测试多态下的成品类
WRITE: /,/ '
成品:'.
go_super_matl = go_sub_matl_fin.
go_super_matl->print_material_desc( ).

"
向下转型,测试成品类自身方法
WRITE: /,/ '
成品:'.
go_sub_matl_fin ?= go_super_matl.
go_sub_matl_fin->print_material_desc( ).
go_sub_matl_fin->print_material_type( ).

  

如图5-56所示,代码结果如下,其中最后点的物料类型,为下转型后才可以调用的子类特有的方法。

 

 

 

转型对象具有如下特点:

  1. 向上转型对象可以访问子类继承的属性,也可以使用子类继承的或重写的方法,这是向上转型的目的所在。
  2. 向上转型对象操作子类继承或重写的方法时,是按照子类对象去调用这些方法的,此时,如果子类重写了父类的该方法后,程序调用的是子类重写的方法,而不是类原来定义的方法。这是下一章多态的具体体现。
  3. 向上转型对象不能直接操作子类新增的属性(转型中会失掉了这部分属性的定义,也不能直接使用子类新增的方法(失掉了一些特定的功能),这是向上转型的代价。
可以将对象的上转型对象再强制转换到一个子类的对象(强制向下转型),这时,该子类对象又具备了子类的所有属性和功能。

 

 

向下转型,应该会有同学觉得是多此一举:要使用子类的对象的方法,先是创建子类对象,然后向上转型为父类对象,然后又将类对象向下转型子类对象,我不向上转型,也不向下转型,直接用子类对象不就行了吗?

其实,向上转型和向下转型是为了实现多态,而多态是面向对象的重要规则,可以实现很多设计功能。

后面章节会介绍设计模式,都是依照这些基本的面向对象规则而建立的有效的面向对象设计方法和模式。

 

 

5.4.3 多态的实现(基于抽象类)

前面说明,实现多态有三个前提条件:

  1. 多态发生在有继承关系的类之间(继承的主要目的也是为了多态)
  2. 子类要对类方法进行重写覆盖(重写覆盖就是功能细分的实现)
  3. 类引用指向子类对象(向上转型就是多态的标志)

 

前提中并没有提到抽象类的概念,但在面向对象程序设计的实践中,为了能满足一些设计原则(里氏替换原则,后面的章节会介绍)我们使用多态时,总是要将类定义为抽象类,然后再用子类继承抽象的父类。这里我们说明一下抽象类的概念。

 

抽象类就是不能用来创建对象实例的类,也就是不能实例化的类,现实中的例子有很多,比如我们所说的"水果",世上没有一种水果叫"水果",要么叫苹果,要么叫桃子等等。"水果"就是一个不能直接实例化的抽象概念,我们可以将"水果"定义为抽象类,定义所有水果共有的一些属性和方法,而苹果,桃子是"水果"这个抽象类的可实例化的非抽象的子类。

 

面向对象中,抽象类的定义为"包含抽象方法的类",这里所谓的抽象方法,就是用abstract修饰的,只能定义方法名称,参数,而没有任何实现代码的"空方法"。这种包含抽象方法的抽象类就是用于继承用的,本身不能实例化,通过继承和覆盖方法的子类的实例对象来表现业务逻辑。

 

当然如果一个类不包含抽象方法,只是用abstract修饰类本身的话,这个类也是一个抽象类,这种抽象类一般是不是用于继承的,而是一些不必要实例化的计算类,或者用于创建别的类实例的中间类,类中的方法用static标识为静态方法,外部程序用调用静态方法的方式用类的类型调用方法获得计算结果。

 

我们用一个例子来说明基于抽象类的继承,大家熟知花木兰替父从军的故事:"阿爷无大儿,木兰无长兄,愿为市鞍马,从此替爷征"。我们用这个例子来实现抽象类和多态。

 

设计分析多态的情况时,我们要设定类之间的父类和子类继承关系,那么花木兰的父亲(名曰花弧)和花木兰之间的类关系应该如何设计呢?

有同学可能会说:花是花木兰的父亲,那自然花就应该是类,然后花木兰是继承父一些能力的子类啦。——其实不是的,和你的直觉相反,这对父女不仅不是父类和子类的关系,还应该是平等的同一层级的类关系(生活中你的直觉往往是对的,而科学或技术中的最大的障碍可能就是我们的直觉),前面讨论过,面向对象的程序设计和生物遗传的继承中还是不太一样的,面向对象的程序设计里的继承一定要符合"IS-A"关系,也就是说,当B的每个实例都可以被看做A的实例时,才适合定义类B继承类A。

换句话说,如果我们能用自然语言描述说"每一个B都是A",或者"B是A中的一个",那么就可以用B来继承类A",显然"花木兰都是花"或者"花木兰是花中的一个",这种描述是说不通的。

所以,我们应该引用一个新的类,因为他们生活在南北朝的北魏时期,并且尚武彪悍,全民皆兵。我们就叫这个类为"北朝军民"吧。

严格说来,花和花木兰其实都应该是这个类"北朝军民"的一个子类(如骑兵)的不同的对象实例(比如他们的名字就是子类的一个属性,用于标识自己的对象实例),但此例中,我们强调的是整个花木兰的故事情节,所以我们将花和花木兰都分别定义为"北朝军民"的一个子类。

同样,我们用自然语言描述说"花木兰是北朝军民中的一个",或者"花是北朝军民中的一个",是说的通的。

对于类"北朝军民"这个概念,当然不会有一个实际的人物就叫做"北朝军民",所以这个类是没有直接的实例的,也就是应该被定义为不能实例化的抽象类。所以我们设定"北朝军民"为抽象类,再设定花木兰类和花类作为"北朝军民"的非抽象的子类,作为可以实例化的人物的子类。

 

如图5-57所示,首先创建抽象的父类, ZCL_BEICHAO_CIVILIAN,其中Final不可选中,否则不可以被继承,选择Instantiation(实例化类型)为"Abstract",即设定为抽象类。

 

 

如图5-58所示,定义父类的属性,属性都是外部只读,防止外部程序不经类方法直接修改对象属性,具体属性如表5-8所示。

 

类属性Attribute

级别

Level

可见性

Visibility

外部只读Read-Only

对应类型

Associated type

MV_NAME

Instance Attribute

Public

Yes

STRING

MV_GENDER

Instance Attribute

Public

Yes

STRING

 

 

 

 

 

如图5-59所示,定义父类的方法,包括两个有代码的非抽象方法SETUP_PROFILE,SELF_INTRODUCE,和一个不能写入代码的抽象方法SHOW_CAPABILITY。

父类的方法的可见性都为Public。本例中不包含静态方法,因为静态方法不能访问非静态属性,并且不可以在子类中被重新定义,在本例中的作用不明显,具体方法如表5-9所示。

 

类方法Method

可见性Visibility

抽象方法Abstract

解释Description

SETUP_PROFILE 

Public

No

Setup Profile 

SHOW_CAPABILITY 

Public

Yes

Show Capability 

SELF_INTRODUCE 

Public

No

Self Introduce 

 

 

 

 

如图5-60所示,编辑非抽象方法SETUP_PROFILE的代码,设定两个传入的参数,用于设定类的姓名和性别。

 

"示例程序5.26

METHOD setup_profile.
mv_name = iv_name.
mv_gender = iv_gender.
ENDMETHOD.

 

 

 

 

代码如示例程序5.26所示。

 

如图5-61所示,编辑非抽象方法SELF_INTRODUCE的代码,用于类自我介绍。

 

 

 

代码如示例程序5.27所示。

"示例程序5.27

METHOD self_introduce.
WRITE:/ '
我是 ', mv_name, '性别 ', mv_gender.
ENDMETHOD.

 

 

"北朝军民"中,上两个非抽象方法设定属性和自我介绍,是每个军民都通用的,而用于能力展示的方法SHOW_CAPABILITY则不然,因为每一个军民都有自己比较独特的能力,所以类只能声明方法,不在类中直接定义方法的代码。

如图5-62所示,定义父类的方法SHOW_CAPABILITY为抽象方法,(其实此时编译类,类如果当时设定的Instantiation(实例化类型)不是"Abstract",系统也会强制变换为"Abstract",因为一个类一旦含有抽象方法,那么这个类就会自动设定为抽象类。一旦设定为抽象类,该类就不能实例化成为对象)。

选择方法为"Abstract"后,系统会提示"Implementation of method CAPABILITY deleted",表明该方法不可以有任何实现代码,只能对方法名称,参数等进行声明。点击该方法的"代码"Code按钮,也只会返回"Method CAPABILITY is abstract",而不能进行编码。

 

 

 

 

 

 

如图5-63所示,如果试图为抽象方法SHOW_CAPABILITY设定代码,系统会返回提示,"Method SHOW_CAPABILITY is abstract",不允许进行代码编辑。

 

 

 

下面进行子类的定义:

如图5-64所示,创建"ZCL_HUAHU"花,点击按钮"Superclass",设定类。

 

如图5-65所示,设定类为"北朝军民"类ZCL_BEICHAO_CIVILIAN。

 

 

如图5-66所示,类花弧,继承了北朝军民类的属性和方法,然后对其中的继承的抽象方法SHOW_CAPABILITY进行重新定义,因为是对抽象方法进行重定义,点击重新定义按钮(Redefine)后,可不经提示就进入代码编辑环境。

 

 

如图5-67所示,花弧的能力是"年老体衰,耕读持家",他是不能承担艰苦的作战任务的。

代码如示例程序5.28所示。

"示例程序5.28

METHOD show_capability.
WRITE: / '
年老体衰,耕读持家'.
ENDMETHOD.

 

  

 

如图5-68所示,创建"ZCL_HUAMULAN"花木兰类,同样设定类为"北朝军民"类ZCL_BEICHAO_CIVILIAN。

 

 

 

如图5-69所示,花木兰,继承了北朝军民类的属性和方法,然后对其中的继承的抽象方法SHOW_CAPABILITY进行重新定义。

 

 

 

如图5-70所示,花木兰的能力是"年轻力壮,冲锋陷阵"。

 

"示例程序5.29

METHOD show_capability.
WRITE: / '
年轻力壮,冲锋陷阵'.
ENDMETHOD.

  

代码如示例程序5.29所示。

 

如图5-71所示,花木兰虽然武艺在身,但依然是女孩子,具有特殊的能力"梳妆打扮",再为花木兰添加一个特有的方法MAKE_UP。

 

 

 

"示例程序5.30

METHOD make_up.
WRITE: / '
当窗理云鬓,对镜贴花黄'.
ENDMETHOD.

 

 

 

 

代码如示例程序5.30所示。

 

和花木兰两个实例分别创建,各自测试两个子类的展示能力时各自展示的能力都是什么,花无法作战,花木兰有作战能力,但却是个女孩子,并且有女孩子特有的"梳妆打扮"的能力。

类的Code Based代码示例程序5.31。

"示例程序5.31

为了节约篇幅,类与子类的全局code based 代码请参照Github代码5.31

<https://github.com/ABAPOOP/ABAP_OOP_SAMPLE/blob/master/ABAP_OOP_Sample_5.31.txt >

 

 

 

 

下面用抽象类的多态来展示故事情节:

 

一,"唧唧复唧唧,木兰当户织。"花木兰的对象实例创建,设定属性并展示的能力。花木兰的自我介绍是"我是花木兰,性别女,年轻力壮,可以冲锋陷阵,还可以当窗理云鬓,对镜贴花黄"。

 

二,花弧的对象实例创建,设定属性并展示的能力。花弧的自我介绍是"我是花,性别男,年老体衰,耕读持家"。

 

三,"昨夜见军帖,可汗大点兵,军书十二卷,卷卷有爷名",书上被点兵参军的是年迈的父亲花弧

创建基于花弧类的"北朝军民"的实例对象,相当于向上转型,测试多态下弧的能力,如果"北朝军民"花在军中报名,调用的是父类的自我介绍:我是花,男,如果花在军中作战展示能力的话就是开始展示多态,就会发现,花不能负担作战任务,只能白白牺牲。

 

四,木兰决定代父从军。基于第一步创建好的花木兰的"北朝军民"的实例对象,让子类花木兰声称自己就是花,以父亲的名义参军 (这里还不是多态,是继承得来的方法,但参数不同而已)。

 

五,然后将花木兰对象实例向上转型,由"北朝军民"这个类型进行多态调用,"北朝军民"花木兰是以弧的名义在参军,其实是花木兰女扮男装代父从军。花木兰在军中报名,调用的是继承得来的自我介绍方法,参数是修改后的:"我是花,男。"而打仗的时候,就展示出了多态,其实上阵打仗的是花木兰,打仗的功夫也是花木兰自己的能力,而不是花弧的能力。而"北朝军民"调用花木兰自己的特有方法"梳妆打扮"时,系统在编译阶段就会阻止,因为类不能直接访问子类新增的特有的方法,这就是多态的代价,不能调用子类新增加的方法,并且军中也不允许花木兰使用自己新增加的方法(多态状态下是阻止子类的新增方法调用的)。

 

六,"壮士十年归",花木兰告别了战争生活。"愿驰千里足,送儿还故乡"花木兰回家与家人团聚。

花木兰重新声明自己其实是女儿的花木兰,然后"北朝军民"对象向下转型到子类对象, 向下转型(向下转型不是多态),花木兰重新做自己,回家继续过女儿的生活,自我介绍用的是"我是花木兰,性别",花木兰依然具有能冲锋打仗的能力, 如有战事依然能够上阵。向下转型后,花木兰即可以重新调用自己新增加的方法,也就是终于可以继续使用自己的"梳妆打扮"能力:"当窗理云鬓,对镜贴花黄"。

调用程序如示例程序5.32所示。

"示例程序5.32

REPORT zrep_cls_012.
DATA :
"declare super class
go_civi_soldier TYPE REF TO zcl_beichao_civilian,
"declare sub class
go_hua_hu TYPE REF TO zcl_huahu,
"declare sub class
go_hua_mulan TYPE REF TO zcl_huamulan.

START-OF-SELECTION.

"1.
创建花木兰的对象实例
CREATE OBJECT go_hua_mulan.

WRITE : /, / '1.
测试子类对象花木兰:'.
"
自我介绍用的是花木兰的信息,我是花木兰,女
CALL METHOD go_hua_mulan->setup_profile
EXPORTING
iv_name = '
花木兰'
iv_gender = '
'.

CALL METHOD go_hua_mulan->self_introduce.
"
北朝人尚武,女孩子也会武术
CALL METHOD go_hua_mulan->show_capability.
"
当然女孩也能化妆打扮
CALL METHOD go_hua_mulan->make_up.

"2.
创建弧的对象实例
CREATE OBJECT go_hua_hu.
WRITE : /, / '2.
测试子类对象花弧:'.
"
调用的是父类的自我介绍:我是,男

CALL METHOD go_hua_hu->setup_profile
EXPORTING
iv_name = '
花弧'
iv_gender = '
'.

CALL METHOD go_hua_hu->self_introduce.
"
无奈年老体衰,力不从心
CALL METHOD go_hua_hu->show_capability.
"
类没有子类的打扮能力
"CALL METHOD go_hua_hu->make_up.

"3.
测试多态下的花弧的能力
WRITE : /, / '3.
测试子类对象向上转型到类对象,多态 - 父亲无法打仗:'.

"
类是虚拟类,不能直接创建对象, 但可以依据实现所有方法的子类类型创建对象
"CREATE OBJECT go_civi_soldier TYPE zcl_huahu.

"
虚拟类对象变量不经创建,直接由子类对象赋值也可以
go_civi_soldier = go_hua_hu.

"
向上转型,对象是go_civi_soldier
"
此时是查看子类对象弧的能力
go_civi_soldier = go_hua_hu.

"
在军中报名: 调用的是父类的自我介绍:我是花,男
CALL METHOD go_civi_soldier->self_introduce.
"
如果在军中打仗: 花弧不能负担作战任务,只能充当牺牲品
CALL METHOD go_civi_soldier->show_capability.

"4.
木兰决定代父从军
WRITE : /, / '4.
木兰决定代父从军'.
"
子类花木兰声称自己就是,准备以父亲的名义参军
CALL METHOD go_hua_mulan->setup_profile
EXPORTING
iv_name = '
花弧'
iv_gender = '
'.

" 5.
将花木兰对象实例向上转型
WRITE : /, / '5.
测试子类对象向上转型到类对象,多态 - 代父从军:'.
"
向上转型,对象是go_civi_soldier,名义上是花在参军,其实是花木兰女扮男装代父从军
"
向上转型后,对象go_hua_mulango_civi_soldier是指向同一个内存区域
go_civi_soldier = go_hua_mulan.

"
在军中报名: 调用的是重新声明过的自我介绍:我是花,男
CALL METHOD go_civi_soldier->self_introduce.
"
在军中打仗: 打仗的时候,其实上阵打仗的是花木兰,打仗的功夫也是花木兰自己的能力
CALL METHOD go_civi_soldier->show_capability.
"
多态的代价,不能调用子类新增加的方法,军中也不允许花木兰使用自己新增加的方法
"CALL METHOD go_civi_soldier->make_up.


" 6.
花木兰回乡团聚
WRITE : / , /'6.
测试类对象向下转型到子类对象, 花木兰重新做自己:'.
"
不必按子类类型重新创建类对象, 因为子类曾经向上转型,向下转型可以直接进行
"
向下转型,花木兰自己回家重新过女儿的生活
go_hua_mulan ?= go_civi_soldier.
"
花木兰回家,重新声明自己其实是女儿的花木兰
CALL METHOD go_hua_mulan->setup_profile
EXPORTING
iv_name = '
花木兰'
iv_gender = '
'.
"
自我介绍用的是十年后的花木兰的信息
CALL METHOD go_hua_mulan->self_introduce.
"
花木兰依然具有能冲锋打仗的能力, 如有战事依然能够上阵
CALL METHOD go_hua_mulan->show_capability.
"
向下转型后,可以重新调用子类新增加的方法
"
花木兰终于可以开始使用自己的化妆打扮的能力
CALL METHOD go_hua_mulan->make_up.

 

 

 

 

 

 

 

 

 

如图5-72所示,代码执行结果如下。

 

测试结果和分析如表5-10所示。

测试内容和结果

解读分析

1. 测试子类对象花木兰:

我是 花木兰 性别 女

年轻力壮,冲锋陷阵

当窗理云鬓,对镜贴花黄

 

子类ZCL_HUAMULAN测试

2. 测试子类对象花弧:

我是 花弧 性别 男

年老体衰,耕读持家

子类ZCL_HUAHU测试

3. 测试子类对象向上转型到类对象,多态 - 父亲无法打仗:

我是 花弧 性别 男

年老体衰,耕读持家

多态测试:

以下两种代码都是有效的上转型:

1.类是虚拟类,不能直接创建对象, 但可以依据实现所有方法的子类类型创建对象。

CREATE OBJECT go_civi_soldier TYPE zcl_huahu.

 

2.虚拟类对象变量不经创建,直接由子类对象赋值也可以

go_civi_soldier TYPE REF TO zcl_beichao_civilian.

go_civi_soldier = go_hua_hu.

4. 木兰决定代父从军

子类对象中用继承的非抽象方法SETUP_PROFILE修改了自身的属性,这不是多态,而是普通的调用。采用的是父类的原始方法逻辑,传入的参数不同而已。

5. 测试子类对象向上转型到类对象,多态 - 代父从军:

我是 花弧 性别 男

年轻力壮,冲锋陷阵

 

多态测试:

类指向了花木兰,打仗的时候,其实上阵打仗的是花木兰,打仗的功夫也是花木兰自己的能力。

 

多态的代价是,不能调用子类新增加的方法,军中也不允许花木兰使用自己新增加的方法。

 

多态状态下是阻止子类的新增方法调用的,除非使用动态调用方法,绕过ABAP编译器的检查。比如,类可以使用:CALL METHOD mo_sub_class->('SUB_CLASS_METHOD').的形式调用子类的公有新增方法(我并不清楚这个是ABAP编译器留出的后门还是漏洞,大家可以验证一下)

6. 测试类对象向下转型到子类对象, 花木兰重新做自己:

我是 花木兰 性别 女

年轻力壮,冲锋陷阵

当窗理云鬓,对镜贴花黄

强制下转型后,可以继续调用子类花木兰的方法。

强制下转型后,可以继续调用子类的新增加方法MAKE_UP。这属于子类的正常调用,不是多态。

 

在使用虚拟类时需要注意几点:

1.抽象类不能被实例化,抽象类是用来约束和定义子类用的,而子类才是实例化后处理业务的骨干。

2.抽象方法是不包含任何代码的,只能定义方法名称和参数,代码必须由子类来进行重写,实现父类的抽象方法。

3.类中只要包含一个抽象方法,该类就必须定义为抽象类,无论该类是否包含有其他非抽象方法。

4.如果类中所有的方法都是非抽象的,该类依然可以被定义为一个抽象类,如不用实例化的一些计算类。

5. 类的实例化类型设定中,Abstract不能与Public,Protected, Private并列修饰同一个类,也就是说这几个是互斥的设定(详见第四章)。

6.类方法的设定中,abstract不能与Private,Static,Final并列修饰同一个方法,也就是说这几个是互斥的设定。

原因1: 抽象方法不应该被定义成私有Private的,因为Private的就无法被子类继承了。编译时也会报错Private methods cannot be redefined, and they may therefore not be declared "ABSTRACT".) 抽象方法就应该是Public或者Protected的可见度,能够让子类继承。所以abstract和private互斥。

原因2:我们知道,非私有的静态方法可以被子类继承,但不可以被子覆盖重写(否则会报错Static methods cannot be redefined),因为静态方法是类和对象共享的,继承后父类和子类也会共享方法的。

同样,抽象方法不应该被定义成静态的(static),因为静态方法本身是无法被子覆盖重写的。即便抽象方法设定为子类无法继承的私有的(private),编译时也会报错You cannot redefine static methods, and they may therefore not be declared "ABSTRACT".)。所以abstract和static互斥。

原因3:抽象方法不能设定为最终方法final,也就是说abstract不能与final并列修饰同一个方法,因为一旦声明为虚拟方法,就是为了子类继承实现的。所以abstract和final互斥。

 

5.4.3 多态的小结

我们通常所说的多态指的都是程序运行时的多态,也就是ABAP代码在编译的时候是不能确定具体调用是哪个子类的方法的,直到运行的时刻才能确定。

 

多态具有以下优点:

  1. 程序的可复用性较好。程序不必为每一个子类对象专门编写调用,只需要通过抽象类调用多态的方法即可。
  2. 可扩充性和可维护性较好。如果业务上有新的业务形式出现,要用新的子类来描述,新增加的子类不会影响已存在的父类和子类的继承和多态。并且新增加的子类可以根据已有的继承和多态规则,能够更加容易地制定,尤其是当增加新的子类时,调用者的代码无需变动或基于既有规则很少地变动就能适用新的子类。比如物料类设定为抽象类,定义了原料,半成品,成品为子类,当一个新的物料类型如"样品类型"出现,我们就再从父类中继承创建一个新类型即可,不必对原料,半成品,成品类型进行修改,调用程序的修改也较少且很有规律。
  3. 多态的语法比较清晰简洁,只要向上转型,对所有子类方法的调用都是由类对象直接调用的,一致而规律。

 

运行时多态通常有两种实现方法:

1.继承多态,子类继承父(extends)

2.接口多态,类实现接口(implements)

本章讲解的就是继承多态,无论是哪种方法,其核心之处就在于对父类方法的改写或对接口方法的实现,以取得在运行时不同的执行效果。

要使用多态,声明时总是对父类类型或者接口类型,调用时创建的对象是实际类型,通过向上转型动态调用。

 

如基于非抽象类的应用,工厂中的物料有很多种类型,我们要用ABAP OOP编程,为每一物料根据物料类型打印对应的物料检验说明书。

如果不采用多态,打印程序就需要对每一种物料都要编辑调用代码,而多态是运行时动态指定子类,只要子类是确定的,通过向上转型和调用固定名称的打印方法PRINT_MATERIAL_DESC,就可以正确地打印各种类型的物料的检验要求,如果新增加了一个物料类型,我们不必大规模地修改调用程序(如果采用合适的设计模式,甚至连调用程序都不用修改),仅仅需要新增加一个类,原料的代码就可以正常工作。

 

这里还有一点要说:前面提过的里氏原则(Liskov Substitution Principle),要求子类避免重写类方法,而多态的条件之一却是要求子类重写父类的方法。那么多态是否违反了里氏原则呢?具体的解释请看第六章的LSP原则。

 

以上几节分别介绍了封装,继承和多态,基本的思路如下:

  • 封装可以隐藏实现细节,使得代码模块化。
  • 继承可以扩展已存在的类,保证代码重用,并且是多态的基础。
  • 而多态则是在继承的基础上,实现父类和子类调用逻辑的重用,也就是业务接口的重用,提高系统的可复用性。
  • 在基于类的多态时,我们推荐使用抽象类作为基类。
  • 多态的真正的威力,请参看第6章第3节开放封闭原则中的示例。

 

猜你喜欢

转载自www.cnblogs.com/techtalk/p/9781188.html