Apex 企业模式:Domain 与 Selector 层

掌握 Apex 企业模式的两大核心层:Domain 层封装对象级业务逻辑(验证、默认值、触发器逻辑),Selector 层集中管理 SOQL 查询与安全强制,实现关注点分离、可测试、易维护的代码架构。...

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

学习目标

slide_2

完成本模块后,你将能够:

  • 用 EAA(企业应用架构)模式概括 Domain 模式的起源。
  • 判断哪类 Apex 代码应放在 Domain 层。
  • 解释 Domain 层在应用架构和平台中的位置。
  • 按照平台最佳实践设计 Domain 层。

前置条件:本模块基于 Apex Enterprise Patterns: Service Layer 模块中所学的原则。该模块讨论了 Service 层如何封装应用的业务流程。如果还没学过,请先完成该模块再继续。

Domain 层

slide_3

Domain 层(Domain layer)封装的是作用于单一对象类型上的业务逻辑。每个 Salesforce 对象对应一个 Domain 类。放在 Domain 类中的内容包括:

  • 字段默认值逻辑(设置默认值)
  • 验证逻辑(执行业务规则)
  • 触发器逻辑(复杂的触发器处理方法)
  • 针对该对象的自定义业务方法

Domain 模式把「做什么」(Service 层负责编排)和「对这个对象怎么做」(Domain 层)分离开。它的好处是:业务逻辑按对象集中、可跨触发器/服务/控制器复用、可独立测试、无论从哪个入口进入行为都一致。

在 Salesforce 平台企业模式中,Domain 层由你创建的自定义对象(如 project、invoice)来定义。而对象的行为(验证、计算、复杂数据操作)既可以声明式(点选式)表达,也可以用 Apex 编程表达,把二者有效结合是成为成功开发者的关键。

Domain 模式与设计考量

slide_4

如果对象某些行为的复杂度需要 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 层原则

slide_5

本单元动手实现 Domain 类:字段默认值、验证规则、触发器逻辑封装、自定义业务方法、安全强制与测试。

实现字段默认值与验证

slide_6

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 实现复杂的跨字段逻辑。这些方法默认在整个记录列表上操作,天然是批量安全的。

触发器逻辑与自定义方法

slide_7

触发器逻辑(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 类

slide_8

控制安全强制(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 层原则

slide_9

本单元学习 Selector 层——封装某个对象的 SOQL 查询逻辑,把数据访问与业务逻辑分离。

认识 Selector 层

slide_10

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 模式与设计

slide_11

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 层原则

slide_12

本单元深入 Selector 实现:自定义查询方法、Query Factory 模式、部分字段选择、FieldSet 支持与安全强制。

自定义 Selector 方法与 Query Factory

slide_13

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 与安全强制

slide_14

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