1. 程式人生 > >5.4 SAP ABAP 面向物件概念

5.4 SAP ABAP 面向物件概念

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.

"父類水果,從ABAP根類object繼承
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”判定方法是一定要採用的),由於我們前面討論過,面向物件的程式設計和生物遺傳的繼承中是不一樣的,面向物件的程式設計裡的繼承一定要符合"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.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_mulan和go_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 n