Apex 企业模式:Service 层

掌握 Apex 企业模式的 Service 层:理解关注点分离(SOC)的四层架构,用 Service 编排跨对象业务流程,用 Unit of Work 事务化管理 DML。本文系统讲解 SOC 的分层理念与适用时机、Service 层的八大设计考量(命名、批量、安全、参数编组、组合服务、无状态等),以及 fflib_SObjectUnitOfWork 的实践,构建生产级、可测试的 Apex 架构。...

📅 2026/10/4 ✍️ ponybai 🏷️ salesforce, developer, apex, headless

一、理解关注点分离(SOC)

slide_2

本单元介绍关注点分离(Separation of Concerns,SOC)——本模块一切内容背后的架构原则。你将理解 SOC 的含义、它为何不只是代码复用、它在 Salesforce 上如何映射为四层架构,以及何时值得投入、何时属于过度设计。

引言:软件像生命体一样演化

slide_3

软件,像生命一样,会随着时间变化和演化。从单细胞的「Hello World」程序,到企业级软件的复杂性,生命的多样性与广度同样体现在软件中。复杂的生物会演化出专门的系统——骨骼、肌肉、器官各司其职,作为一个整体工作,同时又与其他系统交互以服务整体。

复杂的企业应用也是如此:把不同的关注点分离到不同系统或层中,能让代码更易导航、更易维护,修改时对其他区域的冲击和回归更小,从而演化出更健康、更易适配的程序。这就是关注点分离的基础。

关注点分离(SOC)

slide_4

关注点分离(SOC)就是把应用划分成不同的层,每层只承担单一职责。在 Salesforce 上是四层模型:

  1. 表现层(Presentation)——UI(LWC、Aura、Visualforce),用户看到和交互的部分。
  2. 业务逻辑层(Business Logic)——Service + Domain,编排流程、执行规则,即「该发生什么」。
  3. 数据访问层(Data Access)——Selector,查询和数据检索,即「如何获取数据」。
  4. 数据库层(Database)——Salesforce 存储,由平台自身管理。

每一层都只有一个变化理由,这才能让整个系统保持可维护。

SOC 只是代码复用的花哨说法吗?

slide_5

是……也不全是。代码复用通常是被动的:当两个地方需要同一段逻辑时,你把代码片段搬进共享的 utility 类(往往丢进 MyUtil 之类的通用类),然后继续。这没问题,但不是架构。

SOC 更深:它要求你对应用内部结构做前瞻性思考——类命名约定、编码规范、每类逻辑该放哪里。目标是让代码持久且自描述,好的代码应该「讲故事」。复用发生在事后;SOC 是在你写第一行代码之前就设计好的。

SOC 的好处是什么?

slide_6

高层次上,每个应用都有三样东西:存储(storage)、逻辑(logic)和与它们交互的方式(interaction)。当你把它们分开,就定义了层,每层有各自的关注点和对其他层的职责。三大好处:

  • 演化(Evolution)——随技术和需求变化,一层可以被扩展、重写甚至舍弃(想想过去十年换了多少 JavaScript 框架)。
  • 影响管理(Impact Management)——修改或舍弃一层不应波及其他无关层。
  • 角色与职责(Roles & Responsibility)——每层各司其职,丢弃某个客户端技术或库并不意味着丢失业务逻辑,因为那是另一层的职责。

表现层(Presentation Layer)

slide_7

表现层是用户看到和接触的一切。声明式(点选):Layouts、Record Pages、Flow、Record Types、Formulas、Reports & Dashboards。编码:Apex 控制器、Visualforce、Lightning 组件。

关键洞察:这一层是可替换的——如果你想明年把 Visualforce 页面换成 Lightning Web Component,底层的业务逻辑完全不应改变,因为它住在另一层。

业务逻辑层(Business Logic Layer)

slide_8

业务逻辑层是规则所在——「该发生什么」。声明式:Formula、Flow、Validation Rules、Sharing Rules、Approval Processes。编码:Apex Services、Apex Custom Actions、异步 Apex。

这是你投入最多、也最想保护的一层,因为它编码了你组织独特的业务规则,是应用的「跳动的心脏」。保护这一层免受 UI 变动和数据访问细节的侵蚀,正是关注点分离的全部意义。

数据访问层(Data Access Layer)

slide_9

