Salesforce 架构篇总结(三)Service 层的一些原则

主要目标

创建一个 Service 层的 class 并且在应用中高效的使用
暴露出一个用 Service 层做的 API

创建 Service

下面介绍的这个方法展示了使用 service 利用 discount 来建立一组 discount

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public with sharing class OpportunitiesService
{
public static void applyDiscounts(Set<Id> opportunityIds, Decimal discountPercentage)
{
// Validate parameters
if(opportunityIds == null || opportunityIds.size() == 0)
{
throw new OpportunityServiceException('Opportunities not specified.');
}

if(discountPercentage < 0 || discountPercentage > 100)
{
throw new OpportunityServiceException('Invalid discount to apply.');
}

// Query Opportunities and Lines (SOQL inlined for this example, see Selector pattern in later module)
List<Opportunity> opportunities =[select Amount, (select UnitPrice from OpportunityLineItems)
from Opportunity where Id in :opportunityIds];

// Update Opportunities and Lines (if present)
List<Opportunity> oppsToUpdate = new List<Opportunity>();
List<OpportunityLineItem> oppLinesToUpdate = new List<OpportunityLineItem>();

Decimal factor = 1 - (discountPercentage == null ? 0 : discountPercentage / 100);

for(Opportunity opportunity : opportunities)
{
// Apply to Opportunity Amount
if(opportunity.OpportunityLineItems != null && opportunity.OpportunityLineItems.size() > 0)
{
for(OpportunityLineItem oppLineItem : opportunity.OpportunityLineItems)
{
oppLineItem.UnitPrice = oppLineItem.UnitPrice * factor;
oppLinesToUpdate.add(oppLineItem);
}
}
else
{
opportunity.Amount = opportunity.Amount * factor;
oppsToUpdate.add(opportunity);
}
}

// Update the database
SavePoint sp = Database.setSavePoint();

try
{
update oppLinesToUpdate;
update oppsToUpdate;
}
catch (Exception e)
{
// Rollback
Database.rollback(sp);
// Throw exception on to caller
throw e;
}
}

public class OpportunityServiceException extends Exception {}
}

Salesforce 架构篇总结(二)理解 Service 层业务遵循的规则

主要目标

理解 Martin Fowler 为了做企业级架构而分离出 Service 层的缘由
理解为什么 Apex 代码要属于 Service 层
在应用和平台的开发中如何很好的契合 Service 层的代码
在 salesforce 平台上用代码去实现 Service 的架构

简单概述

上一篇 Blog 只是简单的介绍了 SOC 思想,将软件的结构抽象到层级应用的逻辑方面,本篇 Blog 主要将目标集中在如何去定义和实现 Service 层,它也是别的层级或者 API 调用的关键步骤。

层级架构之间的关系图:

ServiceLayerSketch up-w250

Service 层对实现的业务层、运算层和一些执行业务的逻辑 Process 有一个清晰和严格的封装,同时 service 层必须保证足够的功能专一和抽象,它能够适应后期功能的多次迭代和灵活的可扩展性。下面的内容是如何使用 Apex 代码来实现 Service 层的业务逻辑,同时说明如何在 Force.com 资源有限的情况的下合理的利用资源去实现它。

使用 Service 层的对象

客户端(client)执行 Service 层的业务逻辑,比如像 UI controller 或者 Batch Apex 等。

值得考虑使用 Service 层的一些方向:

Service 层的

1
2
3
4
5
6
注意:

上图中没有提到 Apex Trigger,因为 Trigger 的逻辑属于 Domain layer,它和 Object 联系非常
紧密,在 Domain 层的业务逻辑通过 platform UI 或者 APIs 或多或少的会和 Service 层联系到一


对 Salesforce 平台的创新和适应性的思考

想象一下有这么一个场景,当写了一段 feature 代码,需求只要一变就得重新对代码进行重构,或者更糟糕的是由于害怕之前写的逻辑被破坏,索性不敢动之前的逻辑,然后出现了大量的重复的代码,真是很糟糕的体验。

