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 应用实例

下面我们通过一个简单的例子阐述如何实现设计模式的各个部分。

功能包括:

  1. 建立一个简单的关于 Account 的 Visualforce 页面,其中包含一个输入框和按钮。
  2. 当用户输入文字并点击按钮后,系统会首先查找名字和用户输入相同的 Account 记录,如果找不到则建立这样一个 Account 记录。
  3. 在每个 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 进行模拟数据和测试的时候,一般遵循以下四个步骤:

  1. 建立模拟对象(Create mocks)
  2. 建立模拟数据(Given)
  3. 执行测试步骤(When)
  4. 检验测试结果(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 函数没有被执行
}
}

代码解释:

  1. 我们在 “Then” 部分进行验证的时候,使用了 fflib_ArgumentCaptor 类。这个类的作用是得到在模拟的工作单元中使用的参数。然后,我们可以使用 mocks.verify 函数来验证相应的函数 “registerNew” 是否在模拟工作单元 “uowMock” 中被调用了一次,并且参数是 “Account” 类型
  2. 在 shouldNotCreateAccount 函数中,为了模拟选择逻辑层的输出结果,我们使用了 Apex 提供的 stub API 功能。这样做的好处是我们不需要考虑选择逻辑层的正确与否(选择逻辑层有自己的测试函数来测试),只需要设置它的输出结果,然后用来测试当前的服务层函数即可。我们使用了 mocks.when(functionA(paramA)).thenReturn(B) 函数来设置当调用函数 functionA 并且其参数是 paramA 时,返回值是 B
  3. 注意这一行 “mocks.when(selectorMock.sObjectType()).thenReturn(Account.SObjectType);”,这是必须的设置,用来告诉模拟的选择逻辑层去和 Account 类型相关联。否则在接下来的执行中,模拟的选择逻辑层会给出 null 作为结果
  4. 在对 “Then” 部分进行验证时,不一定要使用 ApexMocks 提供的类或函数。我们也可以直接使用默认的 System.assert 系列函数来测试运行结果

小结

ApexMocks 框架提供了非常强大的模拟对象功能,我们在上文中只给出了很简单的示例。

将 ApexMocks 和 FFLIB 结合使用可以显著地提高单元测试的运行效率。

用 FFLIB 实现 Apex 企业设计模式的更多相关文章

  1. Apex 企业设计模式

    FFLIB 是一个免费的框架,对 Apex 进行了扩展.它的结构实现了 Salesforce 推荐的Apex 企业设计模式. 在学习如何使用 FFLIB 框架之前,我们先来了解一下 Apex 企业设计 ...

  2. Java设计模式学习资源汇总

    本文记录了Java设计模式学习书籍.教程资源.此分享会持续更新: 1. 设计模式书籍 在豆瓣上搜索了一把,发现设计模式贯穿了人类生活的方方面面.还是回到Java与程序设计来吧. 打算先归类,再浏览,从 ...

  3. ASP.NET 设计模式(转)

    Professional ASP.NET Design Patterns 为什么学习设计模式? 运用到ASP.NET应用程序中的设计模式.原则和最佳实践.设计模式和原则支持松散耦合.高内聚的代码,而这 ...

  4. 【设计模式系列】之OO面向对象设计七大原则

    1  概述 本章叙述面向向对象设计的七大原则,七大原则分为:单一职责原则.开闭原则.里氏替换原则.依赖倒置原则.接口隔离原则.合成/聚合复用原则.迪米特法则. 2  七大OO面向对象设计 2.1 单一 ...

  5. Head First设计模式分析学习

    永不放弃的毅力,和对欲望的控制. 注意:要能够理解相类似的设计模式之间的区别和不同.可以把类比列举出来,加深记忆. 是否加入Spring容器中的标准是是否要用到Spring框架的方法或者功能特性,如事 ...

  6. asp.net资料! (.NET) (ASP.NET)

    使用SqlBulkCopy类加载其他源数据到SQL表 在数据回发时,维护ASP.NET Tree控件的位置 vagerent的vs2005网站开发技巧 ASP.NET2.0小技巧--内部控件权限的实现 ...

  7. 身为java程序员你需要知道的网站(包含书籍,面试题,架构...)

    推荐几本书<高级java程序员值得拥有的10本书>,     首页 所有文章 资讯 Web 架构 基础技术 书籍 教程 我要投稿 更多频道 » - 导航条 - 首页 所有文章 资讯 Web ...

  8. Java编程之路相关书籍(三个维度)

    一.关于Java的技术学习.能够依照以下分三个维度进行学习 : (1)向下发展,也就是底层的方向 建议看<深入Java虚拟机>.<Java虚拟机规范>.<Thinking ...

  9. Systemweaver — 电子电气协同设计研发平台

            当前电子电气系统随着功能安全.AutoSAR.车联网.智能驾驶等新要求,导致其复杂性.关联性日益上升.当前,传统基于文档的设计由于其低复用性.无关联性.无协同性等缺点,已经无法适应日益 ...

随机推荐

  1. P4728 [HNOI2009]双递增序列

    题意 这个DP状态有点神. 首先考虑一个最暴力的状态:\(f_{i,j,k,u}\)表示第一个选了\(i\)个,第二个选了\(j\)个,第一个结尾为\(k\),第二个结尾为\(u\)是否可行. 现在考 ...

  2. Codeforces Round #594 (Div. 1) A. Ivan the Fool and the Probability Theory 动态规划

    A. Ivan the Fool and the Probability Theory Recently Ivan the Fool decided to become smarter and stu ...

  3. Codeforces Round #598 (Div. 3) C. Platforms Jumping 贪心或dp

    C. Platforms Jumping There is a river of width n. The left bank of the river is cell 0 and the right ...

  4. Nacos做配置中心经常被问到的问题

    加载多个配置文件怎么处理? 通过@NacosPropertySource可以注入一个配置文件,如果我们需要将配置分类存储或者某些配置需要共用,这种需求场景下,一个项目中需要加载多个配置文件,可以可以直 ...

  5. [开源] FreeSql 配套工具,基于 Razor 模板实现最高兼容的生成器

    FreeSql 经过半年的开发和坚持维护,在 0.6.x 版本中完成了几大重要事件: 1.按小包拆分,每个数据库实现为单独 dll: 2.实现 .net framework 4.5 支持: 3.同时支 ...

  6. PHP获取网址详情页的内容导出到WORD文件

    亲自测试效果一般, css的样式文件获取不到 如果没有特殊的样式  或者是内容里面包括样式的  直接输出有样式的内容 然后导出  这样还是可以的 class word { function start ...

  7. C#上手练习7(方法语句2)

    上一篇方法调用赋值封装,这里使用封装后调用,尽量满足开闭原则. 以及静态类的使用. using System; namespace KingTest03 { class Program { int a ...

  8. sqlserver 问题来了,视图不会自动更新,如果是用*创建的

    奇葩问题一个 create view时候用的select * 关联了几个表创建的. 后修改select *  的表,结果悲剧了. select * from 视图得到的结果绝对让你想哭.不报错,不提示 ...

  9. 滴滴出行开源项目doraemonkit食用指南

    版权声明:本文为xing_star原创文章,转载请注明出处! 本文同步自http://javaexception.com/archives/94 doraemonkit 功能介绍 一两周前在地铁上刷任 ...

  10. Xcode修改工程文件名字

    http://stackoverflow.com/questions/8262613/renaming-xcode-4-project-and-the-actual-folder