Mark的技术博客

SAP顾问 | 咨询专家 | IT与AI技术探索者

吴让宇(Mark)

吴让宇(Mark)

SAP顾问 | 咨询专家 | IT与AI技术探索者

吴让宇(Mark)的个人技术博客,专注于SAP系统实施、行业方案咨询、IT技术与AI技术的研究与分享。

SAP Fiori ABAP CDS OData Dify Docker AI LLM

最新文章

OO编程模型 练习

#practice #oo-basics

📌核心模式
关键词 答案
封装 将数据和操作数据的方法捆绑在一起,隐藏内部实现细节
函数组 vs 类 函数组仅支持单一实例,类支持多实例化和完整的封装
过程式 vs OO 过程式以功能为中心,OO以数据/对象为中心

题目1 - [记忆] 封装的定义

请解释面向对象编程中”封装(Encapsulation)”的概念及其主要目的。

📌查看答案
封装是将数据(属性)和操作这些数据的方法捆绑在一个单元(类)中的机制。其主要目的是:
  1. 隐藏内部实现细节:外部代码无需了解对象内部如何工作
  2. 保护数据完整性:通过定义可见性(public、protected、private)控制外部对内部数据的访问
  3. 降低耦合度:内部实现的变化不会影响外部调用者

在ABAP中,类的公有区域(PUBLIC SECTION)定义外部接口,私有区域(PRIVATE SECTION)隐藏实现细节。


题目2 - [记忆] 函数组与类的区别

请列举函数组(Function Group)和类(Class)之间的三个主要区别。

📌查看答案
特性 函数组
实例化 仅支持单一实例(隐式全局数据) 支持多个独立实例
封装程度 函数组内的全局数据对所有函数模块可见,但封装机制较弱 提供严格的可见性控制(public/protected/private)
扩展性 不支持继承和多态 支持继承、多态和接口
状态管理 全局数据在函数组加载时初始化,仅一份 每个对象实例拥有独立的状态

题目3 - [记忆] 过程式编程模型的特点

请描述过程式编程模型的三个核心特征。

📌查看答案
过程式编程模型的核心特征:
  1. 以功能为中心:程序由一系列按顺序执行的功能/过程组成,数据在功能之间传递
  2. 数据与功能分离:数据结构独立定义,函数操作外部传入的数据
  3. 自顶向下的设计:复杂问题被分解为逐步细化的子程序调用层次

在ABAP中,过程式编程通过报表程序、函数模块和子程序实现。


题目4 - [记忆] 多实例化的含义

在面向对象编程中,”多实例化(Multiple Instantiation)”是什么意思?为什么它比函数组的单一实例更有优势?

📌查看答案
多实例化是指可以从同一个类创建多个独立的对象实例,每个实例拥有各自的属性值(状态)。

优势:

  • 独立状态:不同对象互不干扰,例如可以同时存在多个”订单”对象,各有不同的金额和状态
  • 内存管理:对象可按需创建和释放,通过垃圾回收机制自动管理
  • 灵活性:同一类型的多个实例可以同时存在于程序中,各自执行不同的操作

对比函数组:函数组只有一份全局数据,无法同时维护多个独立的状态。


题目5 - [记忆] 面向对象编程的四大特性

请列出面向对象编程的四大核心特性(也称为四大支柱),并简要说明每一个。

📌查看答案
  1. 封装(Encapsulation):将数据和操作数据的方法组合在一个单元中,通过可见性控制保护数据
  2. 继承(Inheritance):子类可以继承父类的属性和方法,并可以扩展或重定义它们,实现代码复用
  3. 多态(Polymorphism):同一操作作用于不同的对象时可以有不同的行为,通过方法重定义和接口实现
  4. 抽象(Abstraction):通过类和接口定义对象的抽象模型,隐藏复杂的实现细节,只暴露必要的接口

题目6 - [应用] 选择编程模型

公司需要开发一个库存管理系统,需要同时处理多个仓库的数据,每个仓库有独立的库存清单和操作日志。使用过程式编程(函数组)还是面向对象编程(类)更合适?请说明理由。

📌查看答案
面向对象编程(类)更合适。

理由:

  1. 多实例需求:系统需要同时管理多个仓库,每个仓库是独立的对象,拥有各自的库存清单和日志。类的多实例化天然支持这种需求
  2. 封装需求:每个仓库的库存数据和操作方法应封装在一起,防止外部直接修改数据,确保数据一致性
  3. 扩展性:未来可能需要添加新类型的仓库(如冷链仓库),可以通过继承实现扩展
  4. 维护性:对象之间的交互关系清晰,代码更易于维护和测试

如果使用函数组,则需要手动管理多个仓库的状态(例如使用内表存储),增加了复杂性且容易出错。


题目7 - [应用] 识别封装违规

以下ABAP代码片段中,类的封装是否被正确使用?请分析并指出问题。

1
2
3
4
5
CLASS lcl_bank_account DEFINITION.
PUBLIC SECTION.
DATA: balance TYPE p DECIMALS 2.
METHODS: deposit IMPORTING iv_amount TYPE p.
ENDCLASS.

假设外部代码直接修改 balance 字段。

📌查看答案
封装被违反。

问题分析:

  1. balance 属性被定义在 PUBLIC SECTION 中,这意味着外部代码可以直接读写该属性
  2. 外部代码可能绕过 deposit 方法直接修改余额,导致:
    • 无法验证金额的有效性(如负数存款)
    • 无法记录交易日志
    • 数据完整性无法保证

修正方案:

1
2
3
4
5
6
7
CLASS lcl_bank_account DEFINITION.
PUBLIC SECTION.
METHODS: deposit IMPORTING iv_amount TYPE p,
get_balance RETURNING VALUE(rv_balance) TYPE p.
PRIVATE SECTION.
DATA: balance TYPE p DECIMALS 2.
ENDCLASS.

