尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ABAP经典Dynpro程序重构:MVC分层+状态机驱动屏幕流转

ABAP经典Dynpro程序重构:MVC分层+状态机驱动屏幕流转 刚接手一个老模块池程序Module Pool时你多半也被那几十个 Dynpro 屏幕的跳转关系折磨过列表页点“新增”进明细页明细页按“返回”不一定回列表点“保存”又可能直接退到初始屏幕业务逻辑和屏幕控制全部堆在 PAI 里全局变量一个比一个多。我后来把这块重构为“MVC 分层 状态机驱动屏幕流转”的结构半年下来维护成本降了不止一个量级。这篇文章就用一个订单维护流程当例子我把改造思路、核心类设计、实操步骤和踩过的坑一次写清楚适合被 Dynpro 跳转逻辑搞到头大、又不想上 Web Dynpro/Fiori 的 ABAP 开发同学参考。多数人一听 MVC想到的是 Spring MVC 或 ASP.NET MVC再不然是 SAP 自家的 Web Dynpro。其实 MVC 只是一种分层的责任心划分方式跟具体技术栈没有绑定关系。经典 Dynpro 程序照样可以把数据Model、屏幕呈现View、流程控制Controller拆清楚。再往深一层多个 Dynpro 之间的跳转本质就是一个状态机屏幕是状态用户操作是事件从当前屏幕能触发什么动作、到哪里去全部由迁移规则决定。把这套规则收口到一张配置表和一个 Controller 里你就告别了散落在几十个 PAI 模块里的SET SCREEN和LEAVE TO SCREEN。1. 为什么经典 Dynpro 流程会失控项目里的真实痛点1.1 传统 PAI/PBO 跳转代码的“面条式”演进一个典型的 SAP 模块池程序屏幕之间通常用SET SCREEN、LEAVE TO SCREEN、CALL SCREEN互相连接。刚开始屏幕数量少三五个 Dynpro 互相跳转还挺清晰。等到需求越接越多屏幕从 5 个涨到 20 个麻烦就来了。最常见的问题有几个。第一跳转逻辑散落在各个屏幕的 PAI 模块里每个模块各自SET SCREEN 2000你想知道“从哪些屏幕能到屏幕 2000”只能全局搜索代码搜出来的结果可能还有两处是死代码根本跳不过去。第二回到上一层屏幕时经常把“是正常返回还是出错返回”写混。比如明细界面按“ESC”返回列表和保存失败后“返回列表重新编辑”的跳转目标完全不同现实代码里经常是同一个SET SCREEN 1000连中间该刷新的数据都没区分。第三屏幕多了以后事务代码的初始屏幕只是其中一个 Dynpro用户在某些边缘路径下会直接进到不该进的屏幕。这种代码维护起来最典型的特征是“改一个跳转要冒很大风险”。改 A 屏幕的返回逻辑担心影响 B、C、D 屏幕对 A 的调用想加一个中间确认屏幕又怕某些回调路径没有经过确认。面多了水多了最后全靠人肉测试找出所有可用路径。1.2 MVC 出现在 ABAP 历史中的必然性MVC 的初衷是让“数据、界面、业务动作”各司其职。放到经典 Dynpro 里的对应关系其实很自然Dynpro 本身和屏幕字段就是 View事务中的全局数据、数据库表读写就是 Model而 PAI 模块里的OK_CODE判断、SET SCREEN跳转、业务功能调用就是 Controller 的工作。但你去看很多老代码这三层是搅在一起的。业务逻辑写在 Dynpro 的 PAI 里面屏幕字段直接当全局变量用SY-UCOMM判断完马上又是UPDATE又是LEAVE TO SCREEN。一屏代码干了三件事出问题自然不好定位。MVC 化不一定要CREATE OBJECT一堆类才是“面向对象”哪怕你还是写PERFORM只要把数据访问集中到一个类/函数组、屏幕跳转集中到一个流程表就已经在向 MVC 靠拢了。1.3 状态机视角把用户操作变成状态迁移状态机的核心就三样状态State、事件Event、迁移Transition。放到 Dynpro 程序中状态就是当前屏幕号Dynpro Number事件就是用户触发的功能码比如“保存”是 SAVE、“返回”是 BACK、“下一步”是 NEXT。迁移规则就是“当前状态 事件 → 目标状态 要执行的动作”。把屏幕流转上升为状态机之后有个非常明显的好处你可以用一个二维表回答“从屏幕 X 按了功能键 Y应该去哪里”。这个表甚至可以直接做成配置表或者一个方法里的 CASE而不是东一榔头西一棒子地藏在几十个 PAI 里。更重要的是你能在程序启动时把这张表加载进内存所有的跳转统一走同一个函数彻底杜绝“有人手滑直接写了SET SCREEN”的情况。2. 思路拆解从 Dynpro 流程到 MVC 状态机2.1 划分 Model / View / Controller 的边界我在做重构时最先做的不是写代码而是在纸上把每个 Dynpro 的职责标清楚。Model 层负责业务数据和持久化。比如订单主数据程序订单抬头、行项目的数据结构数据库表读写校验逻辑计算逻辑都放这里。Model 不关心字段在哪个屏幕上显示也不关心用户按了哪个按钮。View 层就是每一个 Dynpro 屏幕。这层包括屏幕的 Layout、字段属性、搜索帮助、表格控件等。Dynpro 的 PBO 模块可以把数据从 Model 搬运到屏幕字段PAI 模块可以把屏幕字段搬回 Model。但不碰业务规则尤其不碰跳转。Controller 层是核心调度器。它接收 PBO 和 PAI 事件调用 Model 处理数据再选择下一个 View。在 ABAP 里Controller 可以是一个全局类也可以是一个全局函数组里的几个函数。Controller 最关键的地方是接管所有SET SCREEN/LEAVE TO SCREEN调用业务代码里不能有人绕过 Controller 自己跳屏。分层边界定死了很多历史遗留问题自然消失。比如屏幕上某个字段触发值请求你现在可以精确知道是去 Model 取值还是去查配置表PAI 里按下“保存”后业务更新逻辑不会和屏幕跳转逻辑搅在一起。2.2 状态定义屏幕号、操作、迁移表的统一建模状态机的建模我一般用一个结构GS_STATE里面至少包含当前屏幕号MV_DYNNR、当前流程标记MV_MODE比如新增/修改/显示、以及业务上下文对象引用。不一定要多复杂但一定要保证“当前在哪里、正在做什么”能随时问得出来。迁移表我做成内部表GT_TRANSITION字段大概长这样字段说明FROM_DYNNR来源屏幕号EVENT_CODE触发事件FCODE 或功能码TO_DYNNR目标屏幕号ACTION_METHOD迁移时要执行的 Controller 方法DESCRIPTION说明给维护的人看有了这张表程序的跳转逻辑就变成了一次查询根据当前屏幕和用户功能码找到对应记录先执行ACTION_METHOD再执行SET SCREEN TO_DYNNR。表中没有记录说明该操作在当前状态下非法直接拒绝并提示用户这比“无声无息留在当前屏”更符合用户体验。2.3 为什么状态机适合 Dynpro 这种线性流程SAP 经典屏幕流程天然是线性的一个事务里屏幕按顺序出现一个业务操作往往跨越多个屏幕。拿订单创建举例查询页 → 明细页 → 确认页带返回路径维护模式又有“修改中不允许跳转”“保存之前必须经过确认”等业务语义。这些语义用状态机表达简直再合适不过。如果你有状态机的基础Verilog 里的三段式状态机也好、嵌入式里的层次状态机也好思路都是同一个把“调度逻辑”和“业务执行逻辑”解耦调度逻辑只看状态和事件业务逻辑只负责做具体动作。放到 ABAP 里也一样Controller 只负责“该不该跳、跳到哪”Model 只负责“数据变了没、落库了没”。这套模式对 Dynpro 这种有明确步骤的场景非常友好。还有一个容易被忽略的好处测试性。因为跳转规则集中了你可以写一个单元测试或者一个简单的测试程序模拟“从屏幕 1000 触发 EVENT NEW断言目标屏幕是 2000”不至于为了验证一个跳转路径去跑完整调用链。3. 落地实现核心类与内部表的搭建3.1 基础数据结构状态、事件、迁移表在 ABAP 里我倾向于把状态机相关类型定义到 TOP Include 或者一个局部类的定义区。先类型定义后数据声明不要到处都是DATA: OK_CODE TYPE SY-UCOMM却不知道属于哪一层。TYPES: BEGIN OF ty_transition. INCLUDE TYPE ty_state. TYPES: event_code TYPE sy-ucomm, to_dynnr TYPE sy-dynnr, action_method TYPE char30, description TYPE char100. TYPES: END OF ty_transition. TYPES: BEGIN OF ty_state. dyynnr TYPE sy-dynnr, 当前屏幕号 mode TYPE char10, DISPLAY/INSERT/UPDATE object_key TYPE vbeln, 业务主键示例 TYPES: END OF ty_state. DATA: gs_state TYPE ty_state, gt_transition TYPE STANDARD TABLE OF ty_transition WITH EMPTY KEY.这里我习惯把GS_STATE放到全局数据因为 Controller 和 Model 都可能要访问它。但注意全局数据越少越好能通过方法参数传的就不要全部开成全局。迁移表初始化直接在程序启动时调用一个BUILD_TRANSITION_TABLE方法填充。如果跳转规则需要业务人员维护可以把它做成配置表用SAP 表维护生成器生成维护视图。但对大多数项目来说代码内维护反而更安全因为一旦上配置表还要考虑缓冲、传输、权限成本不低。3.2 Controller 核心流程PBO/PAI 的调用分发Controller 类我一般命名为LCL_SCREEN_CONTROLLER核心方法有三个PBO在屏幕输出前调用根据当前状态准备数据PAI在用户操作后调用接收 FCODE执行迁移EXECUTE_TRANSITION真正执行跳转所有SET SCREEN和LEAVE TO SCREEN都从这里走。PAI方法看起来类似这样METHOD pai. DATA: ls_transition TYPE ty_transition. READ TABLE gt_transition INTO ls_transition WITH KEY from_dynnr gs_state-dynnr event_code iv_fcode. IF sy-subrc 0. MESSAGE i001(zfi) WITH 当前屏幕不允许此操作. RETURN. ENDIF. 先执行业务动作Model 方法 CALL METHOD mo_model-(ls_transition-action_method) EXCEPTIONS error. IF sy-subrc 0. RETURN. ENDIF. 再执行跳转 gs_state-dynnr ls_transition-to_dynnr. SET SCREEN gs_state-dynnr. ENDMETHOD.这里有个关键点所有跳转都走EXECUTE_TRANSITION并且由GS_STATE-DYNNR统一维护当前屏幕。你不再需要关心调用者是从哪个屏幕跳过来的因为控制器内部天然知道当前状态。3.3 Model 层设计数据容器与业务流程解耦Model 层的职责是数据读写和业务规则我通常定义为LCL_MODEL。它包含业务数据结构比如订单头、行项目表、数据库读写方法INSERT_ORDER、UPDATE_ORDER、READ_ORDER、数据校验方法。在重构初期Model 可以只是一个函数组或者一个局部类先把所有UPDATE、SELECT、MOVE-CORRESPONDING从 PAI 里搬进来。不要一上来就设计几十个方法先从“哪个 PAI 操作最频繁”开始比如保存、刷新、查询三个方法逐步把散落的逻辑收拢。Model 和 View 之间的数据交换要尽量用结构体而不是直接操作屏幕字段。比如说屏幕字段S_KUNNR不能直接出现在 Model 方法里应该先MOVE-CORRESPONDING到GS_ORDER_DATA再传给 Model。这样以后把 View 换成 ALV 或者 Web 界面Model 不需要改。3.4 View 层封装Dynpro 字段和屏幕控件管理对于经典 DynproView 层往往不是一个类而是一组 PBO/PAI 模块加上屏幕布局。但“封装”依然可以做主要体现在三个方面。第一PBO 模块里只做“数据填充”和“字段属性控制”不写业务逻辑。例如控制某个字段是否可输入可以在 PBO 里根据GS_STATE-MODE直接LOOP AT SCREEN设置SCREEN-INPUT 0但不要在这里调数据库。第二PAI 模块里只做“数据搬入 调 Controller”不直接UPDATE也不直接SET SCREEN。第三屏幕上的表格控件、下拉框、搜索帮助尽量封装成独立方法方便复用。做到这一步以后即使是 Dynpro 改到 ALV 界面你也只需要调整 View 层Controller 和 Model 的代码基本不动。这正是 MVC 在 ABAP 项目里最大的价值——界面形态在变业务逻辑稳定得很。4. 实操过程与核心环节实现4.1 从零搭建一个 Demo客户主数据维护流程先说调试一个标准场景一个简单的客户主数据维护事务有三个屏幕1000查询/列表页功能码NEW、OPEN、EXIT2000明细页功能码SAVE、BACK、NEXT3000确认页功能码CONFIRM、BACK按状态机来建迁移表大概是当前屏幕功能码目标屏幕动作1000NEW2000CREATE_NEW_ORDER1000OPEN2000READ_ORDER2000SAVE3000CHECK_ORDER_DATA2000BACK1000SAVE_LOG可选3000CONFIRM1000SAVE_ORDER_DB3000BACK2000返回编辑有了这张表你在 PAI 里的代码会变得非常短。MODULE pai_1000 INPUT. CASE sy-ucomm. WHEN NEW OR OPEN. CLEAR gs_order_data. mo_controller-handle_user_command( sy-ucomm ). WHEN EXIT. LEAVE PROGRAM. ENDCASE. ENDMODULE.注意NEW和OPEN都交给 ControllerController 根据当前状态到迁移表找目标屏幕再去调 Model 准备数据。这比你直接在 PAI 里写IF SY-UCOMM NEW. SET SCREEN 2000. ENDIF.干净得多。4.2 状态迁移的代码怎么写CASE METHOD vs 配置表迁移表里ACTION_METHOD字段我前面提到了这其实是一个非常值得琢磨的设计点。方案一是直接 CASE METHOD 名字在 Controller 里写一个大 CASE 分发好处是代码明白易跟踪坏处是每加一个动作就要改 Controller 类。CASE ls_transition-action_method. WHEN CREATE_NEW_ORDER. mo_model-create_new_order( ). WHEN READ_ORDER. mo_model-read_order( gs_state-object_key ). WHEN SAVE_ORDER_DB. mo_model-save_order( ). ENDCASE.方案二是用“函数指针”或者“方法引用”直接调用ABAP 里方法引用比函数指针稍麻烦但新版本也可以做到。DATA: mo_method TYPE REF TO if_abap_behavior_method_call. 示意实际项目里我建议用方案一 接口规范。Controller 里 CASE 的分支数量等同于业务动作数量不可能特别多而且每个分支代码量很小。真要严格避免 CASE 膨胀可以把 ACTION_METHOD 设计成 Model 的公共方法名然后通过TRY CALL METHOD动态调用但动态调用牺牲了编译期检查维护并不舒服。我个人倾向 CASE 分发直观可靠。4.3 多 Dynpro 之间传参全局数据 VS Model 上下文MVC 重构最大的冲突点之一就是“原来几十个全局变量怎么办”。刚重构时屏幕之间传参自然想靠全局结构体这不违反 MVC因为 View 和 Model 之间本来就要有数据载体。但要注意全局结构体要统一不能你定义了GS_ORDER别人又定义GV_KUNNR。我建议屏幕字段全部映射到一个GS_ORDER_DATA结构体字段名和屏幕字段尽量对应然后用MOVE-CORRESPONDING做批量传值。更好的做法是把“当前业务上下文”封装成 Model 的一个实例属性。假设MO_MODEL是 Model 对象那么MO_MODEL-GS_ORDER_DATA保存当前编辑中的订单数据屏幕 PAI 负责把字段搬到MO_MODEL-GS_ORDER_DATAPBO 负责把MO_MODEL-GS_ORDER_DATA搬到屏幕字段。这样你不再需要“全局变量搬家”数据依赖变得清晰。4.4 PAI/PBO 中如何避免重复调用和循环触发ABAP 经典 Dynpro 有个经典坑PAI 模块里调用了一个对话框比如CALL SCREEN或者POPUP_TO_CONFIRM返回之后当前屏幕会重新执行 PBO甚至可能再次触发 PAI 的后续逻辑。如果不设计好代码可能会出现“一次保存三次写库”的诡异现象。状态机化之后PBO/PAI 的重复调用有了更好的控制手段。我常用的做法是设置一个GV_PAI_LOCK标志在一个 PAI 动作进入后立即置位在 PBO 中根据它决定是否重新加载数据。另外所有会触发屏幕刷新的操作都通过 Controller 分发Controller 内部可以在真正SET SCREEN之前先检查目标屏幕是否跟当前屏幕一样一样就只刷新数据不重复跳转。还有一个小技巧LEAVE TO SCREEN和SET SCREEN的区别要分清楚。LEAVE TO SCREEN会离开当前屏幕直接进入目标屏幕SET SCREEN则会在当前 PAI 结束后再跳转。在状态机迁移中我默认使用SET SCREEN因为这样能够保证当前屏幕 PAI 的收尾逻辑先执行完。只有确实需要“立即退出并打开其他屏幕”的场景才用LEAVE TO SCREEN。5. 常见问题与排查技巧实录5.1 常用问题速查表现象可能原因排查方向点“返回”没反应迁移表缺少 BACK 事件记录查GT_TRANSITION是否包含当前屏幕BACK保存时数据没写入Model 方法未在迁移动作中被调用检查 ACTION_METHOD 拼写和 CASE 分支屏幕字段被清空PBO 中数据搬移时机不对确认 PBO 是否在SET SCREEN后已经执行跳转后进到错误屏幕某个 PAI 模块里有手工SET SCREEN全项目搜索SET SCREEN/LEAVE TO SCREEN确认对话框点“是”后重复刷新PAI 锁标志未正确控制增加GV_PAI_LOCK处理完再重置状态机表中找不到对应迁移状态数据GS_STATE-DYNNR未更新在SET SCREEN前先更新当前状态这个表我在团队内部一直保留着每次新人碰到屏幕跳转问题先查表对照能省不少排查时间。5.2 状态机中“非法操作”的防御处理状态机最大的优势是可以拒绝非法操作。很多老程序的问题不在于“能跳”而在于“不该跳的时候也能跳”。比如订单明细页处于“修改模式”用户按“删除行项目”之后紧跟着按“保存”有可能直接把未校验的修改存进库里。状态机化之后你可以把“保存”事件限定在“明细页 数据校验通过”的状态下。如果用户在某些中间状态触发保存Controller 查迁移表直接提示“当前状态不允许该操作”。这看起来是限制了用户实际上是在保护业务数据。我还喜欢加一个“状态可回溯”的地址栈。Controller 内部维护STACK_OF_SCREENS在迁移前把旧状态压栈必要时实现“NEXT 钻取后 BACK 返回上一屏”。这比用 Dynpro 自带的SET SCREEN栈机制更可控因为业务上你可能希望 Back 回到的是列表页而不是上一个编辑页。5.3 调试技巧如何串起多个 Dynpro 的调用链状态机重构之后调试反而比之前简单因为所有跳转都集中了。但“集中”也意味着一旦 Controller 出问题所有屏幕都跑不了。我的建议是先在 Controller 里加日志METHOD execute_transition. 调试日志记录状态迁移 MESSAGE s001(zfi) WITH |State: { gs_state-dynnr } Event: { iv_fcode } - Target: { ls_transition-to_dynnr }|. ENDMETHOD.生产环境可以把它改成写应用日志表开发环境直接看消息条数就能判断是否发生了异常跳转。配合 SAP 调试器里的断点可以很清晰看到每一次迁移的“前因后果”。另外一个实用技巧是在PBO模块第一行和PAI模块第一行打上条件断点断点条件写成SY-DYNNR 2000 AND SY-UCOMM BACK这样就不会被无关屏幕的 PBO 断点打断能快速定位到特定屏幕的特定操作。5.4 首次重构最容易踩的三个坑重构不是一蹴而就的我见过程序员试图一步到位把所有屏幕状态机化结果改动面太大测试根本排不过来。建议分三步走。第一步先只做迁移表收口把所有SET SCREEN/LEAVE TO SCREEN集中到一个 Controller 类的方法里哪怕内部还只是一个大 CASE不算真正的状态机至少跳转路径变得可查了。第二步再把“当前屏幕”抽成GS_STATE让 Controller 方法判断来源时统一读它删除散落的SY-DYNNR判断。第三步最后考虑把跳转规则做成迁移表让“新增屏幕”变成“加一条配置记录”。另一个坑是过度设计。有的同事一开始就画了一堆接口、抽象类、工厂方法结果每个屏幕的差异很大抽象层全是空方法。我建议按“实际有差异的地方”抽象不要为了设计模式而设计。状态机适合流程固定的屏幕流如果你的业务屏幕之间跳转本来就很少比如只有一个屏幕强行状态机化只会增加代码量。最后是数据传递的坑。重构时很容易出现“Model 数据还没初始化Controller 已经调了 SAVE”的并发问题。解决办法是在迁移动作里显式声明依赖比如READ_ORDER动作执行前必须先调用INITIALIZE_ORDER这一点可以通过 OO 上抛异常做到而不是依赖程序员记住调用顺序。6. 一些经验总结与扩展方向做状态机化 Dynpro 重构不是把代码从“能用就好”变成“表演设计模式”而是要把维护成本真正降下来。我个人的经验是当屏幕数量超过 6 个、跨屏幕的 Back 路径超过 3 条时状态机的收益已经非常明显少于这个数量你直接用简单的 CASE 也没毛病不用硬套。这套模式后续还可以往两个方向扩展。一是把迁移表做成 Z 表后台配置维护这样业务顾问可以根据流程变化调整屏幕跳转但这不是必须。二是结合 ALV 增强把列表页从 Dynpro 改为 ALV GridView 层重构后 Model 和 Controller 不受影响正好体现 MVC 的价值。如果在实际项目里你还是感到“拆不动”一个务实的建议是别急着重构所有屏幕先挑一个路径最长、维护最痛苦的事务下手。拿订单创建流程当试点把迁移表跑通让团队看到效果比一开始就铺开全部屏幕稳妥得多。经典 Dynpro 虽然老但维护到位的程序会告诉你老技术不等于糟糕设计它只是需要更认真的组织方式。
返回列表