ABAP OO 的八大理由 ( 转 )
ABAP OO 的八大理由 ( 一 )
几年前 SAP BASIS 4.6 为 ABAP 扩展了 OO 功能,这是很多传统的 ABAP 程序员陷入困境。首先对于 ABAP 程序员来说它们终于可以通过 OO 这种开发方法得潜在价值来提高开发效率和所开发程序的可维护性了。然而一直以来习惯于面向过程编程的 ABAPER 迅速的转向 OO 模式也不是那么容易,很多人还是坚持使用传统的面向过程的开发模式,他们觉得不用 OO 也可以得到他们想要的。这篇文章的主要目的便是让你明白 OO 能给你带来什么以及如何转型为 OO 模式。
很多关于 ABAP OO 方面的文章要么过于抽象,要么举的例子过于简单对于实际应用无关痛痒。本篇文章力图通过简单而又切合实际的例子通过面向过程和面向对象两种模式的比较来说明 OO 的优点。
1. 首先 ABAP OO 具有更高层次的数据封装性 ,从而增强了程序的可维护性和稳定性。在面向过程的模式里,一个程序的全局变量区包含着许多不相干的变量,这些变量在一个区域里交织在一起,这样的话这个程序的某个功能对程序状态的改变并不为程序中另外一个功能所知。为保持整个程序的稳定性需要大量的约束和维护成本。而对于 OO 模式,状态是保存在对象上的。对象可以把内部和外部数据和方法分隔开,以保证功能( OO 中的方法)只能改变与它相关的数据,对象的属性是不会被改变的,从而保证了其在应用程序中的稳定性。
2. ABAP OO 可以实现一个类的多个实例。(对象是由数据以及操作数据的方法组成的。),每一个对象都有自己在类中定义的属性值,并且可以通过本身的方法来改变本身的状态。这就意味着开发者无需为每个对象建立数据和方法之间的联系也无需手工管理每个对象的生命周期。 在面向过程的方法中没有多实例的概念,数据和功能是相互分离的。你使用的是无状态的功能,并且每次都需要通过传递参数来初始化它,并且手工将其占有的内存清除。
3. ABAP OBJECT 通过继承进一步增强了程序代码的可重用性,这正是面向对象方法的一个重要方面。通过这个特点你可以重复利用所继承类的部分或者所有方法,只需编写类本身所特有的方法,这样会减少为每个类编写的方法,从而增强了程序的可维护性。而对于面向过程的编程来说,你将会受制于 all-or-nothing 的状况,你要么调用所有的部分要么就建立新的。
4. ABAP OO 是你可以通过接口( interface )来调用对象的业务逻辑,而不是直接去使用对象,这样就避免了你需要详细了解每一个对象的特定功能。这样也避免了你在需要修改某项功能的时候无需修改接口( interface )的内容,这也为标准程序的修改提供了新的途径,即 BADI 。 面向过程的编程并没有这种提供对立的接口去联系某个对象的概念—接口只是通过 form 或 function module 的参数实现的。
5. ABAP OO 非常容易与事件驱动的模式结合在一块。不同的应用之间是通过发布与预定松散的耦合在一起的,调用者和被调用者之间并不是被动的绑定在一起的。这种方式与面向过程的方法相比更进一步的增强了灵活性,他们的绑定是紧密地,程序的执行流程也是预定义好的。
当然,这些好处都需要你今后再开发中是用 OO 的方法来开发才能得到。然而重新建模你应用程序的开发模式是令人沮丧和头疼的,我们保证这种情况不会出现,读完这篇文章我们相信你会得到同样的结论。当然,没有说我们需要重新建模已有的应用,我们只是希望你在今后的开发过程中最后考虑一下 OO 的模式。
如果你坚持认为面向过程的模式对你来说已经足够了,也请接着往下读。通过使用对象的方法来代替 form 和 function module 你仍然可以增强你的 ABAP 程序。
ABAP OO 的八大理由 ( 二 )
1. ABAP OO 更加明确所以更易于使用。例如在使用 ABAP OO 你的程序的执行流程不再是由运行时隐含的控制。这样你就可以自己去设计程序所执行的流程了而不必像面向过程那样去了解和服从外部控制机制(即报表和 dialog screen 的事件)。
2. ABAP OO 具有更加清晰的语法和语义规则,比如一些容易出错的过时的语句在 ABAP OO 类中已经明确不能再使用。而在面向过程的程序中这些语法仍然被支持,顶多就是在关键的时候给你报个警告信息。
3. ABAP 的一些新技术只能通过 ABAP OO 来实现。 例如所有新的 GUI 的概念比如 SAP Control Framework 和 BSP 只有通过 ABAP OO 的方式才能够实现。而对于面向过程的 ABAP 你就只能使用传统的 screen 和 list processing 了。
所以即便你在未来的开发中还不准备完全的转型为 OO ,你可以使用的 OO 技术来减少错误的隐患以及增强代码的可维护性。下面的部分将会阐述如何达到这一目的。
那么面向过程的 ABAP 和 ABAP OO 究竟是孰优孰劣?下面的部分将逐一进行论述。首先先了解以下 ABAP OO 的年代史。
1. SAP Basis Release 4.5 发布了 ABAP OO 的一个版本,引入了类接口的概念,并可以通过类来创建对象(实例化类)。
2. SAP Basis Release 4.6 发布了 ABAP OO 的完全版本,引入了 OO 方式的重要概念继承( inheritance ) , 可以通过多个接口来建立一个复合的接口。
3. SAP WEB APPLICATION SERVER 6.10/6.20 SAP basis 的下一代版本,在类之间引入了 friendship 的概念。并引入了对象服务( object service )可以把对象存储在数据库中。
4. SAP WEB APPLICATION SERVER 6.40 引入了共享对象( Shared Objects )的概念 , 即允许在应用服务器的共享内存中存储对象。这样在这个服务器中的任何一个程序都可以访问它。
几个关键点
n ABAP OO 是 ABAP 编程语言的扩展
n ABAP OO 是向下兼容的
n SAP 发布 ABAP OO 是为了进一步增强代码的可重用性
n 随着 ABAP OO 的发布, ABAP 运行时支持面向过程和面向对象两种模式
ABAP OO 的八大理由 ( 三 )
对于面向过程的模式,程序的运行通常是从 screen 的 dialog module 或 selection screen 的 start-of-selection 事件开始的。你在这些处理模块中操作全局变量来实现需求的功能。你可以通过内部的 form 和外部的 function module 来实现程序的模块化。这些过程除了可以操作全局变量外还可以具备内部的本地变量来协助实现内部的一些特定功能。
对于 OO 编程,唯一的结构单位就是类,这里类的 实例对象取代了全局变量。这些对象封装了应用的状态和行为。应用的状态是用属性来代表的它取代了面向过程中的全局变量。应用的行为是通过方法来实现的,他们用来改变应用的属性或者调用其它对象的方法。
ABAP OO 支持 OO 和面向过程的两种模式,这样在传统的 ABAP 程序(比如报表,模块池,功能池等)中你也可以使用 ABAP 对象类。在这些程序里你也就可以使用基于面向对象的新技术了,比如一些用户界面,避免了要想使用这些新技术必须重新编写程序。
目前大部分程序都是面向过程和 ABAP OO 的混合体如下图所示:
左边是纯粹的 ABAP OO 模式,所有的代码都封装在类中。你的应用中并不直接触 presentation layer(SAP Gui , Business Server Pages etc.),persistent data(database table,system file) 。他们是通过类库中的相应服务类来提供的。比如 SAP Control Framework ,Desktop Office Integration, and Business Pages 提供了与表现层的接口。对于 SAP Web Application 6.10 以上提供了与数据库层接口的服务对象。
虽然纯粹的 OO 模式技术上是可行的,但是现实中还存在着大量的两种模式的混合体如右面的图所示。 ABAP 对象和面向过程的技术同时应用,调用常用的功能模块,调用屏幕或者直接访问数据库等在对象中都存在。混合的模式及利用了新技术又保护了已投入的成本。
两种模式的选择正如本文所述, OO 的模式是最佳的选择,除非在绝对必要的情况下才使用面向过程的模式。比如传统的 screen programming 在 OO 中是不支持的,附录中会进一步阐释如何实现 screen 与 OO 的结合。
ABAP OO 的八大理由 ( 四 )
OO 编程优于过程编程的五个原因
下面的部分里我们将着重论述 OO 编程的主要优点,尽管这里所提到的优点与其他的 OO 语言( JAVA C++ )没有什么太大的区别 , 我们这里着重在 ABAP OO 与传统的 ABAP 程序相比而体现处来的优点。我们将通过实现一个银行账户管理的简单例子来比较两种模式的差别。
原因一:数据封装
将数据和程序封装在一个组件中将使程序变得容易修改。不要把一个应用的所有的数据和功能放在同各超长的程序里,你只需要把你的应用通过组件的稳定的接口把需要的部分包进来即可。如果一个封装的组件有一个好的接口,而这些接口的设计比较完善的话,那么这些接口的内部结构就会被很好的隐蔽起来,对这个部件的内部的修改便不会影响到应用的其他部件。让我们来看看面向过程和面向对象的方式是如何实现封装的。
面向过程模式的封装
在面向过程模式中有两种变量:
全局变量:他在程序的全部分声明,可以在程序的仍何不分应用。
局部变量:他在某个过程( form of function module )中声明也只能在其中应用。
全局变量的生命周期取决于整个程序的生命周期,局部变量的生命周期取决于该过程的生命周期。由于生命周期的限制,局部变量不适合于封装,它是过程中为了达到某个目的而使用的辅助变量。如果没有使用 TABLES 和 COMMON PARTS 语句那么程序中所声明的全局变量只能被该程序内部的各个模块调用。那么这就决定了在面向过程的模式中数据的封装只对程序本省有效。下面是一个简单的如何通过功能池实现对一个银行帐号的封装。
Function-pool Account
DATA: current_account TYPE accounts-amount.
Function deposit.
* IMPORTING REFERENCE(amount) TYPE account-amount
Current_amount = current_amount + amount.
EndFunction.
Function withdraw.
* IMPORTING REFERENCE(amount) TYPE account-amount
* RAISING
* CK_NEGATIVE_AMOUNT
IF current_amount > amount.
Current_amount = current_amount – amount.
ELSE.
RAISE EXCEPTION TYPE CK_NEGATIVE_AMOUNT.
ENDIF.
ENDFUNCTION.
这个模块池封装了银行账户的余额,并且通过功能模块 deposit 和 withdraw 来处理银行账户的余额。虽然单单是一个银行帐号的话这个模块池工作的非常好,但如果是多个应行帐号并要实现银行帐号之间交互的功能,问题就出现了。为了实现现实世界中多个银行帐号的管理,你不得不为每一个银行帐号建一个唯一名字的模块池和功能模块,这显然是不可取的。下面是个把所有银行帐号封装在一个模块池中的例子:
FUNCTION-POOL accounts.
DATA: account_table TYPE SORTED TABLE OF accounts
WITH UNIQUE KEY id.
ENDFUNCTION.
LOAD-OF-PROGRAM.
SELECT * INTO TABLE account_table
FROM accounts.
FUNCTION deposit.
* IMPORTING
* REFERENCE(id) TYPE accounts-id
* REFERENCE(amount) TYPE accounts-amount
DATA: account_wa TYPE accounts.
READ TABLE account_tabe
INTO account_wa
WITH TABLE KEY id = id.
Account_wa-amount = account_wa-amount + amount.
MODIFY account_table FROM account_wa.
ENDFUNCTION.
FUNCTION withdraw.
* IMPORTING
* REFERENCE(id) TYPE accounts-id
* REFERENCE(amount) TYPE accounts-amount
* RAISE
* CX_NEGATIVE_AMOUNT
DATA: account_wa TYPE accounts.
READ TABLE account_table
INTO account_wa
WITH TABLE KEY id = id.
IF account-amount > amount.
Account-amount = account-amount – amount.
MODIFY account_table FROM account_wa.
ELSE.
RAISE EXCEPTION TYPE CX_NEGATIVE_AMOUNT.
ENDIF.
ENDFUNCTION.
FUNCTION transfer.
* IMPORTING
* REFERENCE(id_from) TYPE accounts-id
* REFERENCE(id_to) TYPE accounts-id
* REFERENCE(amount) TYPE accounts-amount
* RAISE
* CX_NEGATIVE_AMOUNT
CALL FUNCTION ‘withdraw’
EXPORTING id = id
Amount = amount.
CALL FUNCTION ‘deposit’
EXPORTING id = id
Amount = amount.
ENDFUNCTION.
这样多个银行帐号就被封装在内表 account_tab 中,每个银行帐号是通过关键字 ID 来区分的。内表 account_tab 在模块池被调用时被填充以便模块池中的所有功能模块都可以使用它。功能模块 deposit 和 withdraw 通过 id 来处理一个银行帐号,而新的功能模块 transer 来实现两个银行帐号之间的转账。这样我们通过一定的技巧实现了多个银行帐号的管理但是为了区别每个银行帐号却增加了每个功能模块接口参数的复杂性。
ABAP OO 的八大理由(五)
面向对象模式的数据封装
OO 模式的编程也有两种数据类型,类实例的属性和类方法中的本地变量。
实例属性的生命周期依赖于对象的生命周期,本地变量的生命周期依赖于类方法的生命周期。所以面向对象的本地变量与面向过程的本地变量的生命周期是一样的。它是类方法中的辅助变量与数据封装无关。 ABAP 对象的数据封装在类这个级别。可以定义类属性的可见性,它的级别分别是在类内可见,在子类中可见,或者在外部类中可见。下面的例子演示了如何在类中封装银行账号:
CLASS account DEFINITION.
PUBLIC SECTION.
METHODS: constructor IMPORTING id TYPE account-id,
Deposit IMPORTING amount TYPE accounts-amount,
Withdraw IMPORTING amount TYPE accounts-amount
RAISING cx_negative_amount,,
Transfer IMPORTING amount TYPE accounts-amount
Target REF TO account
RAISING cx_negative_amount.
PRIVATE SECTION.
DATA amount TYPE accounts-amount.
ENDCLASS.
CLASS account IMPLEMENTATION.
METHOD constructor.
SELECT SINGLE amount INTO (amount)
FROM accounts
WHERE id = id.
ENDMETHOD.
METHOD deposit.
Me->amount = me->amount + amount.
ENDMETHOD.
METHOD withdraw.
IF me->amount > amount.
Me->amount = me->amount – amount.
ELSE.
RAISE EXCEPTION TYPE cx_negative_amount.
ENDIF.
ENDMETHOD.
METHOD transfer.
Me->withdraw( amount ).
Target->deposit( amount ).
ENDMETHOD.
ENDCLASS.
在 account 类的定义部分可以定义属性和方法的可见性( private public )。类的定义把功能和数据结合到了一块。理论上,前面例子的功能池也做到了这点,只不过在不同的数据模块间进行交互的时候就暴露了其局限性,所以功能池只能说是多个功能模块的容器还不能真正的共享数据。在 ABAP OO 的类中 PUBLIC 部分定义了其与外部交互的接口。在这个例子中描述类状态的属性是私有的,而对外的公共接口是操作这些属性的方法。
这个例子中的类既实现了前面面向过程的第一个例子的简单功能也实现了第二个例子较为复杂的功能。这个类的 deposit,withdraw 方法与第一个例子的两个功能模块的作用相同,由于你可以创建一个类的多个对象,所以这个类只需封装一个银行帐号的数据即可, constructor 方法用来为不同的银行账号初始化数据。由于每个对象都有它自己的数据,所以你不需为类添加额外的参数。最后,不止于此,由于一个对象可以调用另外一个对象的方法,那么不同银行账号之间的交互便有了更加简洁的方案,这例子中 transfer 功能的实现要比前面第二个例子的实现简单的多。
ABAP OO 的八大理由(六)
原因二,实例化
对象的实例化是面向对象编程的一个重要特点。在 ABAP OO 中,通过实例化你可以实现一个类的多个变体,这样程序员便可以调用任何一个对象并让他完成相应的功能(比如一个银行帐号的收款存款和窗户的打开关闭等)。
在面向过程的编程中,当程序内容被编译到内存中便隐含着一个实例化的过程,但是这个实例并不能为程序员显式的调用,因为这需要数据和功能的分离,而面向过程的的程序员并不是直接对对象进行操作,而是要告诉一个功能模块需要修改那些数据(比如需要告诉功能模块那个银行帐号要付款或者那个窗户要打开)。下面我们将详细描述这两种不同的实例化之间的区别。
面向过程的实例化
大多数程序员可能没有意识到其实在面向过程的程序里也有类似于对象实例化的现象。程序本身在被调入内存的时候其实就是一个隐含的实例化过程,因为程序本身被调用或者程序中的某个过程被其他程序调用了。
模块池隐含实例化的例子
DATA: id1(8) TYPE n,
Id2(8) TYPE n,
Amnt TYPE p DECIMALS 2,
Exc_ref TYPE REF TO cx_negative_amount,
Text TYPE string.
TRY.
Id1 = …..
Id2 = …..
Amnt = …..
CALL FUNCTION ‘TRANSFER’
Id_from = id1
Id_to = id2
Amount = amnt.
CATCH cx_negative_amount INTO exc_ref.
Text = exc_ref->get_text().
MESSAGE text TYPE i.
ENDTRY.
上面的例子演示了调用银行账户模块池的转账功能模块的例子,两个银行账户是通过账户 ID 来区分的。
如果银行账户模块池中的功能模块是第一次被调用,那么整个模块池就会被调到调用程序的内部 session 中,模块池的 LOAD-OF-PROGRAM 事件就会被触发。对应的事件部分充当着模块池实例化的结构事件。如果在同一个内部 session 中,在同一个程序或另一个程序中银行帐号模块池已经被调用,那么下一调用,模块池已经被装载到这个内部 session 中。由于程序或者过程只有一次装载到内存的过程,所以功能模块便可以使用其所在模块池中的全局数据。
这种实例化对于数据封装带来了如下缺陷:
l 你无法控制实例化的时刻(在一个较大的程序中功能模块在任何时候,任何地点都有可能被调用),这样当一个程序在调用功能模块的时候它将无法知道是否另一个程序或过程改变了模块池的全局数据。
l 你无法在内存中删除模块池的实例,因为模块池的实例的生命周期取决于调用主程序的生命周期。如果你想在模块池的全局数据中使用数据量较大的变量,那么你在使用后需要手工将其清除。另外模块池一般是许多功能模块的容器,这样就会给内存带来很大的压力,即便是你只是在较短的时间调用很简单的功能模块。
l 每个程序在内存中只能创建一个实例。正如我们在原因 1 种所看到的,这就限制了我们充分发挥模块池的数据封装性。
还要注意的如果在模块池中,全局变量不能被整取操作,那么将是功能模块对全局变量的使用变得更加危险。全局变量的状态取决于功能模块的调用顺序,尤其是在一个大程序里,功能模块在不同的地方都有可能调用,这样就导致了模块池全局变量状态的不稳定。
ABAP OO 的八大理由 ( 七 )
面向对象的实例化
行如其名,在面向对象的模式中,对象是唯一存在的东西。对象是类的实例,是通过 CREATE OBJECT 语句来实现的。 ABAP 对象不可能被隐含的实例化,你通过指针变量来操作对象。
当然你通过指针所操作的对象不一定是自己创建的,这种现象主要是通过指针操作的对象很有可能是从其他地方传过来的,比如说工厂方法( factory method )或者接口参数。你可以在一个类中创建多个对象每个对象通过属性都有自己的标识符和内容。
对象的生命周期是由使用者来控制的(其他对象,程序或过程等),只要有指针变量指向他,他就在内存中存在。
如果没有指针变量指向对象,这个对象就会被自动的被 ABAP 内存收集器从内存中删除。和程序的实例一样, ABAP 对象也存储在主程序的内部 SESSION 中。在 640 之后你就可以在应用服务器的 SHARED MEMORY 中创建 shared object 了,这样在这应用服务器上运行的任何程序都可以调用它。
下面的例子演示了从银行账号类中产生的两个银行账号对象。
DATA: account1 TYPE REF TO account,
Account2 TYPE REF TO account,
Amnt TYPE p DECIMALS 2,
Exc_ref TYPE REF TO cx_negative_amount,
Text TYPE string.
CREATE OBJECT : account1 EXPORTING id = ….,
Account2 EXPORTING id = …..
TRY.
Amnt = ….
Account1àtransfer ( EXPORTING amount = amnt
Target = account2).
CATCH cx_negative_amount INTO exc_ref.
Text = exc_ref->get_text().
MESSAGE text TYPE i.
ENDTRY.
指针变量 account1,account2 是类 account 的对象的显示的操作符。在 CREATE OBJECT 语句中每个账号标识被传入到了对象的结构体。对象一旦被创建,账号标识就不用再被声明了。你可以调用一个对象的 transfer 方法,通过通过账号指针变量来指明要转入的账号。
与面向过程的上面的例子相比,显然面向对象的例子更加直接。在面向过程的例子里你不得不为每个功能模块指明要操作的数据,而在面向对象的例子里你只需调用对象的方法那么他就会自动地使用它自己的数据。值得注意的是例外信息我们都是使用基于类的方法,这在 610 之后是被推荐的。在面向过程的程序中使用指针变量 exc_ref 显得有些另类,因为在非 OO 模式的程序中使用了 OO 模式的东西,这也是 ABAP OO 的一个自然特性。
ABAP OO 的八大理由 ( 八 )
原因三:代码重用性
代码重用性是软件维护和开发的一个重要衡量指标。我们应当把一些通用的代码放在一个中心库里,而无须一遍一遍的重复编写功能相似的代码。下面就在代码重用方面检查一下 ABAP OO 和 ABAP OP 孰优孰劣。
面向过程的代码重用性
在面向过程的模式中没有什么专门的机制来支持代码的重用性。你可以定义通用的过程,但是如果你想在特殊情况下使用通用代码,你不得不建立很庞大的模块池,每个功能模块又不得不有复杂的参数,而每个功能模块中你又不得不使用复杂的分支语句,例如 SWITCH CASE. 。
比如为了给银行账号加入两个特殊的账号 , 检查账号和存款账号,我们不得不修改我们的程序以支持这种变化。下面的例子演示了对功能模块 withdraw 的修改。
FUNCTION withdraw.
CASE kind.
WHEN ‘C’.
PERFORM withdraw_from_checking_account USING id amount.
WHEN ‘S’.
PERORM withdraw_from_savings_account USING id amount.
WHEN OTHERS.
RAISE EXCEPTION TYPE cx_unknown_account_type.
ENDCASE.
ENDFUNCTION.
FORM withdraw_from_checking_account.
USING i_id TYPE accounts-id
Amount TYPE accounts-amount.
DATA: account_wa TYPE accounts.
READ TABLE account_tab WITH TABLE KEY id = i_id
INTO account_wa.
Account_wa-amount = account_wa-amount – amount.
MODIFY TABLE account_tab FROM account_wa.
IF account_wa-amount < 0.
………..
ENDIF.
ENDFORM.
FORM withdraw_from_savings_account.
USING i_id TYPE accounts-id
Amount TYPE accounts-amount
RAISING cx_negative_amount..
DATA: account_wa TYPE accounts.
READ TABLE account_tab WITH TABLE KEY id = i_id
INTO account_wa.
IF account_wa-amount > i_amount.
Account_wa-amount = account_wa-amount – amount.
MODIFY account_tab FROM account_wa.
ELSE.
RAISE cx_negative_amount.
ENDIF.
ENDFORM.
这样你就不得不怎加新的参数帐户类型了
在这个例子中还得增加新的例外信息,即如果帐户类型不在合理范围之内的情况。
最终为了实现对不同的帐户类型采取不同的处理逻辑,那么你就不得不写两个 form 来分别处理两种不同的情况。通过 CASE 语句来实现分支。
ABAP OO 的代码可重用性
在 ABAP OO 中通过继承来实现代码的可重用性,可以通过通用的父类来派生特殊的子类。这样好处是简单的类既可代替代码庞杂的模块池,简单的方法参数既可代替功能模块复杂的参数,最重要的是通过继承实现了类的多态。这样就实现了子类的对象仍然可以使用父类的方法。这大大增强了软件开发的建模能力。