balance 移至 PRIVATE SECTION,通过公共方法提供受控的访问。


题目8 - [分析] 编程范式对比分析

请从以下五个维度对比分析过程式编程和面向对象编程,并讨论在ABAP项目中何时应选择哪种范式。

📌查看答案
维度 过程式编程 面向对象编程
设计重心 以功能/过程为中心,数据在过程间流动 以数据/对象为中心,功能封装在对象内
数据管理 全局数据或通过参数传递 封装在对象内部,通过方法访问
复用机制 子程序和函数模块的调用 继承、组合、接口实现
扩展方式 修改现有代码或添加新函数 通过继承和重定义扩展,符合开闭原则
可维护性 大型项目中代码耦合度高,维护困难 职责清晰,耦合度低,易于维护和测试

选择建议:

  • 使用过程式的场景:简单的数据转换程序、一次性报表、批量数据处理脚本
  • 使用面向对象的场景:复杂的业务逻辑、需要多实例管理的系统、需要长期维护和扩展的应用、与设计模式结合的架构设计
  • 实际项目中:ABAP支持两种范式混合使用,但新开发应优先采用面向对象方式,以获得更好的可维护性和扩展性

📌模式总结
关键词 答案
封装 数据 + 方法捆绑,隐藏内部细节,通过可见性保护数据
函数组限制 单一实例,弱封装,无继承/多态
多实例化 同一类创建多个独立对象,各自维护状态
四大支柱 封装、继承、多态、抽象
过程式特征 以功能为中心,数据与功能分离,自顶向下
封装违规 属性放在公有区域,外部可直接修改,破坏数据完整性
范式选择 简单任务用过程式,复杂业务逻辑用OO,新开发优先OO

构造器与方法调用 (★★★)

#abap-objects #oo-basics #constructor #method-call

概览表

项目 关键点
实例构造器 METHODS: constructor, 创建对象时自动调用
静态构造器 CLASS-METHODS: class_constructor, 类首次使用时自动调用
方法调用 显式/简写/函数式三种语法
ME 方法内的自引用

构造器 (Constructor)

实例构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CLASS lcl_vehicle DEFINITION.
PUBLIC SECTION.
METHODS: constructor
IMPORTING
!iv_make TYPE string
!iv_model TYPE string.
ENDCLASS.

CLASS lcl_vehicle IMPLEMENTATION.
METHOD constructor.
make = iv_make.
model = iv_model.
ENDMETHOD.
ENDCLASS.

调用方式:

1
2
3
4
CREATE OBJECT vehicle
EXPORTING
iv_make = 'BMW'
iv_model = 'X5'.

关键规则:

  • 每个类最多一个实例构造器
  • 构造器不能重定义(REDEFINITION)
  • 构造器只能定义在 PUBLIC SECTION(一般情况下)
  • 实例化对象时自动调用

静态构造器

1
2
3
4
CLASS lcl_vehicle DEFINITION.
PUBLIC SECTION.
CLASS-METHODS: class_constructor.
ENDCLASS.

关键规则:

  • 无参数,不能有 IMPORTING/EXPORTING
  • 首次被使用时自动调用(CREATE OBJECT 或访问静态组件)
  • 只执行一次
  • 不能显式调用
⚠️静态构造器的调用时机
静态构造器在类首次被访问时自动触发。应避免在静态构造器中访问其他类的静态属性,因为可能导致不可预期的初始化顺序。

方法调用语法

三种调用方式

方式 语法 适用场景
显式调用 CALL METHOD ref->meth EXPORTING ... 兼容旧代码
简写调用 ref->meth( iv_param = value ). 推荐方式
函数式调用 DATA(result) = ref->meth( ). 有RETURNING参数的方法

参数传递简写

1
2
3
4
5
6
7
8
9
10
11
12
13
" 无参数
ref->meth( ).

" 一个IMPORTING参数
ref->meth( value ). " 位置参数
ref->meth( iv_param = value ). " 命名参数

" 多个IMPORTING参数
ref->meth( EXPORTING iv_a = a iv_b = b ).
ref->meth( iv_a = a iv_b = b ). " 简写可省略EXPORTING

" 接收RETURNING值
DATA(count) = ref->get_count( ).

函数式方法 (Functional Method)

RETURNING 参数的方法可以像函数一样使用:

1
2
3
4
5
6
7
" 直接在表达式中使用
IF ref->is_valid( ) = abap_true.
WRITE: / ref->get_name( ).
ENDIF.

" 链式调用(7.0 EhP2+)
ref->get_vehicle( )->get_motor( )->get_power( ).

自引用 ME

1
2
3
4
METHOD some_method.
me->attribute = value. " 明确指向自身属性
CALL METHOD me->method. " 调用自身方法
ENDMETHOD.
  • ME 是预定义的引用变量
  • 在每个实例方法中自动可用
  • 指向当前对象实例本身
  • 常用于区分参数名和属性名同名的情况

私有方法与公有方法的协作

1
2
3
4
5
6
7
8
9
10
11
外部调用者


┌──────────────────┐
│ 公有方法 │ ← 外部接口
│ (入口点) │
│ │ │
│ ↓ │
│ 私有方法 │ ← 内部实现细节
│ (辅助逻辑) │
└──────────────────┘
💡私有方法的作用
私有方法将复杂的实现细节隐藏在公有方法背后,实现真正的封装。

考试/测试模式