设计上的一些考究

  • 命名规范:Service 层应该足够抽象而且其意义,同时应该很广泛来涵盖客户端的很多情况,这些方面可以表现在对类、方法、参数名的命名规范上,什么时候用动词(verbs),什么时候用名词(nouns)都要有所思考;确保名字所表达的是种一般情况而非特殊情况,举个例子,这个方法名是以业务操作来命名的 InvoiceService.calculateTax(…) ,而第二个方法名是以特定的业务操作来命名的 InvoiceService.handleTaxCodeForACME(…) 应该避像第二种这样的的命名方式

  • 平台 / 协调一致:设计一个签名的算法和 Salesforce 平台进行交互是一个很好的实践,尤其要使用 Bulkificaiton 的时候,一个主要需要考虑的因素是,在所有运行在 Force.com 上的代码都是 Bulkificaiton,要考虑调用服务器端的代码参数数组化,而不是一个单一的参数集,举个例子,这个方法就可以使用 Bulkificaiton => InvoiceService.calculateTax(List taxCalulations),而这个方法 => InvoiceService.calculateTax(Invoice invoice, TaxInfo taxCodeInfo),由于参数是单一参数集,也应该尽量去避免

  • SOC 方面的考究:在应用当中 Service 层的代码利用很多 Objects 封装一些任务或者 Process 业务逻辑,与之相对应的是,有些代码关联着特定的 validationfield values 或者 calculations,通过插入、更新、删除触发Trigger 来影响其相对应 Object, 这些代码一般存放在 Trigger 当中,并且可以保留在那里

  • 安全性:Service 层的代码和那些被调用的代码应该确保用户安全,要确保这一点使用 with sharing(with Security Settings enforced)修饰符,尤其需要注意的是如果使用 global 修饰符暴露了太多关键代码,就需要引起注意。如果一个 Apex 类的逻辑必须通过一些 Recodes 让外部的用户可见,那么代码必须提炼它的执行环境,越简略越好,一个好的办法是使用私有的 Apex 内部类,使用 without sharing
    关键字

  • 编组:简言之,避免指定如何处理与 Service 层交互的方面错误信息,因为某些方面最好留给服务的调用者,只管原样抛出异常就好(it is typically best to leverage the default error-handling semantics of Apex by throwing exceptions. )

  • 整合服务:虽然客户端可以一个接一个的执行多次 callout,但这么做会很低效,也会造成一些数据库事务方面的问题,最好的办法是建一个整合服务( compound services)(compound services),让一次 callout 涵盖多个客户端的请求。同样很重要的是尽可能优化 Service 层的 SOQL 和 DML 操作。当然了这并不意味着不能 callout 更细节的逻辑单元;如果需要的话,可以开发特定的单元去以供客户端 callout

  • 事务管理和无状态: Service 层的客户端经常有一些拥有长存活时间且不同的 Process 请求和一些消息用来执行和处理,举个例子,一个单一的请求和多个请求分隔成单独的作用范围到服务器端:管理状态(比如 Batch Apex)或者复杂的 UI 都是通过它们自己的页面状态来接受很多请求的,在状态管理上最好的方式是在做一次 callout 到 Service 层时,封装数据库操作和服务状态。换句话说,使 Service 端保持无状态,以使调用的环境灵活地使用它们自己的状态管理解决方案。例如,一个在数据库事务的作用域同样应该被包括在每一个 Service 层的方法当中,以便于调用者不用去考虑和其相关的 SavePoints

  • 配置:在 Service 层中,可能有常见的配置或行为被覆盖,合理使用方法重载,接受一个共享的选项参数,类似于 Apex 中的 DML 方法

Service 层在 Apex 中的应用

情景介绍:假设在 Opportunity 页面有个自定义 Button 当点击 Button 会出现一个 Visualforce 页面,提示用户对 Opportunity Amount(或相关联的 Opportunity Line)项目应用一个折扣百分比。
实例展示如何将 OpportunitiesService.applyDiscounts 方法应用到比如 Visualforce、Batch Apex、 或者 JavaScript Remoting 这些地方上。

下面的 Code 处理的是通过 StandardController 选择的单个 Opportunity,注意:controller 的错误信息是 controller 自己来处理的,而非 service,因为 visualforce 有其自身的错误表现形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public PageReference applyDiscount()
{
try
{
// Apply discount entered to the current
Opportunity OpportunitiesService.applyDiscounts(new Set<ID>{ standardController.getId() }, DiscountPercentage);
}
catch (Exception e)
{
ApexPages.addMessages(e);
}

return ApexPages.hasMessages() ? null : standardController.view();
}

下面的 Code 展示了通过 StandardSetController 处理多个 Opportunities

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public PageReference applyDiscounts()
{
try
{
// Apply discount entered to the selected Opportunities
OpportunitiesService.applyDiscounts(
// Tip: Creating a Map from an SObject list gives easy access to the Ids (keys)
new Map<Id,SObject>(standardSetController.getSelected()).keyValues(),
DiscountPercentage
);
}
catch (Exception e)
{
ApexPages.addMessages(e);
}

return ApexPages.hasMessages() ? null : standardController.view();
}

下面的 Code 展示了如何使用 Batch Apex 处理大量的数据,注意和之前代码的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public with sharing class OpportunityApplyDiscountJob implements Database.Batchable<SObject>
{
public Decimal DiscountPercentage { get; private set; }

public OpportunityApplyDiscountJob(Decimal discountPercentage)
{
// Discount to apply in this job
this.DiscountPercentage = discountPercentage;
}

public Database.QueryLocator start(Database.BatchableContext ctx)
{
// Opportunities to discount
return Database.getQueryLocator('select Id from Opportunity where StageName = \'Negotiation/Review\'');
}

public void execute(Database.BatchableContext BC, List<sObject> scope)
{
try
{
// Call the service
OpportunitiesService.applyDiscounts(
new Map<Id,SObject>(scope).keySet(),DiscountPercentage);
}
catch (Exception e)
{
// Email error, log error, chatter error etc..
}
}

public void finish(Database.BatchableContext ctx) { }
}

