Design patterns are an invaluable tool for developers and architects looking to build enterprise solutions. In this session we will present some tried and tested enterprise application engineering patterns that have been used in other platforms and languages. We will discuss and illustrate how patterns such as Data Mapper, Service Layer, Unit of Work and of course Model View Controller can be applied to Force.com. Applying these patterns can help manage governed resources (such as DML) better, encourage better separation-of-concerns in your logic and enforce Force.com coding best practices.
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 intellectual property and other
litigation, risks associated with 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-
Q for the most recent fiscal quarter ended July 31, 2012. This 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. All about FinancialForce.com
Leading Native ISV on Force.com
• #1 Accounting App on Force.com
• #1 Professional Services Automation App on Force.com
Backed by Salesforce.com & UNIT4
• UNIT4 - $600 million, 33 years building business apps
Growing Rapidly
• San Francisco HQ – 595 Market St.
• 145 Employees
• Customers in 23 countries
5. What are Enterprise Design Patterns?
Recipes for Engineers
Key Patterns
Separation of Concerns (SOC)
Data Mapper
Domain Model
Services
Unit of Work
Reference Martin Fowler
http://martinfowler.com/eaaCatalog/
Author of “Patterns of Enterprise
Application Architecture”
NOTE: Some additional care is required
to select and apply to Apex and Force.com
6. Agenda and sample code overview
“Opportunity Extensions” Package
Required Features
• Ability to Discount an Opportunity easily
• Ability to create Invoices from an Opportunity
• Quick Opportunity Wizard from Account and
MRU Products
Agenda : Code walk throughs…
Review code sets to compare,
discuss and contrast
Sample A is implemented without
considering patterns…
7. Sample A: Apply Discount @ v1
01: // Calculate discount factor
02: Decimal factor = Util.calculateFactor(discountPercentage);
/** 03: if(opportunity.OpportunityLineItems.size()==0)
* Version: v1 04: {
* Visualforce Controller accepts a discount value Opportunity if no lines
05: // Adjust the Amount on the
06: opportunity.Amount = opportunity.Amount * factor;
* from the user and applies it to the Opportunity
*/ 07: update opportunity;
08: }
09: else
01: public PageReference applyDiscount()
10: {
02: {
11: // Adjust UnitPrice of each line
03: try
12: for(OpportunityLineItem line : opportunity.OpportunityLineItems)
04: {
13: line.UnitPrice = line.UnitPrice * factor;
05: // Query Opportunity and line items to apply discount to
14: update opportunity.OpportunityLineItems;
06: Opportunity opportunity =
15: }
07: [select Id, Name, (select UnitPrice from OpportunityLineItems)
08: from Opportunity o
09: where Id = :standardController.getId()];
10: // Apply discount to line items
11: Util.applyDiscount(opportunity, DiscountPercentage);
12: }
13: catch (Exception e)
14: {
15: ApexPages.addMessages(e);
16: }
17: return null;
18: }
8. Sample A: Create Invoice @ v1
/**
* Version: v1
* Developer has reused Util.applyDiscount to combine as a convenience for the user the two
* tasks into one. But what would be the state of the Opportunity if Util.createInvoice threw
* an exception?
*/
01: public PageReference createInvoice()
02: {
03: try {
04: // Query Opportunity and line items to apply discount to
05: Opportunity opportunity =
06: [select Id, Name, AccountId, (select UnitPrice, Description, Quantity from OpportunityLineItems)
07: from Opportunity o
08: where Id = :standardController.getId()];
09: // Apply discount to line items
10: Util.applyDiscount(opportunity, DiscountPercentage); Transaction issue!
11: // Create Invoice from line items
Opportunity line changes are
12: Id invoiceId = Util.createInvoice(opportunity);
13: // Redirect to Invoice not rolled back on Invoice
14: return new PageReference('/'+invoiceId); creation failure. Automatic
15: } rollback only occurs for
16: catch (Exception e) { unhandled exceptions.
17: ApexPages.addMessages(e);
18: }
9. Sample A: Apply Discount @ v2
/**
* Version: v2
* Developer has implemented an enhancement to apply discounts conditionally per line based on the
* two new fields Opportunity.DiscountType__c and Product.DiscoutingApproved__c
*/ 01: // ENH:1024. Adjust UnitPrice of each line according to Discount Type of Opportunity
02: for(OpportunityLineItem line : opportunity.OpportunityLineItems)
01:public PageReference applyDiscount()
03: {
02:{ 04: // ENH:1024. Skip products that have not been approved for discounting01:
03: try 05: if(opportunity.DiscountType__c == 'Approved Products')
04: { 06: if(line.PricebookEntry.Product2.DiscountingApproved__c == false)
05: // 07:
Query Opportunity and line items to apply discount to
continue;
06: // 08:
ENH:1024. Added new columns to support new features in applyDiscount
// Adjust UnitPrice
07: Opportunity line.UnitPrice = line.UnitPrice * factor;
09: opportunity =
08: [select Id, Name, DiscountType__c,
10: }
09: (select UnitPrice, PricebookEntry.Product2.DiscountingApproved__c from OpportunityLineItems)
10: from Opportunity o
11: where Id = :standardController.getId()];
12: // Apply discount to line items
13: Util.applyDiscount(opportunity, DiscountPercentage);
10. Sample A: Create Invoice @ v2
/**
* Version: v2
* NOTE: This code has not been changed since v1. What error occurs when Util.applyDiscount is
* executed given the changes in v2 on the slide before?
*/
01: public PageReference createInvoice()
02: {
03: try
04: {
05: // Query Opportunity and line items to apply discount to
06: Opportunity opportunity =
07: [select Id, Name, AccountId,
08: (select UnitPrice, Description, Quantity from OpportunityLineItems)
09: from Opportunity o
10: where Id = :standardController.getId()];
11: // Apply discount to line items
12: Util.applyDiscount(opportunity, DiscountPercentage);
13: // Create Invoice from line items
14: Id invoiceId = Util.createInvoice(opportunity);
15: // Redirect to Invoice
16: return n
17: }
11. Sample A: Opportunity Wizard
/**
* A Opportunity Wizard that displays the most recently used Products and allows the user to
* select them to create an new Opportunity. Is there any logic here that does not belong?
*/
01: public QuickOpportunityWizardController(ApexPages.StandardController controller)
02: {
03: standardController = controller;
04:
05: // Create a new Opportunity defaulting from the Account
06: Account account = (Account) standardController.getRecord();
07: viewState = new ViewState();
08: viewState.Opportunity = new Opportunity();
09: viewState.Opportunity.Name = account.Name;
10: viewState.Opportunity.AccountId = account.Id;
11: viewState.Opportunity.DiscountType__c = OpportunitySettings__c.getInstance().DiscountType__c;
12: viewState.SelectLineItemList = new List<SelectLineItem>();
13:
14: // Recently used Opportunity lines In order to ensure consistent
15: List<OpportunityLineItem> lines = behavior, across this
wizard, Salesforce UI and
API’s, this logic needs to be
placed in a trigger
12. Sample A: Create Invoice @ v2
Issues Discussed
• Partial database updates in error conditions
• Data dependent runtime errors
• Inconsistent behavior between UI’s, API’s and Tools
Other Concerns
• Code not factored for exposure via an Application API
• Business functionality implemented in controller classes
• Util methods are not bulkified, causing callers to inhert bulkification issues
• CRUD security not enforced
14. Sample B: Controller Methods
/**
* Controller methods interact and call only the
* Service class methods. There is no transaction
* management needed in the controller methods. However
* error handling and reporting is managed by controller.
*/
01: public PageReference applyDiscount()
01: public PageReference createInvoice()
PageReference createOpportunity()
02: {{
02:
03:
03: try
try
try
04:
04: {{
{
05:
05: // Apply discount entered to line items selected by the user
// Create Invoice from fromitems of the current Opportunity
// Create Opportunity line the current Opportunity
06:
06: OpportunitiesService.applyDiscounts(
List<ID> invoiceIds = =
List<ID> opportunityIds
07:
07: new List<ID> { standardController.getId() }, DiscountPercentage);
OpportunitiesService.createInvoices(
OpportunitiesService.createOpportunities(
08:
08: } new List<ID> { standardController.getId() }, DiscountPercentage);
List<ID> { standardController.getId() }, selectedLines);
09:
09: catch (Exception e)
10:
10: { // Redirect to Invoice
// Redirect to Opportunity
11:
11: ApexPages.addMessages(e);
return new PageReference('/'+invoiceIds[0]);
return PageReference('/'+opportunity[0]);
12:
12: }
13:
13: return null;
}
}
14: }
14: catch (Exception e)
catch (Exception e)
15: {
{
16: ApexPages.addMessages(e);
ApexPages.addMessages(e);
17: }
}
18: return null;
return null;
19: }
15. Sample B: Service and Unit Of Work
/**
* Service methods are the main entry point for your applications
* logic. Your controller methods call these. In addition they can
* form a public API for partners and developers using your
* application. Service methods utilize code from Domain, Selector
* and Unit Of Work classes.
*/
01: global with sharing class OpportunitiesService
02: {
03: global static void applyDiscounts(Set<ID> opportunityIds, Decimal discountPercentage)
04: {
05: // Create unit of work to capture work and commit it under one transaction
06: SObjectUnitOfWork uow = new SObjectUnitOfWork(SERVICE_SOBJECTS);
07: // Query Opportunities (including products) and apply discount
08: Opportunities opportunities = new Opportunities(
09: new OpportunitiesSelector().selectByIdWithProducts(opportunityIds));
10: opportunities.applyDiscount(discountPercentage, uow);
11: // Commit updates to opportunities
12: uow.commitWork();
13: }
14:
15: // SObject's used by the logic in this service, listed in dependency order
16: private static List<Schema.SObjectType> SERVICE_SOBJECTS =
17: new Schema.SObjectType[] {
18: Invoice__c.SObjectType,
19: Opportunity.SObjectType,
20: OpportunityLineItem.SObjectType };
16. Sample B: Service and Unit Of Work
/**
* This service method passes its Unit Of Work to a related domain
* class such that it can also register any database changes.
* See Opportunities.applyDiscount method.
*/
01: global static List<Id> createInvoices(Set<ID> opportunityIds, Decimal discountPercentage)
02: {
03: // Create unit of work to capture work and commit it under one transaction
04: SObjectUnitOfWork unitOfWork = new SObjectUnitOfWork(SERVICE_SOBJECTS);
05: // Query Opportunities
06: Opportunities opportunities = new Opportunities(
07: new OpportunitiesSelector().selectByIdWithProducts(opportunityIds));
08: // Optionally apply discounts as part of invoice creation
09: if(discountPercentage!=null && discountPercentage>0)
10: opportunities.applyDiscount(discountPercentage, unitOfWork);
11: // Create Invoices from the given opportunities
12: List<Invoice__c> invoices = new List<Invoice__c>();
13: for(Opportunity opportunityRecord : (List<Opportunity>) opportunities.Records)
14: {
15: // Create Invoice and populate invoice fields (removed here) from Opportunity ...
16: Invoice__c invoice = new Invoice__c();
17: unitOfWork.registerNew(invoice);
18: }
19: // Commit any Opportunity updates and new Invoices
20: unitOfWork.commitWork();
17. Sample B: Unit of Work : Commit Work
01: public with sharing class SObjectUnitOfWork
02: {
03: public void commitWork()
04: {
05: // Wrap the work in its own transaction
06: Savepoint sp = Database.setSavePoint();
07: try
08: {
09: // Insert by type
10: for(Schema.SObjectType sObjectType : m_sObjectTypes)
11: {
12: m_relationships.get(sObjectType.getDescribe().getName()).resolve();
13: insert m_newListByType.get(sObjectType.getDescribe().getName());
14: }
15:: // Update by type
16: for(Schema.SObjectType sObjectType : m_sObjectTypes)
17: update m_dirtyListByType.get(sObjectType.getDescribe().getName());
18: // Delete by type (in reverse dependency order)
19: Integer objectIdx = m_sObjectTypes.size() - 1;
20: while(objectIdx>=0)
21: delete m_deletedListByType.get(m_sObjectTypes[objectIdx--].getDescribe().getName());
22: }
23: catch (Exception e)
24: {
25: // Rollback
26: Database.rollback(sp);
27: // Throw exception on to caller
28: throw e;
29: }
30: }
31: }
18. Sample B: Data Mapper : Simple
/**
* These classes manage in a single place all queries to a given object. Maintaining a single list of
* fields that will be queried. The base class SObjectSelector provides selectSObjectById method for free!
* NOTE: This base class also handles CRUD security.
*/
01: public with sharing class OpportunitiesSelector extends SObjectSelector
02: {
03: public List<Schema.SObjectField> getSObjectFieldList()
04: {
05: return new List<Schema.SObjectField> {
06: Opportunity.AccountId, Opportunity.Amount, Opportunity.CloseDate, Opportunity.Description,
07: Opportunity.ExpectedRevenue, Opportunity.Id, Opportunity.Name, Opportunity.Pricebook2Id,
08: Opportunity.Probability, Opportunity.StageName, Opportunity.Type, Opportunity.DiscountType__c
09: };
10: }
11: public Schema.SObjectType getSObjectType()
12: {
13: return Opportunity.sObjectType;
14: }
15: public List<Opportunity> selectById(Set<ID> idSet)
16: {
17: return (List<Opportunity>) selectSObjectsById(idSet);
18: }
19: }
19. Sample B: Data Mapper : Complex
/**
* This additional Selector method illustrates how other selectors can be consumed to ensure that
* field lists from other objects queried via a parent object query are also consistently applied.
* NOTE: The SObjectSelector logic is also multi-company aware, injecting CurrencyIsoCode as needed.
*/
01: public List<Opportunity> selectByIdWithProducts(Set<ID> idSet)
02: {
03: /** Omitted for brevity: Construction of dependent selectors */
04:
05: String query = String.format(
06: 'select {0},
07: (select {3},{5},{6},{7} from OpportunityLineItems order by {4}) ' +
08: 'from {1} where id in :idSet order by {2}',
09: new List<String> {
10: getFieldListString(), getSObjectName(), getOrderBy(),
11: opportunityLineItemSelector.getFieldListString(),
12: opportunityLineItemSelector.getOrderBy(),
13: pricebookEntrySelector.getRelatedFieldListString('PricebookEntry'),
14: productSelector.getRelatedFieldListString('PricebookEntry.Product2'),
15: pricebookSelector.getRelatedFieldListString('PricebookEntry.Pricebook2’) });
16:
17: return (List<Opportunity>) Database.query(query);
18: }
20. Sample B: Domain Model : Trigger and Domain Class
/**
* Implement the Apex Trigger by calling the SObjectDomain method triggerHandler, which routes to the
* appropriate methods implemented in the domain class. Such as applyDefaults() during
* record inserts. Other methods are validate(), beforeInsert (), afterInsert() etc..
*/
01: trigger OpportunitiesTrigger on Opportunity
02: (after delete, after insert, after update, before delete, before insert, before update)
03: {
04: // Creates Domain class instance and calls appropriate override methods according to Trigger state
05: SObjectDomain.triggerHandler(Opportunities.class);
06: }
01: public with sharing class Opportunities extends SObjectDomain
02: {
03: public Opportunities(List<Opportunity> sObjectList)
04: {
05: super(sObjectList); // Classes are initialized with lists to enforce bulkification throughout
06: }
07:
08: public override void applyDefaults()
09: {
10: for(Opportunity opportunity : (List<Opportunity>) Records) // Apply defaults to Opportunities
11: {
12: opportunity.DiscountType__c = OpportunitySettings__c.getInstance().DiscountType__c;
13: }
14: }
15: }
21. Sample B: Domain Model : Business Logic
01: public void applyDiscount(Decimal discountPercentage, SObjectUnitOfWork unitOfWork)
/**
02: {
* Domain class method implements 03: // Calculate discount factor
* the discount logic, note 04: Decimal factor = Util.calculateDiscountFactor(discountPercentage);
* bulkification is enforced here. 05: // Opportunity lines to apply discount to
* Unit of Work for database 06: List<OpportunityLineItem> linesToApplyDiscount = new List<OpportunityLineItem>();
* updates. While delegating to 07: // Apply discount
* the domain class responsible 08: for(Opportunity opportunity : (List<Opportunity>) Records)
09: {
* for Opportunity Products to
10: // Apply to the Opportunity Amount?
* apply discounts as needed to 11: if(opportunity.OpportunityLineItems.size()==0)
* product lines. 12: {
*/ 13: // Adjust the Amount on the Opportunity if no lines
14: opportunity.Amount = opportunity.Amount * factor;
15: unitOfWork.registerDirty(opportunity);
16: }
17: else
18: {
19: // Collect lines to apply discount to
20: linesToApplyDiscount.add(opportunity.OpportunityLineItems);
21: }
22: }
23: // Apply discount to lines
24: OpportunityLineItems lineItems = new OpportunityLineItems(linesToApplyDiscount);
25: lineItems.applyDiscount(discountPercentage, unitOfWork);
26: }
22. Summary
Separation of Concern Pattern. Code is factored in respect to its purpose
and responsibility and thus easier to maintain and behaves consistently.
Service Pattern, task and process based logic is exposed consistently from a
single place and consumed by all ‘clients’. Controllers and external callers.
Data Mapper, the selector classes provide a single place to query objects and
enforce CRUD security.
Unit Of Work Pattern provides a single transactional context via the
commitWork method. Bulkification of DML operations is simplified via
registerXXX methods.
Domain Pattern, provides object orientated model for defaulting, validation
and behavioral logic. Since domain classes manage record sets, they also
enforce bulkfication throughout your logic not just in triggers. Trigger
implementations can be standardized.
23. Resources
Java Code : {SourceObject__c} Listener
• Other // TODO:’s and Ideas?!
Review SObjectUnitOfWork further, it also aids in inserting related
records and updating references!!
Auto handle CRUD security in SObjectDomain base class trigger
delegate methods?
• Source Code and Contact Details
GitHub: https://github.com/financialforcedev
Twitter: andyinthecloud
Editor's Notes
Rehab Breakfast on Friday, talking to people in the area for roles we have open, come along even if your tired! ;-)Expanding Development Group, adding Development Team in SFIndustrial Strength Applications
Timing: 1 minuteNOTE: Method should return to Opportunity page?
Timing: 1 minute
Timing: 1 minuteNOTE: Probably easier to just say DiscountApproved__c == true and then do the UnitPrice adjustment?
Timing: 1 minute
Timing: 1 minute
Timing: 2 minutes
Timing: 2 minutes
Timing: 1 minutes
Timing: 2 minutes
Timing: 4 minutes
Timing: 4 minutes
Timing: 1 minuteNOTE: Need to make the above field list reflect that used by the Sample A queries ideally.