场景/关键词 答案
“构造器能重定义吗” 不能
“静态构造器参数” 不能有任何参数
“静态构造器何时调用” 类首次被使用时,自动调用一次
“ME的用途” 区分同名参数和属性,明确自引用
“函数式方法” 有 RETURNING 参数的方法
“链式调用” 7.0 EhP2+ 支持 ref->meth( )->meth2( )

调整 SAP 标准软件 (★★★)

#abap #abap-enhancement

概述表

术语 要点
原始对象 在其开发系统(”出生地”)中的对象
副本 通过传输到达另一个系统的对象
更正 修改原始对象
修复 修改副本
修改 修复 SAP 对象(客户系统)

四种调整选项

选项 描述 版本升级安全?
客户开发 在 Y*/Z* 命名空间中创建自有对象
自定义 通过维护事务配置系统属性
增强(首选) 与版本无关的调整
修改(最后手段) 直接修改 SAP 代码 — 升级时需要调整
⚠️修改与增强
增强始终是首选方案。修改在升级时需要进行修改调整(将旧修改版本与新 SAP 版本进行比较)。这既耗时又容易出错。

增强类型

程序出口(最重要)

  • 用户出口:较旧的技术
  • 客户出口:更结构化的方式
  • BTEs(业务交易事件)
  • BAdIs(业务附加项)

其他增强类型

类型 描述
菜单出口 SAP 菜单项链接到客户代码
屏幕出口 SAP 子屏幕区域用于自定义屏幕
字段文档 用自己的文本覆盖 F1 帮助
字段标签 覆盖数据元素标签
附加结构 向 SAP 透明表添加字段

原始对象和副本

1
2
3
开发系统 (原始对象) ──传输──→ 质量系统 (副本)
更正 = 修改原始对象 修复 = 修改副本
升级时的修改调整
📌原始对象不能被覆盖
原始对象永远不会被传输覆盖。

SSCR 注册

注册 用途
开发者密钥 每个开发者一次(用户 + 系统许可证)
对象密钥 每个被修改的 SAP 对象

考试/测试模式

场景/关键词 答案
“首选调整方式?” 增强(而非修改)
“为什么要避免修改?” 升级时需要调整
“原始对象可以被覆盖吗?” 不可以——永远不能
“更正与修复的区别?” 更正 = 修改原始对象;修复 = 修改副本
“附加结构?” 向 SAP 表添加字段而无需修改

相关笔记

面向对象编程模型 (★★★)

#abap-objects #oo-basics

概览表

项目 关键点
过程式模型 数据与函数分离,全局变量可被任意访问
函数组 封装数据与服务,但只能有一个实例
OO模型 封装 + 多实例化 + 继承 + 多态

过程式编程模型

数据和函数通常分开存储:

  • 全局变量存储数据
  • 子程序(Subroutines) 存储函数
  • 每个子程序都可以访问任何变量 → 无法保证一致的数据访问

典型过程式ABAP程序结构:

1
2
3
4
5
类型定义 + 数据声明          (数据)
├── Modularization Units (函数)
│ ├── Subroutines (FORM)
│ └── Function Modules
└── 全局数据无特殊保护
⚠️过程式模型的风险
主程序层面上,全局数据对象没有特殊保护。任何地方都可以读写全局变量。

使用函数组实现封装

函数组提供了一定程度的封装:

  • 函数组加载到独立内存区域
  • 主程序只能通过函数模块访问函数组的数据
  • 不能直接访问函数组的全局数据
1
2
3
4
5
6
7
8
9
┌─────────────────┐     ┌──────────────────┐
│ Main Program │ │ Function Group │
│ │────→│ Function Modules │
│ │调用 │ ├ INC_SPEED() │
│ │ │ ├ DEC_SPEED() │
│ │ │ └ GET_SPEED() │
│ │ ✗ │ Global Data: │
│ │────→│ SPEED (不可直接) │
└─────────────────┘ └──────────────────┘

OO编程的关键特性

特性 说明
封装 对象保护自身数据,外部只能通过公有方法访问
多实例化 同一个类可以创建多个独立的运行时实例
继承 子类继承父类的特征,可以扩展和修改
多态 同一接口可以有不同的实现

多实例化 — OO的核心优势

函数组的局限:一个程序中一个函数组只有一个实例。如果要管理多个”车辆”,需要额外的编程和管理工作。

OO编程:可以轻松创建多个独立对象实例:

1
2
3
4
5
6
7
CLASS LCL_VEHICLE 定义
├── 对象1: car (make=BMW, speed=100)
├── 对象2: truck (make=VW, speed=60)
├── 对象3: bus (make=Mercedes, speed=80)
└── 对象4: car (make=Audi, speed=120)
→ 所有对象共享相同数据结构和功能
→ 但各自有独立的数据值

内存管理

1
2
3
4
5
6
7
8
┌── Internal Session ──────────────────────┐
│ Main Program │
│ ├── 对象1 (LCL_VEHICLE) [独立数据区] │
│ ├── 对象2 (LCL_VEHICLE) [独立数据区] │
│ ├── 对象3 (LCL_VEHICLE) [独立数据区] │
│ └── 函数组 [独立数据区] │
│ → 所有区域相互隔离,数据受保护 │
└──────────────────────────────────────────┘

考试/测试模式

场景/关键词 答案
“函数组 vs 类的区别” 函数组只能单实例,类支持多实例化
“封装在过程式中的实现” 函数组可以封装,但不够灵活
“OO的主要优势” 封装 + 多实例 + 继承 + 多态
“对象存储在哪里” 与主程序相同的内部会话中,但数据区分离

SAP 列表查看器 (ALV) (★★★)

#abap #abap-dialog

概述表

用途
CL_SALV_TABLE 简单 ALV 显示
CL_GUI_ALV_GRID 自定义容器中的高级 ALV

简单 ALV (CL_SALV_TABLE)

