学习目标
完成本模块后,你将能够:
- 用 EAA(企业应用架构)模式概括 Domain 模式的起源。
- 判断哪类 Apex 代码应放在 Domain 层。
- 解释 Domain 层在应用架构和平台中的位置。
- 按照平台最佳实践设计 Domain 层。
前置条件:本模块基于 Apex Enterprise Patterns: Service Layer 模块中所学的原则。该模块讨论了 Service 层如何封装应用的业务流程。如果还没学过,请先完成该模块再继续。
Domain 层
Domain 层(Domain layer)封装的是作用于单一对象类型上的业务逻辑。每个 Salesforce 对象对应一个 Domain 类。放在 Domain 类中的内容包括:
- 字段默认值逻辑(设置默认值)
- 验证逻辑(执行业务规则)
- 触发器逻辑(复杂的触发器处理方法)
- 针对该对象的自定义业务方法
Domain 模式把「做什么」(Service 层负责编排)和「对这个对象怎么做」(Domain 层)分离开。它的好处是:业务逻辑按对象集中、可跨触发器/服务/控制器复用、可独立测试、无论从哪个入口进入行为都一致。
在 Salesforce 平台企业模式中,Domain 层由你创建的自定义对象(如 project、invoice)来定义。而对象的行为(验证、计算、复杂数据操作)既可以声明式(点选式)表达,也可以用 Apex 编程表达,把二者有效结合是成为成功开发者的关键。
Domain 模式与设计考量
如果对象某些行为的复杂度需要 Apex 编码,应考虑用面向对象编程(OOP)技术和 Domain Model 模式来组织和重构代码。Domain Model(Fowler 定义)是「一个融合了行为与数据的领域对象模型」。与 Service 层一样,Domain Model 提供了更细粒度的代码封装和复用,例如复杂验证、默认值以及复杂的计算和操作逻辑。
Domain 类主要在两个地方被消费:
- Apex 触发器(triggers):用户或工具通过标准 Salesforce UI 或 API 交互时,对自定义对象的 CRUD 操作会被路由到对应对象的 Domain 类。
- Apex 服务(services):Service 层代码通过 Domain 类复用与一个或多个对象相关的逻辑,从而把服务层聚焦在「编排业务流程」上。
重要:Lightning Web Component 控制器、Visualforce 控制器、Batch Apex 和 API 类应只使用 Service 层暴露的功能;Domain 层代码通常作为应用的「内部业务逻辑」,总是被服务层客户端类间接调用。
Domain 类的结构如下(继承 fflib_SObjectDomain):
public class Accounts extends fflib_SObjectDomain {
public Accounts(List<Account> sObjectList) {
super(sObjectList);
}
public override void onBeforeInsert() {
// 默认值逻辑
}
public override void onValidate() {
// 验证逻辑
}
}
触发器保持极简,只做分发(一行实例化 Domain 并调用方法):
trigger AccountTrigger on Account (before insert, before update) {
fflib_SObjectDomain.triggerHandler(Accounts.class);
}
Service 也可以直接使用 Domain 类:Accounts accounts = new Accounts(accountList); accounts.validate();。
应用 Domain 层原则
本单元动手实现 Domain 类:字段默认值、验证规则、触发器逻辑封装、自定义业务方法、安全强制与测试。
实现字段默认值与验证
fflib_SObjectDomain 基类(来自 Apex Enterprise Patterns 开源库)为所有 Domain 类提供触发器处理功能和对象安全等能力。它用模板方法模式(template method pattern)提供标准钩子:onValidate() 用于验证记录、onApplyDefaults() 用于默认字段值。
字段默认值(field defaulting)——重写 onBeforeInsert()(或 onApplyDefaults()),在记录首次创建时设置默认值:
public override void onBeforeInsert() {
for (Account acct : (List<Account>) records) {
if (acct.Type == null) {
acct.Type = 'Prospect'; // 默认值
}
}
}
把逻辑放在这里,能确保记录新增时默认值在整个应用内一致。
验证(validation)——重写 onValidate(),在保存前执行业务规则:
public override void onValidate() {
for (Account acct : (List<Account>) records) {
if (acct.AnnualRevenue > 0 &&
acct.NumberOfEmployees == 0) {
acct.addError('Employees required if revenue specified');
}
}
}
验证用 addError()——它会阻止保存并向用户显示消息,效果等同于验证规则,但用 Apex 实现复杂的跨字段逻辑。这些方法默认在整个记录列表上操作,天然是批量安全的。
触发器逻辑与自定义方法
触发器逻辑(trigger logic):超出简单验证和默认值的复杂操作,例如重写 onAfterInsert() 来创建关联记录、发送通知或更新父记录。
自定义方法(custom methods):针对该对象的业务逻辑:
public void updateOpportunityStage() {
for (Account acct : (List<Account>) records) {
// 根据 Account 变化更新关联 Opportunity 的阶段
}
}
// Service 调用 Domain:
Accounts accts = new Accounts(accountList);
accts.updateOpportunityStage();
Domain vs Service:Domain 层负责「这个对象记录」的逻辑,Service 层负责「编排多个对象」的逻辑。Domain 是单对象专家,Service 是多对象编排者——Service 调用 Domain 方法,Domain 处理对象级细节。这种分离让两层都可测试、可维护。
安全与测试 Domain 类
控制安全强制(security enforcement):Domain 类可以强制或绕过安全:
SObjectDomain.setEnforceSecurity(true); // 强制对象和字段权限 SObjectDomain.setEnforceSecurity(false); // 系统级操作可绕过
默认情况下 fflib_SObjectDomain 强制 Salesforce 对象的 CRUD 安全;但服务逻辑可能想代表用户访问对象而不要求其拥有该对象权限,此时可在构造函数中禁用默认行为。
测试 Domain 类:Domain 类天然可测试——无需触发器执行,只需实例化、传入测试记录、直接调用方法并断言结果:
@IsTest
static void testValidation() {
Account acct = new Account(AnnualRevenue=100);
Accounts domain = new Accounts(new List<Account>{acct});
domain.onValidate();
// 断言添加了错误
}
把逻辑分解成更小的封装单元,有利于测试驱动开发(TDD),也让你能对服务层做更增量的测试。
学习 Selector 层原则
本单元学习 Selector 层——封装某个对象的 SOQL 查询逻辑,把数据访问与业务逻辑分离。
认识 Selector 层
Selector 层(Selector layer)封装的是针对单一对象类型的 SOQL 查询。每个对象对应一个 Selector 类。放在 Selector 类中的内容包括:标准 CRUD 查询(selectById、selectAll)、针对特定需求的自定义查询方法、动态 SOQL 构造的查询工厂、字段集(FieldSet)支持和部分字段选择,以及安全强制(对象/字段/共享)。
好处:某个对象的所有查询集中在一处、SOQL 不再散落在触发器/服务/控制器中、一次编写处处复用、单元测试中可 mock Selector、字段选择和安全强制保持一致。
把 SOQL 查询散落在各层,会随复杂度增长带来三类问题:
- 查询不一致:同样的查询从不同地方发出(或略有差异),复制粘贴过程中可能丢失某些条件。
- 查询数据不一致:查询到的记录字段不固定,调用方无法保证哪些字段已查询,容易触发
System.SObjectException: SObject row was retrieved via SOQL without querying the requested field运行时错误。 - 安全不一致:Apex 需遵守运行用户的对象安全,但开发者很容易在查询前忘记做安全检查,且在单元测试中不易发现。
Selector 模式正是为解决这些问题而生。
Selector 模式与设计
Selector 模式基于 Martin Fowler 的 Mapper 模式——「在对象与数据库之间移动数据的映射器层」。之所以叫 Selector 而非 Mapper,是因为在 Salesforce 语境中,它并非把结果集映射到数据对象,而是直接提供 SObject 记录(平台表达查询数据的原生方式),本质上是「选择数据」。
在关注点分离(SOC)上,Selector 负责:可见性/可复用性/可维护性(集中查询逻辑,用编译期字段引用,字段删除时平台会阻止);查询数据的可预测性(保证最小字段集,作为 Selector 对全 org 的「契约」);安全(让调用方选择是否强制共享和权限检查);平台亲和性(用集合表达条件,鼓励批量调用)。
Selector 类的结构如下(继承 fflib_SObjectSelector):
public class AccountsSelector extends fflib_SObjectSelector {
public List<Schema.SObjectField> getSObjectFieldList() {
return new List<Schema.SObjectField>{
Account.Id, Account.Name,
Account.Industry, Account.AnnualRevenue
};
}
public override Schema.SObjectType getSObjectType() {
return Account.SObjectType;
}
public List<Account> selectById(Set<Id> ids) {
return (List<Account>) selectSObjectsById(ids);
}
}
继承的方法提供标准查询(selectById、selectAll),自定义方法添加具体查询逻辑。关键规则:Selector 类之外不允许有任何内联 SOQL——Service、Domain 和控制器都通过 Selector 访问数据。
应用 Selector 层原则
本单元深入 Selector 实现:自定义查询方法、Query Factory 模式、部分字段选择、FieldSet 支持与安全强制。
自定义 Selector 方法与 Query Factory
fflib_SObjectSelector 是抽象基类,扩展它至少要实现 getSObjectType() 和 getSObjectFieldList() 两个抽象方法。它还提供组织功能依赖字段(如多币种下的 CurrencyIsoCode)、可选的 FieldSet 字段、以及对象级安全检查(用户无对象读权限时抛异常,可用构造函数参数禁用)。
自定义 Selector 方法:在保持字段与排序一致的前提下,为特定业务添加查询:
public List<Account> selectByIndustry(String industry) {
return (List<Account>) Database.query(
newQueryFactory()
.setCondition('Industry = :industry')
.toSOQL());
}
Query Factory 方式:基类用 fflib_QueryFactory(builder 模式,流式接口)以面向对象方式构造 SOQL,比字符串拼接更健壮、更不易出错。newQueryFactory() 帮助方法可创建工厂实例,再链式调用:
.setCondition()—— WHERE 子句.setLimit()—— LIMIT 子句.addOrdering()—— ORDER BY.selectField()—— 添加特定字段.setEnforceSecurity()—— 启用 FLS/安全检查
部分字段选择和跨对象查询:给 newQueryFactory(false) 传 false 可忽略 getSObjectFieldList() 中的默认字段,再用 selectField() 添加特定字段(含关联的 Account、User 字段),并可把结果包装成显式暴露字段的小 Apex 类(如 OpportunityInfo),给调用方更安全、自文档化的契约。
FieldSet 与安全强制
FieldSet 支持:让管理员无需改代码即可控制查询哪些字段。通过 selectFieldSet() 动态注入管理员在 FieldSet 中配置的字段:
public List<Account> selectByFieldSet(Schema.FieldSet fieldSet) {
return (List<Account>) Database.query(
newQueryFactory()
.selectFieldSet(fieldSet)
.toSOQL());
}
这让 Selector 半动态化,可配合需要已查询字段的 Lightning 页面或 Visualforce 页面使用。若对象上有多个 FieldSet,也可以把 FieldSet 作为参数传给自定义 Selector 方法,而不必在类级别注册。
安全强制(security enforcement):默认情况下 fflib_SObjectSelector 在运行用户没有对象读权限时抛异常,但默认不强制字段级安全,除非显式启用。安全分三层:
- 对象级:检查用户是否可读该对象。
- 字段级:只查询用户可访问的字段(需显式
.setEnforceSecurity())。 - 共享规则:遵循组织范围的默认共享设置(用
with sharing关键字;如需查询所有记录,可在私有内部类上用without sharing显式提升)。
安全被内建在 Selector 层——每个查询都尊重用户的权限,避免意外的数据泄露。这就是数据访问的纵深防御。
文章来源:Trailhead - Apex Enterprise Patterns: Domain & Selector Layers












