The problem with Billy's code is that it will create a new Order record every time the Opportunity is updated, even if it was already closed previously.
A better approach would be to use the Trigger Pattern to check for changes to the Opportunity and only create the Order if the Opportunity changes to the closed state. This can be done using the Trigger Pattern with the State design pattern.
Some key points:
- Use Trigger Pattern to separate trigger logic from main logic
- Check for changes to Opportunity using Trigger.oldMap and Trigger.newMap
- Only process if Opportunity changes to closed state
- Use State design pattern to represent different Opportunity states
- Create Order only if state changes to "Closed"
This
2. Safe Harbor
Safe harbor statement under the Private Securities Litigation Reform Act of 1995:
This presentation may contain forward-looking statements that involve risks, uncertainties, and assumptions. If any such uncertainties
materialize or if any of the assumptions proves incorrect, the results of salesforce.com, inc. could differ materially from the results
expressed or implied by the forward-looking statements we make. All statements other than statements of historical fact could be
deemed forward-looking, including any projections of product or service availability, subscriber growth, earnings, revenues, or other
financial items and any statements regarding strategies or plans of management for future operations, statements of belief, any
statements concerning new, planned, or upgraded services or technology developments and customer contracts or use of our services.
The risks and uncertainties referred to above include – but are not limited to – risks associated with developing and delivering new
functionality for our service, new products and services, our new business model, our past operating losses, possible fluctuations in our
operating results and rate of growth, interruptions or delays in our Web hosting, breach of our security measures, the outcome of any
litigation, risks associated with completed and any possible mergers and acquisitions, the immature market in which we operate, our
relatively limited operating history, our ability to expand, retain, and motivate our employees and manage our growth, new releases of our
service and successful customer deployment, our limited history reselling non-salesforce.com products, and utilization and selling to
larger enterprise customers. Further information on potential factors that could affect the financial results of salesforce.com, inc. is
included in our annual report on Form 10-K for the most recent fiscal year and in our quarterly report on Form 10-Q for the most recent
fiscal quarter. These documents and others containing important disclosures are available on the SEC Filings section of the Investor
Information section of our Web site.
Any unreleased services or features referenced in this or other presentations, press releases or public statements are not currently
available and may not be delivered on time or at all. Customers who purchase our services should make the purchase decisions
based upon features that are currently available. Salesforce.com, inc. assumes no obligation and does not intend to update these
forward-looking statements.
3. Richard Vanhook
Senior Technical Solution Architect
@richardvanhook
Dennis Thong
Director - Technical Solution Architect
@denniscfthong
4. The Plan
Cover four design patterns
▪ Problem
▪ Pattern Abstract
▪ Code
Your participation is encouraged!
6. Meet Billy, Your Admin
• 3+ yrs as Salesforce Admin
• Just recently started coding
• Sad because of this:
Trigger.AccountTrigger: line 3, column 1
System.LimitException: Too many record
type describes: 101
• Needs your help!
7. The Offending Code
01
02
03
04
05
06
trigger AccountTrigger on Account (before insert, before update) {
for(Account record : Trigger.new){
AccountFooRecordType rt = new AccountFooRecordType();
....
}
}
07
08
09
10
11
12
13
public class AccountFooRecordType {
public String id {get;private set;}
public AccountFooRecordType(){
id = Account.sObjectType.getDescribe()
.getRecordTypeInfosByName().get('Foo').getRecordTypeId();
}
}
• When a single Account is inserted, what happens?
• When 200+ Accounts are inserted…?
9. Let’s Code It!
01
02
03
04
05
06
trigger AccountTrigger on Account (before insert, before update) {
for(Account record : Trigger.new){
AccountFooRecordType rt = new AccountFooRecordType();
....
}
}
07
08
09
10
11
12
13
public class AccountFooRecordType {
public String id {get;private set;}
public AccountFooRecordType(){
id = Account.sObjectType.getDescribe()
.getRecordTypeInfosByName().get('Foo').getRecordTypeId();
}
}
• Above is the offending code again
• Changes are highlighted in next slides
10. Let’s Code It!
01
02
03
04
05
06
trigger AccountTrigger on Account (before insert, before update) {
for(Account record : Trigger.new){
AccountFooRecordType rt = (new AccountFooRecordType()).getInstance();
....
}
}
07
08
09
10
11
12
13
14
15
16
public class AccountFooRecordType {
public String id {get;private set;}
public AccountFooRecordType(){
id = Account.sObjectType.getDescribe()
.getRecordTypeInfosByName().get('Foo').getRecordTypeId();
}
public AccountFooRecordType getInstance(){
return new AccountFooRecordType();
}
}
11. Let’s Code It!
01
02
03
04
05
06
trigger AccountTrigger on Account (before insert, before update) {
for(Account record : Trigger.new){
AccountFooRecordType rt = AccountFooRecordType.getInstance();
....
}
}
07
08
09
10
11
12
13
14
15
16
public class AccountFooRecordType {
public String id {get;private set;}
public AccountFooRecordType(){
id = Account.sObjectType.getDescribe()
.getRecordTypeInfosByName().get('Foo').getRecordTypeId();
}
public static AccountFooRecordType getInstance(){
return new AccountFooRecordType();
}
}
12. Static
▪ Can modify member variables, methods, and blocks
▪ Executed when?
• When class is introduced (loaded) to runtime environment
▪ How long does that last in Java?
• Life of the JVM
▪ In Apex?
• Life of the transaction
13. Let’s Code It!
01
02
03
04
05
06
trigger AccountTrigger on Account (before insert, before update) {
for(Account record : Trigger.new){
AccountFooRecordType rt = AccountFooRecordType.getInstance();
....
}
}
07
08
09
10
11
12
13
14
15
16
public class AccountFooRecordType {
public String id {get;private set;}
public AccountFooRecordType(){
id = Account.sObjectType.getDescribe()
.getRecordTypeInfosByName().get('Foo').getRecordTypeId();
}
public static AccountFooRecordType getInstance(){
return new AccountFooRecordType();
}
}
14. Let’s Code It!
01
02
03
04
05
06
trigger AccountTrigger on Account (before insert, before update) {
for(Account record : Trigger.new){
AccountFooRecordType rt = AccountFooRecordType.getInstance();
....
}
}
07
08
09
10
11
12
13
14
15
16
17
18
public class AccountFooRecordType {
public static AccountFooRecordType instance = null;
public String id {get;private set;}
public AccountFooRecordType(){
id = Account.sObjectType.getDescribe()
.getRecordTypeInfosByName().get('Foo').getRecordTypeId();
}
public static AccountFooRecordType getInstance(){
if(instance == null) instance = new AccountFooRecordType();
return instance;
}
}
15. Let’s Code It!
01
02
03
04
05
06
trigger AccountTrigger on Account (before insert, before update) {
for(Account record : Trigger.new){
AccountFooRecordType rt = AccountFooRecordType.getInstance();
....
}
}
07
08
09
10
11
12
13
14
15
16
17
18
public class AccountFooRecordType {
private static AccountFooRecordType instance = null;
public String id {get;private set;}
public AccountFooRecordType(){
id = Account.sObjectType.getDescribe()
.getRecordTypeInfosByName().get('Foo').getRecordTypeId();
}
public static AccountFooRecordType getInstance(){
if(instance == null) instance = new AccountFooRecordType();
return instance;
}
}
16. Let’s Code It!
01
02
03
04
05
06
trigger AccountTrigger on Account (before insert, before update) {
for(Account record : Trigger.new){
AccountFooRecordType rt = AccountFooRecordType.getInstance();
....
}
}
07
08
09
10
11
12
13
14
15
16
17
18
public class AccountFooRecordType {
private static AccountFooRecordType instance = null;
public String id {get;private set;}
private AccountFooRecordType(){
id = Account.sObjectType.getDescribe()
.getRecordTypeInfosByName().get('Foo').getRecordTypeId();
}
public static AccountFooRecordType getInstance(){
if(instance == null) instance = new AccountFooRecordType();
return instance;
}
}
19. Your Suggestion to Billy
01
02
03
04
05
06
public class GoogleMapsGeocoder{
public static List<Double> getLatLong(String address){
//web service callout of some sort
return new List<Double>{0,0};
}
}
07
08
System.debug(GoogleMapsGeocoder.getLatLong('moscone center'));
//=> 13:56:36.029 (29225000)|USER_DEBUG|[29]|DEBUG|(0.0, 0.0)
But then Billy mentions these requirements
▪ Need other options besides Google Maps
▪ Allow client code to choose provider
20. Solution: Strategy
Context
Strategy
strategies
Client
+operation()
1
*
+operation()
defines a family of algorithms,
ConcreteStrategyA
encapsulates each one, and makes
them interchangeable
+operation()
▪ Context => Geocoder
▪ operation() => getLatLong()
▪ Strategy => GeocodeService
▪ ConcreteStrategyA => GoogleMapsImpl
▪ ConcreteStrategyB => MapQuestImpl
ConcreteStrategyB
+operation()
21. Let’s Code It
01
02
03
public interface GeocodeService{
List<Double> getLatLong(String address);
}
04
05
06
07
08
public class GoogleMapsImpl implements GeocodeService{
public List<Double> getLatLong(String address){
return new List<Double>{0,0};
}
}
09
10
11
12
13
public class MapQuestImpl implements GeocodeService{
public List<Double> getLatLong(String address){
return new List<Double>{1,1};
}
}
26. Question
How do we extend the functionality of an sObject in Apex without adding new fields?
Transient selection
checkboxes
Calculated Fields
with Updates
28. Example - Scenario
Weather sObject
▪ City__c
▪ TempInFahrenheit__c (in Fahrenheit)
VisualForce Requirements
▪ Stored temperature in Fahrenheit is displayed in Celcius
▪ User enters Temperature in Celcius and is stored to the record in Fahrenheit
Bi-directional Display and
Calculation
29. The Code – Decorated sObject Class
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class DecoratedWeather {
public Weather__c weather { get; private set; }
public DecoratedWeather ( Weather__c weather) {
this.weather = weather;
}
public Decimal tempInCelcius {
get {
if (weather != null && tempInCelcius == null )
tempInCelcius = new Temperature().FtoC(weather.TempInFahrenheit__c);
return tempInCelcius;
}
set {
if (weather != null && value != null )
weather.TempInFahrenheit__c= new Temperature().CtoF(value);
tempInCelcius = value;
}
}
}
30. The Code – Custom Controller
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public class Weather_Controller {
public List<DecoratedWeather> listOfWeather {
get {
if (listOfWeather == null) {
listOfWeather = new List<DecoratedWeather>();
for (Weather__c weather : [ select name, temperature__c from Weather__c]) {
listOfWeather. add(new DecoratedWeather(weather));
}
}
return listOfWeather;
}
private set;
}
}
31. The Code – VisualForce Page
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<apex:page controller="weather_controller">
<!-- VF page to render the weather records with Ajax that only rerenders
the page on change of the temperature
-->
<apex:form id="theForm">
<apex:pageBlock id="pageBlock">
<apex:pageBlockTable value="{!listOfWeather}" var="weather">
<apex:column value="{!weather.weather.name}"/>
<apex:column headerValue="Temperature (C)">
<apex:actionRegion >
<apex:inputText value="{!weather.tempInCelcius}">
<apex:actionSupport event="onchange"
reRender="pageBlock"/>
</apex:inputText>
</apex:actionRegion>
</apex:column>
<apex:column headerValue="Temperature (F)"
value="{!weather.weather.Temperature__c}"
id="tempInF"/>
</apex:pageBlockTable>
</apex:pageBlock>
</apex:form>
</apex:page>
No client side
logic!!!
34. Billy Has a New Problem!
Billy wrote a trigger to create an order on close of an opportunity,
however:
▪ It always creates a new order every time the closed opportunity is
updated
▪ When loading via Data Loader, not all closed opportunities result in an
order
35. The Offending Code
01
02
03
04
05
06
07
08
trigger OpptyTrigger on Opportunity (after insert, after update) {
if (trigger.new[0].isClosed) {
Order__c order = new Order__c();
…
insert order
}
}
Poor reusability
Occurs regardless
of prior state
No Bulk
Handling
36. Any Better?
01
02
03
04
05
trigger OpptyTrigger on Opportunity (after insert, after update) {
01
02
03
04
05
06
07
08
09
10
11
12
13
public class OrderClass {
new OrderClass().CreateOrder(trigger.new);
}
public void CreateOrder(List<Opportunity> opptyList) {
for (Opportunity oppty : opptyList) {
if (oppty.isClosed) {
Order__c order = new Order__c();
Occurs regardless
...
of prior state
insert order;
}
}
}
}
Not bulkified
37. How’s this?
01
02
03
04
0506
trigger OpptyTrigger on Opportunity (before insert, before update) {
if (trigger.isInsert) {
new OrderClass().CreateOrder(trigger.newMap, null);
else
new OrderClass().CreateOrder(trigger.newMap, trigger.oldMap);
01
public class OrderClass {
02
03
public void CreateOrder(Map<Opportunity> opptyMapNew
04
Map<Opportunity> opptyMapOld) {
05
List<Order__c> orderList = new List<Order__c>();
06
for (Opportunity oppty : opptyMapNew.values()) {
07
if (oppty.isClosed && (opptyMapOld == null ||
08
!opptyMapOld.get(oppty.id).isClosed)) {
09 least it’s
Order__c order = new Order__c();
At
10
...
bulkified
11
orderList.add(order);
12
}
13
}
14
insert orderList ;
15
}
This code is highly coupled
and not reusable
38. Solution – Bulk Transition
•
•
•
Checks for eligible records
that have changed state and
match criteria
Calls utility class method to
perform the work
Only eligible records are
passed in
•
Generic utility class method that can
be called from any context
39. At Last!!!
01
02
03
04
05
06
07
08
09
10
11
trigger OpptyTrigger on Opportunity (after insert, after update) {
if (trigger.isAfter && (trigger.isInsert || trigger.isUpdate)) {
List<Opportunity> closedOpptyList = new List<Opportunity>();
for (Opportunity oppty : trigger.new) {
if (oppty.isClosed && (trigger.isInsert ||
(trigger.isUpdate &&
!trigger.oldMap.get(oppty.id).isClosed)))
closedOpptyList.add(oppty);
}
if (!closedOpptyList.isEmpty())
new OrderClass().CreateOrderFromOpptys(closedOpptyList)
01
02
03
04
05
06
07
08
09
public class OrderClass {
public void CreateOrdersFromOpptys(List<Opportunity> opptyList) {
List<Order__c> orderList = new List<Order__c>();
for (Opportunity oppty : opptyList) {
Order__c order = new Order__c();
This method is now a lot
...
more reusable and is bulkorderList.add(order);
safe
}
insert orderList ;
Trigger handles state
transition
41. Resources
Wrapper Classes - http://wiki.developerforce.com/page/Wrapper_Class
Apex Code Best Practices - http://wiki.developerforce.
com/page/Apex_Code_Best_Practices
Apex Web Services and Callouts - http://wiki.developerforce.
com/page/Apex_Web_Services_and_Callouts
Sample Code - https://github.com/richardvanhook/Force.com-Patterns
42. More at #DF13
Apex Enterprise Patterns: Building Strong Foundations
Tuesday, November 19th: 12:15 pm - 01:00 pm - Moscone Center West – 2009
High Reliability DML and Concurrency Design Patterns for Apex
Monday, November 18th: 11:15 am - 12:00 pm - Moscone Center West – 2009
Making Your Apex and Visualforce Reusable
Tuesday, November 19th: 12:15 pm - 01:00 pm - Moscone Center West – 2022
Design Patterns for Asynchronous Apex
Tuesday, November 19th: 05:15 pm - 06:00 pm - Moscone Center West – 2024
The Apex Ten Commandments
Wednesday, November 20th: 04:00 pm - 04:45 pm - Moscone Center West - 2006 / 2008
44. We want to hear
from YOU!
Please take a moment to complete our
session survey
Surveys can be found in the “My Agenda”
portion of the Dreamforce app