1
2
3
4
5
6
7
DATA: go_alv TYPE REF TO cl_salv_table.

CALL METHOD cl_salv_table=>factory
IMPORTING r_salv_table = go_alv
CHANGING t_table = gt_flights.

go_alv->display( ).
  • 最少代码实现表格显示
  • 自动从 ABAP 字典获取列标题
  • 内置排序、筛选、汇总功能

带容器控件的高级 ALV

组件 角色
自定义容器 ALV 的屏幕区域
CL_GUI_ALV_GRID 功能完整的网格控件
字段目录 列定义
布局 显示设置

关键 ALV 方法

方法 用途
factory 创建 ALV 实例
display 显示 ALV
set_table_for_first_display 初始数据显示 (CL_GUI_ALV_GRID)
refresh_table_display 更新数据

考试/测试模式

场景/关键词 答案
“简单 ALV 类?” CL_SALV_TABLE
“高级 ALV 类?” CL_GUI_ALV_GRID
“ALV 列标题来自?” ABAP 字典定义

相关笔记

第 9 单元:SAP 软件调整练习(8 道题)

#practice #abap #abap-enhancement

相关概念

📌关键模式(点击展开)
关键词 答案
原始对象 对象的出生地
更正 修改原始对象
修复 修改副本
修改 在客户系统中修复 SAP 对象
首选方案 增强(而非修改)

问题 1 - 原始对象与副本 [recall]

原始对象和副本的区别是什么?

📌显示答案
原始对象位于其开发的系统(”出生地”)。副本是同一对象传输到另一个系统后的版本。

问题 2 - 更正与修复 [recall]

定义更正和修复。

📌显示答案
更正:修改原始对象(开发任务)。修复:修改副本(修复任务)。

问题 3 - 修改的问题 [recall]

为什么应该避免修改?

📌显示答案
升级期间,修改过的对象可能被覆盖。需要进行修改调整——手动重新应用更改。既耗时又容易出错。

问题 4 - 增强的优势 [recall]

为什么增强优于修改?

📌显示答案
与版本无关——升级时无需调整。相同功能,无维护负担。

问题 5 - 四种选项 [recall]

列出调整 SAP 软件的四种选项。

📌显示答案
  1. 客户开发 — 在客户命名空间中创建自有对象
  2. 自定义 — 通过维护事务配置系统属性
  3. 增强 — 扩展 SAP 对象而不修改
  4. 修改 — 直接更改(最后手段)

问题 6 - 增强类型 [recall]

列举至少四种增强类型。

📌显示答案
程序出口(用户出口、客户出口、BTE、BAdI)、菜单出口、屏幕出口、附加结构、字段文档/标签覆盖。

问题 7 - SSCR 密钥 [application]

开发者需要修改 SAP 标准对象。需要哪些注册?

📌显示答案
  1. 开发者密钥(一次性,关联用户 + 系统许可证)
  2. 对象密钥(每个 SAP 对象:名称 + 类型 + 系统许可证)

问题 8 - 附加结构 [analysis]

附加结构与修改有什么区别?

📌显示答案
附加结构向表添加字段而不改变 SAP 对象。它是一种增强——在升级时无需调整即可保留。

📌模式总结(点击展开)
关键词 答案
原始对象 开发系统
副本 传输到另一个系统
更正 修改原始对象
修复 修改副本
修改 修复 SAP 对象
增强 与版本无关,首选方案
附加结构 无需修改即可添加字段
SSCR 开发者密钥 + 对象密钥

对象与引用 (★★★)

#abap-objects #oo-basics #class-definition

概览表

项目 关键点
引用变量 指向对象的指针,用 TYPE REF TO 声明
CREATE OBJECT 在内存中创建类的新实例
垃圾回收 无引用的对象自动被回收
自引用 ME 方法内指向当前对象自身的引用

引用变量

引用变量是指向对象的指针

1
DATA: ref_vehicle TYPE REF TO lcl_vehicle.
  • 声明时引用变量为初始值 (不指向任何对象)
  • 使用 CREATE OBJECT 创建实际对象

创建对象

1
2
3
4
5
" 声明引用
DATA: vehicle TYPE REF TO lcl_vehicle.

" 创建对象实例
CREATE OBJECT vehicle.

对象在内存中的表示

1
2
3
4
5
6
引用变量:  vehicle ──────→ ┌───────────────────┐
│ LCL_VEHICLE 实例 │
│ make = "" │
│ model = "" │
│ speed = 0 │
└───────────────────┘
📌引用 vs 对象
  • 引用变量和对象是两个不同的东西
  • 多个引用变量可以指向同一个对象
  • 引用变量本身也可以是对象的属性(关联关系)

多个引用指向同一对象

1
2
3
4
5
DATA: ref1 TYPE REF TO lcl_vehicle,
ref2 TYPE REF TO lcl_vehicle.

CREATE OBJECT ref1.
ref2 = ref1. " ref1 和 ref2 指向同一个对象

访问对象组件

1
2
3
4
5
6
7
8
" 访问属性
WRITE: / vehicle->make.

" 调用方法
vehicle->set_type( iv_make = 'BMW' iv_model = 'X5' ).

" 静态组件访问
WRITE: / lcl_vehicle=>count. " 类名=>组件名
语法 用途 示例
ref->attr 通过引用访问实例属性 vehicle->make
ref->meth( ) 通过引用调用实例方法 vehicle->set_type( )
class=>attr 访问静态属性 lcl_vehicle=>count
class=>meth( ) 调用静态方法 lcl_vehicle=>get_count( )

垃圾回收 (Garbage Collection)