数据访问层负责「如何获取数据」。声明式:Data Loaders、Salesforce Connect。编码:SOQL、SOSL、Salesforce APIs。

这是 Selector 模式所在——每个对象一个专用类封装其所有查询。当所有查询逻辑集中在一处,就能避免分散在触发器、控制器、服务里的重复、不一致查询这个经典问题。

数据库层(Database Layer)

slide_10

数据库层是 Salesforce 自己的数据存储。声明式:Custom Objects、Fields、Relationships、Rollups。编码:Apex Triggers。

关键点:这一层由平台管理——你不用写存储引擎,只需定义数据的形状,让 Salesforce 处理其余部分。触发器属于这里,因为它们与记录操作紧密耦合,但其逻辑应委托给更高层的 Domain 类。

何时在 Salesforce 上不需要 SOC

slide_11

SOC 并非普遍必需。Salesforce 最大的好处之一就是声明式模型——无需写一行代码就能创建对象、字段、布局、校验规则、工作流和公式字段。声明式开发更快更易,且已经实现了一定程度的关注点分离。

如果你的应用高度数据为中心、能大部分用声明式交付,就别重新造轮子——用平台内置的层即可。声明式开发本身就是一种架构层(即使它不是代码)。简单场景确实可能不值得引入完整 SOC 架构的复杂度。

何时在 Salesforce 上使用 SOC

slide_12

当你的应用变成流程为中心(而非数据为中心)时,就该采用 SOC。需要 SOC 的信号:

  • 添加新 UI——不想重写与界面无关的业务逻辑。
  • 构建面向公众的 API——@AuraEnabled 方法不是干净的 API 边界。
  • 通过 Batch Apex 扩展——无论处理十条还是一千万条记录,都希望得到一致结果。
  • 控制器逻辑复杂——仅靠 MVC 无法隔离业务逻辑。
  • 新开发者入职——清晰的分层让他们容易找到代码该放哪。

经验法则:从简单开始,随复杂度(和痛感)增长再加层。

二、学习 Service 层原则

slide_13

第二单元介绍 Service 层。你将学习 Service 模式是什么、谁在调用它、保持其干净可复用的设计考量,以及它如何融入平台。这是编排跨多个对象业务流程的层——协调 Domain、Selector 和 Unit of Work 的「指挥」。

引言:什么是 Service 层?

slide_14

Martin Fowler 把 Service 层定义为:「用一层服务定义应用的边界,建立一组可用操作,并协调应用在每个操作中的响应」。实践中,它是进入应用逻辑的关键入口。

Service 层让你对业务任务、计算和流程形成清晰、严格的封装。设计目标是纯粹与抽象——Service 层必须能被移动应用、UI 表单、富 Web UI 或 REST API 使用,而无需知道或关心是谁在调用它。

谁在使用 Service 层?

slide_15

技术上,消费者被称为「客户端(client)」——与用户或系统交互的代码,而非直接的人。想想 Apex 在 Salesforce 平台上所有被调用的方式:UI 控制器、Apex Web 服务、REST 服务、Flow 调用的 Invocable 方法、入站邮件处理器、Batch Apex、定时 Apex、Queueable Apex——这些都是客户端。

注意缺失了什么:Apex 触发器。触发器属于 Domain 层,因为它与记录操作紧密耦合。所有入口都汇入 Service 层,因此业务逻辑只存在于一处。把 Service 层逻辑「泄漏」到其他层的 Apex 代码中,会侵蚀 Service 层的价值,因为这种不一致最终会反映到用户体验上。

平台创新与适应性

slide_16

Salesforce 不断推出新的平台功能。想象如果代码与某个特定功能紧密绑定,每个新功能都会迫使你重构;或者更糟,因为害怕重构会破坏现有功能而复制代码。

干净的 Service 层能把业务逻辑与特定平台功能隔离开,让你无需重构核心逻辑就能采纳新功能。这种适应性是前期投资 Service 层的最大回报之一——它是你应对平台演化的「保险」。

设计考量(1):命名与平台亲和性

slide_17

命名约定:Service 方法应以业务操作而非特定客户端来表达。InvoiceService.calculateTax(...)(基于业务操作)对任何客户端都有意义;InvoiceService.handleTaxCodeForACME(...)(基于特定客户)暴露了特定调用方,应该让你不安。