下面的 Code 将 service 层打包起来,并且通过 JavaScript Remoting 暴露给客户端,供其调用

1
2
3
4
5
6
7
8
9
10
public class OpportunityController
{
@RemoteAction
public static void applyDiscount(Id opportunityId, Decimal discountPercent)
{
// Call service
OpportunitiesService.applyDiscounts(new Set<ID> { opportunityId }, discountPercent);
}
}

总结

在 Service 层提不断投入对更大的重用性和适应性的都带来很大的好处,同时也为应用程序实现 API 提供了一种更干净、更经济的方式之一。
原文:
Investing in a service layer for your application offers the engineering benefits of greater reuse and adaptability, as well as provides a cleaner and more cost effective way of implementing an API for your application, a must in today’s cloud-integrated world. By closely observing the encapsulation and design considerations described above, you start to form a durable core for your application that will endure and remain a solid investment throughout the ever-changing and innovative times ahead!

相关资料

Separation of concerns
Martin Fowler’s Service Layer Pattern
Martin Fowler’s Enterprise Architecture Patterns
Learn Service Layer Principles

知识扩展

With Sharing、Without Sharing 和 non-sharing-specified 修饰符的区别

在这里学到 Salesforce 相关的知识的

The fun way to learn Salesforce

Salesforce 架构篇总结(一)理解 SOC

主要目标

解释 SOC 用于商业的价值
在使用一些需求或者平台技术中使用 SOC 来适用一些解决方案
将 SOC 应用到 Force.com 中
什么时候决定使用 SOC 技术

1
2
3
4
5
作为一个 salesforce developer 不能仅仅停留在为客户解决一些业务逻辑方
面的问题,并且作为一个公司产品的后端程序猿,更为重要的是当产品迭代到一定
程度,随着难度的加大以及业务复杂度的加深,我们需要在开发中融入一些重要的
编程核心的思想,方便及时发现产品中的 bug 以及对产品后期可扩展性方面都有
不错的提升。

对于SOC(Separation of Concerns) 的理解

广义上来说SOC是一种分层思想的体现,在大多数OOP语言中都有涉及,这里不再赘述,但需要强调的是,代码的功能模块是可以不断复用的,我们不提倡 copy & paste,但要时刻想着复用,并且类的命名规范(class naming conventions),变量名的命名规范(variable naming conventions)都有助于代码可读性的提升,好的代码如同讲一段优美的故事,That is Good code should tell a story.

SOC 的一些好处

顺应技术革命的潮流(Evolution)
技术不断在进步的进步的同时保证代码层级之间是可以扩展,修改,甚至要做到可以使层级之间拆卸自如,回想前端框架数十年发展的太快了,心疼前端小伙伴,各种全家桶层出不穷,学也学不完,都是好学霸
对影响的管控(Impact management)
当修改或者拆卸掉一些层级组件的时候,应尽量避免影响到其他层级的功能,除非是出于设计的考究而去有意去修改的
角色及其职能(Roles and responsibility)
每一个层级都用其职能,不要低于或者高于其职能,比如丢弃掉一个客户端的应用或者类库,不是意味着丢去掉其业务层的逻辑,因为业务层是另一个层级的职责。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
salesforce  层级分类如下:

* 业务逻辑层(Business Logic Layer)
非编码(Declarative): Formula, Validation, Workflow, Process Builder, Sharing Rules
遍码(Coding): Apex Services, Apex Custom Actions

* 数据处理层 (Data Access layer)
非编码(Declarative): Data Loaders
编码(Coding): SOQL, SOSL, Salesforce APIs

* 数据库层 (Database Layer)
非编码(Declarative): Custom Objects, Fields, Relationships, Rollups
编码(Coding): Apex Triggers

在 Force.com 中使用 SOC

  • 在 App 中替代或者添加另一种 UI(Replacing or adding another UI to your app),需要考虑的是有多少代码需要去重写,或者有些 UI 的端口虽然什么都没做,但是影响到了 App 的 插入(inserting),更新(updating),验证(validating)或者计算处理(calculating)的一些功能。

  • 提供公用 API(Providing a public-facing API)评估一下所实现的 API 使用已有代码库的哪一部分,不要将一些动作行为的方法作为 API 基础的调用

  • 通过 Batch 类来提升应用层逻辑(Scaling your application logic via Batch Apex)使用 Batch 来增大数据吞吐量,使多个用户在登录相同的 UI 页面的时候有相一致的结果

  • 在 Visualforce 页面或者 Lightning Component controllers 中执行复杂的业务逻辑(Working with complex action methods in your Visualforce or Lightning Component controllers)言简意赅,使用 MVC 架构,很老生常谈的东西,在随后的章节会陆续介绍

  • 让一个新的开发人员能快速上手项目的架构(Making it easy for new developers to find their way around your code base)一个新的开发人员花在熟悉代码组织架构的时间也是衡量一个项目好坏的标准之一