ABAP 运行时系统自动管理对象的生命周期:

  • 当对象没有任何引用指向它时,成为垃圾
  • 垃圾回收器会自动清理这些对象
  • 开发者不需要手动释放内存
1
2
3
ref1 ──→ [对象A]     ← 有引用,不会被回收
ref2 ──→ [对象B] ← 有引用,不会被回收
[对象C] ← 无引用,将被垃圾回收

对象作为属性(关联)

对象引用可以作为其他对象的属性,实现关联关系:

1
2
3
4
CLASS lcl_vehicle DEFINITION.
PRIVATE SECTION.
DATA: mo_motor TYPE REF TO lcl_motor. " 组合关系
ENDCLASS.

内表中存储对象引用

1
2
3
4
5
6
7
8
9
DATA: lt_vehicles TYPE TABLE OF REF TO lcl_vehicle.

" 添加引用到内表
APPEND vehicle TO lt_vehicles.

" 遍历
LOOP AT lt_vehicles INTO DATA(lo_veh).
lo_veh->display( ).
ENDLOOP.

考试/测试模式

场景/关键词 答案
“->” vs “=>” -> 用于实例组件,=> 用于静态组件
“对象何时被回收” 没有引用指向时自动回收
“引用变量初始值” 不指向任何对象(NULL引用)
“对象存在哪里” 与程序相同的内部会话中,独立数据区

类型转换与多态 (★★★)

#abap-objects #inheritance #casting #polymorphism

概览表

项目 关键点
Up-cast 子类→父类引用,隐式安全
Down-cast 父类→子类引用,显式 ?=,可能运行时错误
多态 同一接口不同实现,通过动态绑定实现
泛型调用 使用父类/接口引用统一调用

Up-cast (向上转型 / Widening Cast)

1
2
3
4
5
DATA: lo_vehicle TYPE REF TO lcl_vehicle,
lo_car TYPE REF TO lcl_car.

CREATE OBJECT lo_car.
lo_vehicle = lo_car. " Up-cast: 隐式,总是安全

特性

  • 使用 = 赋值
  • 隐式,编译器允许
  • 总是安全 — 子类一定是父类
  • 可见的组件减少(只能访问父类定义的组件)
  • 类型兼容性增加(可以接受更多动态类型)
1
2
3
转换前: lo_car     → 可访问 [Vehicle + Car 组件]
转换后: lo_vehicle → 只能访问 [Vehicle 组件]
视角变窄,但兼容性变宽
💡Up-cast 的典型用途
将不同子类的对象收集到一个统一类型的内表中:
1
2
3
4
5
6
DATA: lt_vehicles TYPE TABLE OF REF TO lcl_vehicle.
lt_vehicles = VALUE #(
( NEW lcl_car( ) ) " 自动 up-cast
( NEW lcl_truck( ) ) " 自动 up-cast
( NEW lcl_bus( ) ) " 自动 up-cast
).

Down-cast (向下转型 / Narrowing Cast)

1
2
3
4
5
6
DATA: lo_vehicle TYPE REF TO lcl_vehicle,
lo_car TYPE REF TO lcl_car.

CREATE OBJECT lo_car.
lo_vehicle = lo_car. " Up-cast
lo_car ?= lo_vehicle. " Down-cast: 需要显式 ?=

特性

  • 使用 ?= 赋值
  • 显式,开发者自己负责
  • 可能失败 — 如果实际对象不是目标类型
  • 失败时触发 CX_SY_MOVE_CAST_ERROR 异常
1
2
安全:  lo_vehicle 实际指向 lcl_car → ?= 成功
危险: lo_vehicle 实际指向 lcl_truck → ?= lcl_car → 运行时错误!

安全的 Down-cast

1
2
3
4
5
6
7
8
9
10
11
" 方法1: TRY-CATCH
TRY.
lo_car ?= lo_vehicle.
CATCH cx_sy_move_cast_error.
" 处理类型不匹配
ENDTRY.

" 方法2: RTTI 类型检查
IF cl_abap_classdescr=>get_relative_name( lo_vehicle ) = 'LCL_CAR'.
lo_car ?= lo_vehicle.
ENDIF.

多态 (Polymorphism)

多态是继承最重要的应用:

  • 通过父类引用调用方法
  • 实际执行的是子类的重定义实现
  • 运行时动态绑定
1
2
3
4
5
6
7
8
9
10
" 创建不同子类对象
DATA: lt_vehicles TYPE TABLE OF REF TO lcl_vehicle.
APPEND NEW lcl_car( ) TO lt_vehicles.
APPEND NEW lcl_truck( ) TO lt_vehicles.
APPEND NEW lcl_bus( ) TO lt_vehicles.

" 统一调用 — 多态!
LOOP AT lt_vehicles INTO DATA(lo_v).
lo_v->display( ). " 每个对象执行自己的 display 实现
ENDLOOP.

多态的工作原理

1
2
3
4
5
6
7
静态类型: lcl_vehicle          → 编译时检查 vehicle 组件
动态类型: 实际的 car/truck/bus → 运行时调用实际实现

lo_v->display( )
→ lo_v 指向 car? → 执行 lcl_car=>display( )
→ lo_v 指向 truck? → 执行 lcl_truck=>display( )
→ lo_v 指向 bus? → 执行 lcl_bus=>display( )

泛型调用 (Generic Calls)

将 up-cast 和多态结合,实现统一的处理逻辑

1
2
3
4
5
6
7
8
" 方法参数使用父类类型
METHODS: process_vehicle
IMPORTING io_vehicle TYPE REF TO lcl_vehicle.

" 可以传入任何子类
process_vehicle( lo_car ).
process_vehicle( lo_truck ).
process_vehicle( lo_bus ).

Up-cast vs Down-cast 对比