平台/调用方亲和性:在 Salesforce 上必须设计支持批量(bulkification)的方法签名。calculateTax(List<TaxCalculation>) 能一次处理上百条;calculateTax(Invoice, TaxInfo) 会迫使调用方循环重复调用,耗尽 governor limits。

设计考量(2):SOC 与安全

slide_18

SOC 考量:Service 层作为编排者跨对象处理流程;而校验、字段默认值、计算等发生在记录插入更新期间,属于相关对象,由 Domain 层或触发器承担。别模糊这条线。

安全:Service 类默认应以用户安全运行——用 with sharing 修饰符(尤其用 global 暴露时)。如果确实需要访问用户可见性之外的记录,用私有内部类标记 without sharing 尽可能短暂地提权。

设计考量(3):参数编组与组合服务

slide_19

参数编组(Marshalling):不要过度规定 Service 层如何处理错误,因为调用方有自己的呈现方式——Visualforce 用 <apex:pagemessages>,定时任务发邮件、发 Chatter 或记日志。Service 应该直接抛异常让调用方决定,或返回类似 Database.insert 的部分更新反馈。

组合服务(Compound services):与其强迫客户端连续多次调用,不如提供把多次调用组合成一次的组合服务,避免低效往返和事务问题,同时仍可暴露更细粒度的服务。

设计考量(4):事务与配置

slide_20

事务管理与无状态:不同调用方有不同的状态管理需求——单次请求、跨 scope 的 Batch Apex、维护页面状态的复杂 UI。所以让 Service 无状态:方法调用之间不保留实例变量,数据库操作和事务 scope 完全包含在每个方法内,调用方不必自己管理 SavePoint。

配置:通过 Options 参数支持行为覆盖——让调用方指示「不提交」「不发邮件」等,用于预览/试算(what-if)功能,实现为带共享 Options 对象的方法重载。

在 Apex 中使用 Service

slide_21

看单个 Service 方法 OpportunitiesService.applyDiscounts 如何在不同地方使用。从 Lightning 组件:组件提示用户输入折扣百分比,调用 Service,自己处理错误(Lightning 有自己的错误呈现方式)。从 Batch Apex execute():批处理处理记录块,异常处理不同——也许记录日志或重试。

要点:Service 保持中立,每个客户端把同一段业务逻辑适配到自己的上下文和错误处理需求。同一个 Service,不同的客户端,不同的错误语义——这是设计使然。

Apex Service 层的其他好处与考量

slide_22

除复用和适应性外,Service 层还能带来更好的测试和并行开发。用工厂模式 + Apex 接口动态解析实现,能让测试中的 mock 服务更干净。提前定义 Service 层就能实现契约式设计(Design by Contract):需要调用服务的团队可以用返回静态数据的 dummy 实现来构建,而实现服务的团队独立开发真实逻辑,双方并行不互相阻塞——对大团队是巨大胜利。

总结:投资 Service 层

slide_23

投资 Service 层提供真正的工程好处:更大的复用和适应性,以及更干净、更省成本的 API 实现——这在云集成时代必不可少。紧密遵循我们讨论的封装和设计考量(命名、批量、SOC、安全、参数编组、组合服务、无状态、配置),你就为应用形成了一个持久核心,是穿越不断变化、创新时代的坚实投资。

三、在 Apex 中应用 Service 层原则

slide_24

第三单元动手实践。你将在 Apex 中创建一个 Service 类,并将其暴露为 API。这是理论落到实践的地方——编写封装业务操作的静态方法,再把这些操作提供给外部调用方。

创建 Service

slide_25

创建 Service 的标准做法:一个带静态方法的类,每个方法代表服务的一个操作。每个方法通过参数获取所需信息,更新数据库或返回值,用自定义 Apex 异常或结果类型表示失败。具体例子:对一组 Opportunity(及其 line items)应用折扣。

可加配置重载:带 Options 参数的重载版本跳过提交,让调用方拿到折扣后的值做预览。若不同 Opportunity 需要不同折扣,传参数类而非单个折扣值。

将 Service 暴露为 API

slide_26

若你的 Service 已充分测试、健壮且对任何客户端开放,把它暴露给 Apex 开发者的最简单方式是改修饰符 public → global。但如果在构建 AppExchange 包,global 会影响版本间方法签名变更,需理解其含义。

