用 FFLIB 实现 Apex 企业设计模式
Apex 企业设计模式将应用分为服务层、模型层、选择逻辑层、工作单元几个部分。FFLIB 是一个开源的 Apex 框架,可以帮助开发者快速建立相关的功能。
FFLIB 的安装
FFLIB 可以直接部署到需要使用的 Salesforce 系统中。在其 GitHub 主页上可以点击 “Deploy to Salesforce” 按钮直接进行部署。
FFLIB 中的关键类
在 FFLIB 中,有一些关键的类可以帮助开发者实现 Apex 的企业设计模式。
- fflib_SObjectSelectot:实现了 fflib_ISObjectSelector 接口,用于实现选择逻辑层,其中可以定义对象的查询逻辑等
- fflib_SObjectDomain:实现了 fflib_ISObjectDomain 接口,用于实现模型层,其中可以定义某个对象的内部逻辑
- fflib_SObjectUnitOfWork:实现了 fflib_ISObjectUnitOfWork 接口,用于实现工作单元模式,其中包含了数据增删修改的各种逻辑
- fflib_Application:包含了初始化 Apex 企业设计模式各部分的函数,可以作为使用各个部分的统一入口
FFLIB 应用实例
下面我们通过一个简单的例子阐述如何实现设计模式的各个部分。
功能包括:
- 建立一个简单的关于 Account 的 Visualforce 页面,其中包含一个输入框和按钮。
- 当用户输入文字并点击按钮后,系统会首先查找名字和用户输入相同的 Account 记录,如果找不到则建立这样一个 Account 记录。
- 在每个 Account 记录创建之后,建立一个同名的 Contact 对象。
包含以下几个类:
- App_Application:应用框架,使用 fflib_Application 中定义的工厂方法来初始化 Apex 企业设计模式的各部分
- AccountSelector:选择逻辑层
- AccountService:服务层
- Accounts:模型层
- AccountTrigger:Account 对象的触发器类
示例代码
App_Application 类:
public without sharing class App_Application {
public static final fflib_Application.UnitOfWorkFactory unitOfWork = new fflib_Application.UnitOfWorkFactory(
new List<SObjectType> {
Account.SObjectType,
Contact.SObjectType
}
);
public static final fflib_Application.ServiceFactory service = new fflib_Application.ServiceFactory(
new Map<Type, Type> {
AccountService.IService.class => AccountService.class
}
);
public static final fflib_Application.SelectorFactory selector = new fflib_Application.SelectorFactory(
new Map<SObjectType, Type> {
Account.SObjectType => AccountSelector.class
}
);
public static final fflib_Application.DomainFactory domain = new fflib_Application.DomainFactory(
App_Application.selector,
new Map<SObjectType, Type> {
Account.SObjectType => Accounts.class
}
);
}
App_Application 类中使用工厂方法给出了各个部分的初始化逻辑。注意每个工厂方法的参数,它们包含了对象类型和相应的类。在下面的各个部分中我们会直接调用 App_Application 类的成员。
AccountSelector 类:
public with sharing class AccountSelector extends fflib_SObjectSelector {
public static AccountSelector newInstance() {
return (AccountSelector) App_Application.selector.newInstance(Account.SObjectType);
}
/*
* 实现了 fflib_SObjectSelector 中的抽象函数,用于返回当前类所关联的 SObject 对象类型
*/
public Schema.SObjectType getSObjectType() {
return Account.SObjectType;
}
/*
* 实现了 fflib_SObjectSelector 中的抽象函数,用于提供默认搜索时得到的字段
*/
public List<Schema.SObjectField> getSObjectFieldList() {
return new List<Schema.SObjectField> {
Account.Name,
Account.Id
};
}
/*
* 实现了 fflib_SObjectSelector 中的抽象函数,通过一组 ID 的值来查询一组 Account 对象
*/
public List<Account> selectById(Set<Id> idSet) {
return (List<Account>) selectSObjectsById(idSet);
}
/*
* 自定义函数,用于查找 Name 字段和给定的值相同的 Account 对象
*/
public List<Account> selectByName(String name) {
return [SELECT Name FROM Account WHERE Name = :name];
}
}
AccountSelector 类相对简单。除了实现 fflib_SObjectSelector 类中的抽象函数,我们自己定义了一个按名称查找的函数,可供服务层调用。
Accounts 类:
public with sharing class Accounts extends fflib_SObjectDomain {
public Accounts(List<SObject> SObjectList) {
super(SObjectList);
}
/*
* 对于 fflib_SObjectDomain 类中的钩子函数的重写,在插入记录之后自动建立一个 Contact 对象
*/
public override void onAfterInsert() {
fflib_ISObjectUnitOfWork uow = App_Application.unitOfWork.newInstance();
createContact(uow);
uow.commitWork();
}
/*
* 自定义函数,对于每个 Account 记录,建立一个同名的 Contact 记录
*/
public void createContact(fflib_ISObjectUnitOfWork uow) {
for (Account acc : (List<Account>) Records) // Records 变量是 fflib_SObjectDomain 中定义的 List<SObject> 类型的成员,表示此类包含的记录
{
Contact c = new Contact(LastName = acc.Name);
// 将新建的 Contact 对象关联到 Account 对象中
// 这个函数的第二个参数表明了 Contact 对象中和 Account 对象关联的字段
uow.registerNew(c, Contact.AccountId, acc);
}
}
// 每个继承了 fflib_SObjectDomain 类都必须有的内部类
public class Constructor implements fflib_SObjectDomain.IConstructable {
public fflib_SObjectDomain construct(List<SObject> SObjectList) {
return new Accounts(SObjectList);
}
}
}
Accounts 类是模型层,里面定义了一个函数,用来在 Account 对象下面创建一个 Contact 对象。
对于内部类 Constructor,这是 FFLIB 中的一个约定,每一个继承了 fflib_SObjectDomain 类都必须有这一个内部类。
fflib_SObjectDomain 类实现了一个 TriggerHandler 函数,并提供了若干钩子函数,可以和触发器类结合使用,从而使得对象在被增删修改之后可以自动执行相应的逻辑。由于 Apex 缺乏完整的反射机制,在进行触发器操作时,模型层无法直接得到需要处理的记录。这个内部类实现了 fflib_SObjectDomain.IConstructable 接口的 construct 函数,从而可以将需要处理的数据传递给模型层。
AccountTrigger 类:
trigger AccountTrigger on Account (before insert, after insert) {
fflib_SObjectDomain.triggerHandler(Accounts.class);
}
触发器类很简单,直接调用了 fflib_SObjectDomain 中的 triggerHandler 函数,将模型层的类作为参数传进去。
AccountService 类:
public class AccountService implements IService {
public static IService newInstance() {
return (IService) App_Application.service.newInstance(IService.class);
}
public interface IService {
void createAccount(String name);
void createAccount(fflib_ISObjectUnitOfWork uow, String name);
}
/*
* 新建 Account 记录
*/
public void createAccount(String name) {
fflib_ISObjectUnitOfWork uow = App_Application.unitOfWork.newInstance();
createAccount(uow, name);
uow.commitWork(); // 将数据存入数据库
}
/*
* 核心逻辑,重载 createAccount 函数,查找相应的 Account 记录,如果找不到则新建
*/
public void createAccount(fflib_ISObjectUnitOfWork uow, String name) {
AccountSelector selector = (AccountSelector) AccountSelector.newInstance();
List<Account> accList = selector.selectByName(name);
// 如果不存在相应的 Account 记录,才创建
if (accList.isEmpty()) {
Account newAcc = new Account(Name = name);
uow.registerNew(newAcc); // 将 newAcc 记录标记为新记录,等待使用 commitWork 函数来存入数据库
}
}
}
AccountService 类的逻辑很简单,提供了 createAccount 函数,从而让外部代码可以调用并创建 Account 对象。
注意,我们重载了 createAccount 函数。
第一个函数只包含一个参数,即“名字”,让外部代码直接调用即可创建 Account 对象。
第二个函数包含了“名字”和“工作单元”两个参数,并且不包含 commitWork 函数,从而可以被外部代码单独调用,只实现创建记录的逻辑。外部代码可以调用其他的各种逻辑,也可以定义将数据写入数据库的时间。
Visualforce 页面:
<apex:page controller="AccountTestFflibController">
<apex:form>
<apex:inputText label="输入客户名字" value="{!name}"/>
<apex:commandbutton value="创建" action="{!create}" />
</apex:form>
</apex:page>
Visualforce 控制器:
public class AccountTestFflibController {
public String name {get; set;}
public PageReference create(){
AccountService service = (AccountService) AccountService.newInstance();
service.createAccount(name);
return null;
}
}
可以看到,在 Visualforce 控制器中,我们只调用了一次服务层的 createAccount 函数,就完成了所有相关的逻辑。
小结
上述示例只是使用了 FFLIB 中的一些基本功能,实现了 Apex 企业设计模式的基本结构。
FFLIB 还提供了其他的功能和辅助函数,在实际应用中,可以和其他的框架或现有的代码结合,提高代码的维护和更新效率。
对于 FFLIB 的单元测试,可以使用 ApexMocks 框架。
ApexMocks 框架
ApexMocks 框架是为 Apex 的单元测试开发,主要提供了模拟数据的功能。
在 Apex 开发中,我们始终要对开发的类做出单元测试,并且代码覆盖率要不小于75%。为了对功能进行全面的测试,我们往往需要准备很多数据,并将它们插入数据库(当然,在测试结束后 Salesforce 会自动将这些数据删除)。随之而来的问题就是单元测试的效率会随着准备数据的复杂度而降低。
ApexMocks 中提供了多种方法让我们来创建模拟数据,并可以和 Apex 企业设计模式结合,使得我们的单元测试不用真正的对数据库进行操作,从而提高测试效率。
安装
ApexMocks 可以直接部署到需要使用的 Salesforce 系统中。在其 GitHub 主页上可以点击 “Deploy to Salesforce” 按钮直接进行部署。
关键函数
在 ApexMocks 框架中,最关键的函数就是 setMock 函数。通过它,我们可以对要进行测试的类进行依赖注入,让我们的逻辑使用模拟的数据进行测试。
使用步骤
在使用 ApexMocks 进行模拟数据和测试的时候,一般遵循以下四个步骤:
- 建立模拟对象(Create mocks)
- 建立模拟数据(Given)
- 执行测试步骤(When)
- 检验测试结果(Then)
代码示例
让我们使用之前建立的 AccountService 类来进行单元测试。
测试类中包含两个函数,一个测试在没有任何 Account 存在时,新的 Account 对象可以被建立,另一个测试当已经存在同名的 Account 对象时,没有新的 Account 对象被创建。
@isTest(isParallel=true)
public class AccountServiceTest {
@IsTest
private static void shouldCreateAccount()
{
// Create mocks
fflib_ApexMocks mocks = new fflib_ApexMocks();
fflib_ISObjectUnitOfWork uowMock = new fflib_SObjectMocks.SObjectUnitOfWork(mocks); // 建立工作单元的模拟
AccountSelector selectorMock = (AccountSelector) mocks.Mock(AccountSelector.class); // 建立选择逻辑层的模拟
// Given
String testAccountName = 'Test Existing Account';
App_Application.unitOfWork.setMock(uowMock); // 使用 setMock 设置选择逻辑层的模拟
App_Application.selector.setMock(selectorMock); // 使用 setMock 设置工作单元的模拟
// When
AccountService service = (AccountService) AccountService.newInstance();
service.createAccount(testAccountName);
// Then
fflib_ArgumentCaptor argument = fflib_ArgumentCaptor.forClass(fflib_ISObjectUnitOfWork.class);
((fflib_ISObjectUnitOfWork) mocks.verify(uowMock, 1)).registerNew((Account) argument.capture()); // 验证 registerNew 函数被执行了一次,并且其中的参数是 Account 类型的
((fflib_ISObjectUnitOfWork) mocks.verify(uowMock, 1)).registerNew((Account) fflib_Match.anyObject()); // 另一种验证,registerNew 函数被执行了一次,并且其中的参数是任意 Account 类型的任何对象
}
@IsTest
private static void shouldNotCreateAccount()
{
// Create mocks
fflib_ApexMocks mocks = new fflib_ApexMocks();
fflib_ISObjectUnitOfWork uowMock = new fflib_SObjectMocks.SObjectUnitOfWork(mocks);
AccountSelector selectorMock = (AccountSelector) mocks.Mock(AccountSelector.class);
// Given
String testAccountName = 'Test Existing Account';
/*
* 下面这段代码使用 stub API 来模拟选择逻辑层的函数 selectByName 的执行结果:
* 当其参数是变量 testAccountName 的值的时候,返回一个 Account 对象
*/
mocks.startStubbing();
List<Account> existingAccounts = new List<Account> {
new Account(
Id = fflib_IDGenerator.generate(Account.SObjectType), // 建立随机的一个 ID 值
Name = testAccountName)
};
mocks.when(selectorMock.sObjectType()).thenReturn(Account.SObjectType);
mocks.when(selectorMock.selectByName(testAccountName)).thenReturn(existingAccounts);
mocks.stopStubbing();
App_Application.unitOfWork.setMock(uowMock);
App_Application.selector.setMock(selectorMock);
// When
AccountService service = (AccountService) AccountService.newInstance();
service.createAccount(testAccountName);
// Then
fflib_ArgumentCaptor argument = fflib_ArgumentCaptor.forClass(fflib_ISObjectUnitOfWork.class);
((fflib_ISObjectUnitOfWork) mocks.verify(uowMock, 0)).registerNew((Account) argument.capture()); // 验证 registerNew 函数没有被执行
}
}
代码解释:
- 我们在 “Then” 部分进行验证的时候,使用了 fflib_ArgumentCaptor 类。这个类的作用是得到在模拟的工作单元中使用的参数。然后,我们可以使用 mocks.verify 函数来验证相应的函数 “registerNew” 是否在模拟工作单元 “uowMock” 中被调用了一次,并且参数是 “Account” 类型
- 在 shouldNotCreateAccount 函数中,为了模拟选择逻辑层的输出结果,我们使用了 Apex 提供的 stub API 功能。这样做的好处是我们不需要考虑选择逻辑层的正确与否(选择逻辑层有自己的测试函数来测试),只需要设置它的输出结果,然后用来测试当前的服务层函数即可。我们使用了 mocks.when(functionA(paramA)).thenReturn(B) 函数来设置当调用函数 functionA 并且其参数是 paramA 时,返回值是 B
- 注意这一行 “mocks.when(selectorMock.sObjectType()).thenReturn(Account.SObjectType);”,这是必须的设置,用来告诉模拟的选择逻辑层去和 Account 类型相关联。否则在接下来的执行中,模拟的选择逻辑层会给出 null 作为结果
- 在对 “Then” 部分进行验证时,不一定要使用 ApexMocks 提供的类或函数。我们也可以直接使用默认的 System.assert 系列函数来测试运行结果
小结
ApexMocks 框架提供了非常强大的模拟对象功能,我们在上文中只给出了很简单的示例。
将 ApexMocks 和 FFLIB 结合使用可以显著地提高单元测试的运行效率。
用 FFLIB 实现 Apex 企业设计模式的更多相关文章
- Apex 企业设计模式
FFLIB 是一个免费的框架,对 Apex 进行了扩展.它的结构实现了 Salesforce 推荐的Apex 企业设计模式. 在学习如何使用 FFLIB 框架之前,我们先来了解一下 Apex 企业设计 ...
- Java设计模式学习资源汇总
本文记录了Java设计模式学习书籍.教程资源.此分享会持续更新: 1. 设计模式书籍 在豆瓣上搜索了一把,发现设计模式贯穿了人类生活的方方面面.还是回到Java与程序设计来吧. 打算先归类,再浏览,从 ...
- ASP.NET 设计模式(转)
Professional ASP.NET Design Patterns 为什么学习设计模式? 运用到ASP.NET应用程序中的设计模式.原则和最佳实践.设计模式和原则支持松散耦合.高内聚的代码,而这 ...
- 【设计模式系列】之OO面向对象设计七大原则
1 概述 本章叙述面向向对象设计的七大原则,七大原则分为:单一职责原则.开闭原则.里氏替换原则.依赖倒置原则.接口隔离原则.合成/聚合复用原则.迪米特法则. 2 七大OO面向对象设计 2.1 单一 ...
- Head First设计模式分析学习
永不放弃的毅力,和对欲望的控制. 注意:要能够理解相类似的设计模式之间的区别和不同.可以把类比列举出来,加深记忆. 是否加入Spring容器中的标准是是否要用到Spring框架的方法或者功能特性,如事 ...
- asp.net资料! (.NET) (ASP.NET)
使用SqlBulkCopy类加载其他源数据到SQL表 在数据回发时,维护ASP.NET Tree控件的位置 vagerent的vs2005网站开发技巧 ASP.NET2.0小技巧--内部控件权限的实现 ...
- 身为java程序员你需要知道的网站(包含书籍,面试题,架构...)
推荐几本书<高级java程序员值得拥有的10本书>, 首页 所有文章 资讯 Web 架构 基础技术 书籍 教程 我要投稿 更多频道 » - 导航条 - 首页 所有文章 资讯 Web ...
- Java编程之路相关书籍(三个维度)
一.关于Java的技术学习.能够依照以下分三个维度进行学习 : (1)向下发展,也就是底层的方向 建议看<深入Java虚拟机>.<Java虚拟机规范>.<Thinking ...
- Systemweaver — 电子电气协同设计研发平台
当前电子电气系统随着功能安全.AutoSAR.车联网.智能驾驶等新要求,导致其复杂性.关联性日益上升.当前,传统基于文档的设计由于其低复用性.无关联性.无协同性等缺点,已经无法适应日益 ...
随机推荐
- 初学Python之爬虫的简单入门
初学Python之爬虫的简单入门 一.什么是爬虫? 1.简单介绍爬虫 爬虫的全称为网络爬虫,简称爬虫,别名有网络机器人,网络蜘蛛等等. 网络爬虫是一种自动获取网页内容的程序,为搜索引擎提供了重要的 ...
- 更新GitHub项目出现There is no tracking information for the current branch. Please specify which branch you want to merge with. 怎么解决
git pull命令用于从另一个存储库或本地分支获取并集成(整合).git pull命令的作用是:取回远程主机某个分支的更新,再与本地的指定分支合并,它的完整格式稍稍有点复杂. 如果当前分支只有一个追 ...
- python文件操作【目录大全】
总是记不住API.昨晚写的时候用到了这些,但是没记住,于是就索性整理一下吧: python中对文件.文件夹(文件操作函数)的操作需要涉及到os模块和shutil模块. 得到当前工作目录,即当前Pyth ...
- 《细说PHP》第四版 样章 第18章 数据库抽象层PDO 8
18.6.4 执行准备好的查询 当准备好查询并绑定了相应的参数后,就可以通过调用PDOStatement类对象中的execute()方法,反复执行在数据库缓存区准备好的语句了.在下面的示例中,向前面 ...
- Jmeter之用于json格式的响应断言
当响应结果是json格式时,用JSON Assertion更方便判断. 1 在请求上右键添加json断言 2 编辑json Assertion 判断方式: 如果响应结果不是json格式的,fail ...
- app——升级测试点
APP版本升级的测试点 该文章转载于:https://www.cnblogs.com/changpuyi/p/8618755.html 移动端版本更新升级是一个比较重要的功能点,主要分为强制更新和 ...
- JNDI和JDBC的区别-个人理解
网上关于JNDI和JDBC的定义有很多,但是都很官方不容易理解,下面是我最近查阅资料得出的心得体会.希望对你在理解上有一点点的帮助,说的不对的请指正哦. JDBC: 看到最多的就是 Java Data ...
- du 配合sort查看文件夹大小
du -s * | sort -nr | head 选出排在前面的10个 du -s * | sort -nr | tail 选出排在后面的10个
- 【IDEA】(1)---MAC下常用快捷键
IDEA常用快捷键 IDEA是一个很好的开发工具,用好它能大大提高我们的开发效率,所以这里学习总结下有关IDEA实用的一些教程,比如常用快捷键,如何自定义代码模版,如何debug异常断点,或者说多线程 ...
- HTTP Error 500.0 - ANCM In-Process Handler Load Failure 排错历程
先上报错图 环境 Window Server 2008 r2 netcore 2.2 排错历程 看到这个错 我第一个想到netcore 安装问题 先检查了下环境 发现没问题 我排查了下应用池 确定是无 ...