特性 Up-cast Down-cast
方向 子→父 父→子
赋值符 = ?=
安全性 总是安全 可能失败
视角 变窄 变宽
兼容性 变宽 变窄
何时使用 收集不同子类 恢复具体类型

考试/测试模式

场景/关键词 答案
“up-cast赋值符” =,隐式安全
“down-cast赋值符” ?=,显式,可能失败
“多态的核心” 父类引用调用方法,运行时动态绑定到子类实现
“何时需要down-cast” 需要访问子类特有组件时
“down-cast失败” 触发 CX_SY_MOVE_CAST_ERROR
“泛型调用的实现” 方法参数使用父类/接口类型

类、属性与方法 (★★★)

#abap-objects #oo-basics #class-definition #visibility

概览表

项目 关键点
类定义 CLASS ... DEFINITION + CLASS ... IMPLEMENTATION
属性 DATA(实例), CLASS-DATA(静态), CONSTANTS, TYPES
方法 METHODS(实例), CLASS-METHODS(静态)
可见性 PUBLIC / PROTECTED / PRIVATE

类定义结构

1
2
3
4
5
6
7
8
9
10
11
12
CLASS lcl_vehicle DEFINITION.
PUBLIC SECTION.
" 公有组件声明
PROTECTED SECTION.
" 受保护组件声明
PRIVATE SECTION.
" 私有组件声明
ENDCLASS.

CLASS lcl_vehicle IMPLEMENTATION.
" 方法实现
ENDCLASS.
📌类定义规则
  • CLASS 语句不能嵌套(不能在类中定义类)
  • 但可以为全局类定义局部辅助类
  • 可见性部分必须按 PUBLIC → PROTECTED → PRIVATE 顺序声明

属性类型对比