面向平台外调用方(移动、IoT),可用 REST 协议暴露自定义 Apex REST API。异常处理留给调用方,平台会把异常编组为 JSON/XML 响应。专业提示:暴露一些 @InvocableMethod,让 Flow Builder 用户无需写代码即可调用你的 Service。

四、学习 Unit of Work 原则

slide_27

第四单元介绍 Unit of Work 模式——以事务方式管理 DML 操作:注册插入、更新、删除,然后按正确顺序一次性提交。它减少重复的事务管理代码,让 Service 方法更干净。

Unit of Work 原则

slide_28

Unit of Work 是 Martin Fowler 描述的设计模式:「维护一个受业务事务影响的对象列表,协调变更的写出和并发问题的解决」。在 Salesforce 平台上,它处理:记录更新/插入/删除、记录关系(简化子记录插入)、提交时批量处理所有捕获记录、用 SavePoint 包装 DML(开发者无需每个 Service 方法重复实现)。它不是 Service 层的必需,但确实有帮助。

不用 Unit of Work 实现 Service 方法

slide_29

要看 UOW 的价值,先看不用它时每个 Service 方法要写的样板代码。DML 批量化和优化:手动创建和填充列表,追踪哪些记录需要更新。错误处理和事务:用 SavePoint + try/catch 保证「要么全提交、要么都不提交」。

为什么 SavePoint 重要:假设第二个 DML 失败——若调用方不处理异常,整个事务(含第一个 DML)回滚;但若调用方捕获异常却没恢复 SavePoint,运行时会提交第一个 DML,造成部分更新。UOW 消除所有这些样板。

Unit of Work 模式的 Apex 实现

slide_30

Apex 实现位于开源库 fflib_SObjectUnitOfWork 类。register 方法捕获记录:registerNew(创建)、registerDirty(更新)、registerDeleted(删除)、registerRelationship(关联)。commitWork 方法封装 SavePoint + try/catch。

关键:只有 commitWork 真正写数据库,所以 Service 代码可在循环里随意调用 register 方法。在 Service 方法中用:① 初始化一个 UOW 实例界定范围;② 逻辑执行时注册记录;③ 调用一次 commitWork。Service 之间调用时,通过重载传外层 UOW 实例——不要新建,一个方法调用只应有一个 UOW 实例。

用 Unit of Work 实现 Service 方法

slide_31

把 UOW 应用到同一个 Service 方法会彻底改变它:手动列表消失了,SavePoint/try-catch 样板消失了(由 fflib_SObjectUnitOfWork 在 commitWork 里处理)。代码现在只关注业务逻辑。

对跨多个深度和类的复杂代码,可把 UOW 实例传下去——被调用代码继续注册自己的数据库更新,Service 层作为所有者代表所有人执行单次提交或回滚。结果:更干净、更短、更易读。

五、在 Apex 中应用 Unit of Work 原则

slide_32

最后一个单元把 Unit of Work 投入真实场景——从零创建一个 Opportunity 及所有依赖记录。你会看到代码变得多干净,并为动手挑战做好准备。

用 Unit of Work 动手实践

slide_33

看一个复杂场景:从零创建 Opportunity 及所有必需依赖记录——数量惊人。不用 UOW 时,测试 setup 代码冗长,到处都是手动追踪的列表。用 UOW:registerNew 插入记录(并自动给子记录应用正确的父 ID),registerRelationship 关联记录,commitWork 按父先子后的正确顺序执行 DML。

代码不仅更短,而且逻辑上更易读——不再手动追踪那些列表。这就是完整模式:Service → Domain → Unit of Work。

总结:各层各司其职

slide_34

把应用看作各层,各有各的考虑和关注点。就像生物体,每个部分各司其职,让应用健壮、持久、可适应。本模块中,你把应用的「跳动心脏」——业务逻辑——隔离进了 Service 层。

你可以到此为止,也可以继续把关注点分离应用到对象行为(触发器代码)和查询,这正是 Apex Enterprise Patterns: Domain & Selector Layers 模块继续讲的内容。

挑战准备

slide_35

完成动手挑战需要部署一些开源库。fflib_SObjectUnitOfWork 类是 Apex Common(fflib-apex-common)库的一部分,它依赖 ApexMocks(fflib-apex-mocks)框架。安装顺序:先 ApexMocks,再 Apex Common。两个仓库都提供 Deploy to Salesforce 链接,可直接把库装进你的 org。


文章来源:Trailhead - Apex Enterprise Patterns: Service Layer