语法 类型 作用域 说明
DATA: attr TYPE type. 实例属性 每对象独立 每个对象有自己的一份
CLASS-DATA: attr TYPE type. 静态属性 全类共享 所有对象共用一份
CONSTANTS: c TYPE type VALUE v. 常量 不可变 声明时必须赋值
TYPES: t TYPE ... 局部类型 类内使用 与类相关的自定义类型
⚠️DATA 中的 TYPE 和 LIKE 限制
  • 在类的 DATA 语句中,只能使用 TYPE 引用数据类型
  • LIKE 仅允许用于本地数据对象SY字段(如 SY-DATE, SY-UNAME

READ-ONLY 属性

1
2
PUBLIC SECTION.
DATA: make TYPE string READ-ONLY.
  • 外部可以读取但不能修改
  • 只有类内部的方法可以修改
  • 只能在 PUBLIC SECTION 中使用

可见性 (Visibility)

1
2
3
4
5
6
7
8
9
10
┌─────────────────────────────────┐
│ PUBLIC SECTION (+) │ ← 外部可直接访问
│ 公有属性、方法、常量、类型 │
├─────────────────────────────────┤
│ PROTECTED SECTION (#) │ ← 仅子类和自身可访问
│ 受保护属性、方法 │
├─────────────────────────────────┤
│ PRIVATE SECTION (-) │ ← 仅自身可访问
│ 私有属性、方法 │
└─────────────────────────────────┘

封装 (Encapsulation) 的核心原则

  • 将属性设为私有 (PRIVATE)
  • 通过公有方法 (PUBLIC METHODS) 提供受控访问
  • 方法签名明确约束了参数的传递规则
  • 外部用户不需要了解内部实现
💡何时使用公有属性
严格来说公有属性违背OO原则。但在ABAP中:
  • 内表操作(READ TABLE, WHERE 子句)不能使用函数式方法
  • 这种特殊情况下可以使用公有只读属性 (READ-ONLY)

方法定义

方法签名

1
2
3
4
5
6
7
8
9
10
11
12
METHODS: set_type
IMPORTING
!iv_make TYPE string
!iv_model TYPE string
EXPORTING
!ev_result TYPE boolean
CHANGING
!cv_data TYPE any
RETURNING
VALUE(rv_count) TYPE i
EXCEPTIONS
error = 1.
参数类型 方向 说明
IMPORTING 调用者→方法 输入参数
EXPORTING 方法→调用者 输出参数
CHANGING 双向 输入输出参数
RETURNING 方法→调用者 函数式返回值(只能有一个)
📌RETURNING 参数
  • RETURNING 参数只能有一个
  • 使用 VALUE() 传递(值传递)
  • RETURNING 参数的方法称为函数式方法(Functional Method)
  • 不能同时有 EXPORTING/CHANGINGRETURNING

实例方法 vs 静态方法

类型 语法 可访问
实例方法 METHODS: meth. 实例属性 + 静态属性
静态方法 CLASS-METHODS: meth. 静态属性

考试/测试模式

场景/关键词 答案
“READ-ONLY限制” 只能在 PUBLIC SECTION 中使用
“LIKE在类中的使用” 仅限本地数据对象和SY字段
“CLASS嵌套” 不允许,但可以定义局部辅助类
“RETURNING参数限制” 只能有一个,使用值传递,不能与EXPORTING/CHANGING共存
“静态方法能访问什么” 只能访问静态属性,不能访问实例属性

接口 练习

#practice #interface #oo-design

📌核心模式
关键词 答案
接口定义 INTERFACES ~ 操作符访问,或使用 ALIASES 创建别名
多接口 一个类可以实现多个接口,弥补单继承的限制
接口多态 通过接口引用调用方法,实际执行类实现版本

题目1 - [记忆] 接口的定义和特点

请说明ABAP中接口(Interface)的定义方式和主要特点。

📌查看答案
接口定义语法:
1
2
3
4
INTERFACE lif Printable.
METHODS: print.
DATA: max_lines TYPE i.
ENDINTERFACE.

主要特点:

  1. 纯抽象:接口只包含声明(方法、属性、事件),不包含任何实现代码
  2. 无实例化:不能直接创建接口的实例(没有 CREATE OBJECT),只能创建实现该接口的类的对象
  3. 公有可见:接口中的所有成员默认是公有的,没有可见性分区(无PUBLIC/PROTECTED/PRIVATE)
  4. 命名约定:局部接口以 LIF_ 开头,全局接口以 IF_ 开头
  5. 实现语法:类使用 INTERFACES lif_name 来实现接口
  6. 设计作用:定义行为契约,实现类必须提供所有接口方法的实现

题目2 - [记忆] 接口与类的区别

请列举接口(Interface)和类(Class)之间的五个主要区别。

📌查看答案
特性 接口(Interface) 类(Class)
实现代码 仅包含声明,无实现代码 包含完整的实现
实例化 不能创建实例 可以创建实例(除抽象类外)
可见性 所有成员自动为公有 可定义PUBLIC/PROTECTED/PRIVATE
多重性 一个类可以实现多个接口 一个类只能继承一个父类
成员类型 可包含方法、属性、事件、常量 可包含所有类型的成员,包括构造函数
继承 接口可以继承其他接口(复合接口) 类单继承一个父类
设计用途 定义行为契约(”能做什么”) 定义对象的结构和行为(”是什么”)

题目3 - [记忆] 波浪号(~)操作符

请说明ABAP中波浪号(~)操作符在接口上下文中的用途和使用方式。

📌查看答案
波浪号()操作符用于限定接口方法的归属,格式为 `interface_namemethod_name`。

用途场景:

  1. 类实现中:在 IMPLEMENTATION 部分实现接口方法时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    CLASS lcl_report DEFINITION.
    PUBLIC SECTION.
    INTERFACES lif_printable.
    ENDCLASS.

    CLASS lcl_report IMPLEMENTATION.
    METHOD lif_printable~print.
    " 实现接口的print方法
    WRITE: / 'Printing report'.
    ENDMETHOD.
    ENDCLASS.
  2. 外部调用时:通过对象引用调用接口方法时(如果不使用别名)

    1
    2
    DATA(lo_report) = NEW lcl_report( ).
    lo_report->lif_printable~print( ).

注意:通过接口引用调用时不需要波浪号:

1
2
3
DATA: lo_printable TYPE REF TO lif_printable.
lo_printable = NEW lcl_report( ).
lo_printable->print( ). " 直接调用,无需 ~

题目4 - [记忆] 别名(ALIASES)

请说明ABAP中别名(ALIASES)的作用和语法。

📌查看答案
别名的作用:为接口中的方法创建一个更简短、更直观的替代名称,避免使用冗长的 interface~method 语法。

语法:

1
ALIASES alias_name FOR interface_name~method_name.

示例:

1
2
3
4
5
6
CLASS lcl_report DEFINITION.
PUBLIC SECTION.
INTERFACES lif_printable.
ALIASES print FOR lif_printable~print.
ALIASES get_page_count FOR lif_printable~get_page_count.
ENDCLASS.

使用别名后:

1
2
3
DATA(lo_report) = NEW lcl_report( ).
lo_report->print( ). " 使用别名,而非 lo_report->lif_printable~print( )
lo_report->get_page_count( ). " 使用别名

优点:

  1. 简化代码,提高可读性
  2. 隐藏接口名称的实现细节
  3. 当类实现多个接口时,别名可以消除同名方法的歧义

题目5 - [记忆] 接口引用与多态

请说明如何通过接口引用实现多态。

📌查看答案
接口引用实现多态的方式:
  1. 声明接口引用变量

    1
    DATA: lo_serializable TYPE REF TO lif_serializable.
  2. 将实现类的对象赋给接口引用(类似向上转换):

    1
    2
    lo_serializable = NEW lcl_order( ).   " lcl_order 实现了 lif_serializable
    lo_serializable = NEW lcl_product( ). " lcl_product 也实现了 lif_serializable
  3. 通过接口引用调用方法

    1
    lo_serializable->to_json( ).

    实际执行的是具体类(lcl_order 或 lcl_product)的实现版本

优势

  • 调用者只需要知道接口定义,不需要知道具体的实现类
  • 可以在运行时替换不同的实现,符合”依赖倒置原则”
  • 可以创建接口引用的内表,统一管理不同类型的对象
    1
    2
    3
    4
    5
    DATA: lt_objects TYPE TABLE OF REF TO lif_serializable.
    " 可以包含 lcl_order、lcl_product 等各种实现类的对象
    LOOP AT lt_objects INTO DATA(lo_obj).
    lo_obj->to_json( ). " 多态调用
    ENDLOOP.

题目6 - [应用] 实现多个接口

请编写一个类 lcl_document,同时实现两个接口 lif_printable(含方法 print)和 lif_saveable(含方法 save)。要求使用别名简化方法调用。

📌查看答案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
" 接口定义
INTERFACE lif_printable.
METHODS: print.
ENDINTERFACE.

INTERFACE lif_saveable.
METHODS: save
IMPORTING iv_path TYPE string.
ENDINTERFACE.

" 类定义
CLASS lcl_document DEFINITION.
PUBLIC SECTION.
INTERFACES: lif_printable, lif_saveable.
ALIASES: print FOR lif_printable~print,
save FOR lif_saveable~save.
METHODS: constructor IMPORTING iv_title TYPE string.
PRIVATE SECTION.
DATA: mv_title TYPE string.
ENDCLASS.

" 类实现
CLASS lcl_document IMPLEMENTATION.
METHOD constructor.
mv_title = iv_title.
ENDMETHOD.

METHOD lif_printable~print.
WRITE: / |打印文档: { mv_title }|.
ENDMETHOD.

METHOD lif_saveable~save.
WRITE: / |保存文档 '{ mv_title }' 到路径: { iv_path }|.
ENDMETHOD.
ENDCLASS.

使用方式:

1
2
3
4
5
6
7
8
9
" 通过别名调用
DATA(lo_doc) = NEW lcl_document( iv_title = '技术规格书' ).
lo_doc->print( ).
lo_doc->save( iv_path = '/tmp/spec.pdf' ).

" 通过接口引用调用(多态)
DATA: lo_printable TYPE REF TO lif_printable.
lo_printable = lo_doc.
lo_printable->print( ).

题目7 - [应用] 接口多态应用场景

系统中有三种日志记录器:文件日志(lcl_file_logger)、数据库日志(lcl_db_logger)和邮件日志(lcl_email_logger)。请设计一个接口使它们可以互换使用,并编写调用代码在运行时选择不同的日志记录器。

📌查看答案

接口设计

1
2
3
4
5
INTERFACE lif_logger.
METHODS:
log_info IMPORTING iv_message TYPE string,
log_error IMPORTING iv_message TYPE string.
ENDINTERFACE.

实现类(示例一个,其余类似)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CLASS lcl_file_logger DEFINITION.
PUBLIC SECTION.
INTERFACES lif_logger.
ALIASES: log_info FOR lif_logger~log_info,
log_error FOR lif_logger~log_error.
ENDCLASS.

CLASS lcl_file_logger IMPLEMENTATION.
METHOD lif_logger~log_info.
WRITE: / |[FILE-INFO] { iv_message }|.
ENDMETHOD.
METHOD lif_logger~log_error.
WRITE: / |[FILE-ERROR] { iv_message }|.
ENDMETHOD.
ENDCLASS.

运行时选择(多态调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DATA: lo_logger TYPE REF TO lif_logger.

" 根据配置选择不同的日志记录器
CASE lv_log_type.
WHEN 'FILE'.
lo_logger = NEW lcl_file_logger( ).
WHEN 'DB'.
lo_logger = NEW lcl_db_logger( ).
WHEN 'EMAIL'.
lo_logger = NEW lcl_email_logger( ).
ENDCASE.

" 统一调用,不需要知道具体实现
lo_logger->log_info( '系统启动' ).
lo_logger->log_error( '连接超时' ).

设计优势

  1. 新增日志记录器时只需实现接口,无需修改调用代码
  2. 运行时可灵活切换实现
  3. 符合开闭原则和依赖倒置原则

题目8 - [分析] 接口vs继承的设计选择

项目中需要设计一个支付系统。需求如下:

  • 有多种支付方式:信用卡、支付宝、微信支付、银行转账
  • 某些支付方式支持退款(如信用卡、支付宝),某些不支持(如某些银行转账)
  • 某些支付方式支持分期付款(如信用卡),某些不支持

请分析:应该使用继承还是接口来设计?请给出你的设计方案和理由。

📌查看答案

分析:接口优于继承

理由:

  1. 支付方式之间没有”is-a”层次关系:信用卡、支付宝等都是独立的支付手段,不存在谁是谁的子类的层次关系
  2. 行为组合而非层次继承:退款和分期是独立的能力(行为),不是类层次结构
  3. 灵活性:同一个支付方式可能有多个独立的能力组合

推荐设计方案(基于接口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
" 基础支付接口(所有支付方式必须实现)
INTERFACE lif_payment.
METHODS: pay IMPORTING iv_amount TYPE p.
ENDINTERFACE.

" 退款能力接口(可选实现)
INTERFACE lif_refundable.
METHODS: refund IMPORTING iv_amount TYPE p.
ENDINTERFACE.

" 分期能力接口(可选实现)
INTERFACE lif_installment.
METHODS: pay_in_installments
IMPORTING iv_amount TYPE p
iv_months TYPE i.
ENDINTERFACE.

实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
" 信用卡:支付 + 退款 + 分期
CLASS lcl_credit_card DEFINITION.
PUBLIC SECTION.
INTERFACES: lif_payment, lif_refundable, lif_installment.
ENDCLASS.

" 支付宝:支付 + 退款
CLASS lcl_alipay DEFINITION.
PUBLIC SECTION.
INTERFACES: lif_payment, lif_refundable.
ENDCLASS.

" 银行转账:仅支付
CLASS lcl_bank_transfer DEFINITION.
PUBLIC SECTION.
INTERFACES: lif_payment.
ENDCLASS.

调用方式

1
2
3
4
5
6
7
8
" 所有支付方式统一处理
DATA: lt_payments TYPE TABLE OF REF TO lif_payment.

" 检查是否支持退款
IF lo_payment IS INSTANCE OF lif_refundable.
DATA(lo_refundable) = CAST lif_refundable( lo_payment ).
lo_refundable->refund( iv_amount = '100.00' ).
ENDIF.

设计原则总结

  • “是什么”(身份)用继承
  • “能做什么”(能力/行为)用接口
  • 支付方式的核心是”行为能力”的组合,因此接口是最合适的设计选择

📌模式总结
关键词 答案
接口定义 INTERFACE … ENDINTERFACE,纯声明无实现,成员默认公有
接口 vs 类 接口无实现、无实例、可多实现;类有实现、可实例化、单继承
~操作符 interface~method 格式,在实现和通过对象引用调用时使用
ALIASES 为 interface~method 创建简短别名,提高可读性
接口多态 接口引用指向不同实现类对象,调用同一方法执行不同行为
多接口实现 INTERFACES if1, if2, …,一个类可实现多个接口
设计选择 “是什么”用继承,”能做什么”用接口
0%