df12 - applying enterprise application design patterns on force.com

25
Applying Enterprise Application Design Patterns on Force.com Andrew Fawcett, FinancialForce.com, CTO @andyinthecloud

Upload: afawcett

Post on 18-Nov-2014

3.367 views

Category:

Technology


0 download

DESCRIPTION

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.

TRANSCRIPT

Page 1: DF12 - Applying Enterprise Application Design Patterns on Force.com

Applying Enterprise Application Design Patterns on Force.com

Andrew Fawcett, FinancialForce.com, CTO

@andyinthecloud

Page 2: DF12 - Applying Enterprise Application Design Patterns on Force.com

Safe harborSafe 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.

Page 3: DF12 - Applying Enterprise Application Design Patterns on Force.com

All about FinancialForce.com

• #1 Accounting App on Force.com• #1 Professional Services Automation App on Force.com

Leading Native ISV on Force.com

• UNIT4 - $600 million, 33 years building business apps

Backed by Salesforce.com & UNIT4

• San Francisco HQ – 595 Market St.• 145 Employees• Customers in 23 countries

Growing Rapidly

Page 4: DF12 - Applying Enterprise Application Design Patterns on Force.com

Andrew Fawcett

CTO

@andyinthecloud

Page 5: DF12 - Applying Enterprise Application Design Patterns on Force.com

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

Page 6: DF12 - Applying Enterprise Application Design Patterns on Force.com

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…

Page 7: DF12 - Applying Enterprise Application Design Patterns on Force.com

/** * Version: v1 * Visualforce Controller accepts a discount value * from the user and applies it to the Opportunity */

01: public PageReference applyDiscount()02: {03: try04: {05: // Query Opportunity and line items to apply discount to06: Opportunity opportunity = 07: [select Id, Name, (select UnitPrice from OpportunityLineItems) 08: from Opportunity o 09: where Id = :standardController.getId()]; 10: // Apply discount to line items11: Util.applyDiscount(opportunity, DiscountPercentage);12: }13: catch (Exception e)14: {15: ApexPages.addMessages(e);16: }17: return null;18: }

01: // Calculate discount factor02: Decimal factor = Util.calculateFactor(discountPercentage);03: if(opportunity.OpportunityLineItems.size()==0)04: {05: // Adjust the Amount on the Opportunity if no lines06: opportunity.Amount = opportunity.Amount * factor;07: update opportunity;08: }09: else10: {11: // Adjust UnitPrice of each line12: for(OpportunityLineItem line : opportunity.OpportunityLineItems)13: line.UnitPrice = line.UnitPrice * factor;14: update opportunity.OpportunityLineItems;15: }

Sample A: Apply Discount @ v1

Page 8: DF12 - Applying Enterprise Application Design Patterns on Force.com

/** * 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 to05: 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 items10: Util.applyDiscount(opportunity, DiscountPercentage);11: // Create Invoice from line items12: Id invoiceId = Util.createInvoice(opportunity);13: // Redirect to Invoice 14: return new PageReference('/'+invoiceId);15: }16: catch (Exception e) {17: ApexPages.addMessages(e);18: }

Transaction issue!Opportunity line changes are

not rolled back on Invoice creation failure. Automatic

rollback only occurs for unhandled exceptions.

Sample A: Create Invoice @ v1

Page 9: DF12 - Applying Enterprise Application Design Patterns on Force.com

/** * 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:public PageReference applyDiscount()02:{03: try04: {05: // Query Opportunity and line items to apply discount to06: // ENH:1024. Added new columns to support new features in applyDiscount07: Opportunity opportunity = 08: [select Id, Name, DiscountType__c, 09: (select UnitPrice, PricebookEntry.Product2.DiscountingApproved__c from OpportunityLineItems) 10: from Opportunity o 11: where Id = :standardController.getId()];12: // Apply discount to line items13: Util.applyDiscount(opportunity, DiscountPercentage);

01: // ENH:1024. Adjust UnitPrice of each line according to Discount Type of Opportunity02: for(OpportunityLineItem line : opportunity.OpportunityLineItems)03: {04: // ENH:1024. Skip products that have not been approved for discounting01:05: if(opportunity.DiscountType__c == 'Approved Products')06: if(line.PricebookEntry.Product2.DiscountingApproved__c == false)07: continue;08: // Adjust UnitPrice09: line.UnitPrice = line.UnitPrice * factor;10: }

Sample A: Apply Discount @ v2

Page 10: DF12 - Applying Enterprise Application Design Patterns on Force.com

/** * 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: try04: {05: // Query Opportunity and line items to apply discount to06: 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 items12: Util.applyDiscount(opportunity, DiscountPercentage);13: // Create Invoice from line items14: Id invoiceId = Util.createInvoice(opportunity);15: // Redirect to Invoice 16: return n17: }

Sample A: Create Invoice @ v2

Page 11: DF12 - Applying Enterprise Application Design Patterns on Force.com

/** * 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 Account06: 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 lines15: List<OpportunityLineItem> lines = In order to ensure consistent

behavior, across this wizard, Salesforce UI and API’s, this logic needs to be placed in a trigger

Sample A: Opportunity Wizard

Page 12: DF12 - Applying Enterprise Application Design Patterns on Force.com

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

Sample A: Create Invoice @ v2

Page 13: DF12 - Applying Enterprise Application Design Patterns on Force.com

Sample B: Class Diagram

Page 14: DF12 - Applying Enterprise Application Design Patterns on Force.com

/** * 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()02: {03: try04: {05: // Apply discount entered to the current Opportunity06: OpportunitiesService.applyDiscounts(07: new List<ID> { standardController.getId() }, DiscountPercentage);08: }09: catch (Exception e)10: {11: ApexPages.addMessages(e);12: }13: return null;14: }

01: public PageReference createInvoice()02: {03: try04: {05: // Create Invoice from line items of the current Opportunity06: List<ID> invoiceIds = 07: OpportunitiesService.createInvoices(08: new List<ID> { standardController.getId() }, DiscountPercentage);09: 10: // Redirect to Invoice 11: return new PageReference('/'+invoiceIds[0]);12: 13: }14: catch (Exception e)15: {16: ApexPages.addMessages(e);17: }18: return null;19: }

01: public PageReference createOpportunity()02: {03: try04: {05: // Create Opportunity from line items selected by the user06: List<ID> opportunityIds = 07: OpportunitiesService.createOpportunities(08: new List<ID> { standardController.getId() }, selectedLines);09: 10: // Redirect to Opportunity11: return new PageReference('/'+opportunity[0]);12: 13: }14: catch (Exception e)15: {16: ApexPages.addMessages(e);17: }18: return null;19: }

Sample B: Controller Methods

Page 15: DF12 - Applying Enterprise Application Design Patterns on Force.com

/** * 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 transaction06: SObjectUnitOfWork uow = new SObjectUnitOfWork(SERVICE_SOBJECTS);07: // Query Opportunities (including products) and apply discount08: Opportunities opportunities = new Opportunities(09: new OpportunitiesSelector().selectByIdWithProducts(opportunityIds));10: opportunities.applyDiscount(discountPercentage, uow);11: // Commit updates to opportunities12: uow.commitWork();13: }14: 15: // SObject's used by the logic in this service, listed in dependency order16: private static List<Schema.SObjectType> SERVICE_SOBJECTS = 17: new Schema.SObjectType[] { 18: Invoice__c.SObjectType, 19: Opportunity.SObjectType, 20: OpportunityLineItem.SObjectType };

Sample B: Service and Unit Of Work

Page 16: DF12 - Applying Enterprise Application Design Patterns on Force.com

/** * 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 transaction04: SObjectUnitOfWork unitOfWork = new SObjectUnitOfWork(SERVICE_SOBJECTS);05: // Query Opportunities06: Opportunities opportunities = new Opportunities(07: new OpportunitiesSelector().selectByIdWithProducts(opportunityIds));08: // Optionally apply discounts as part of invoice creation09: if(discountPercentage!=null && discountPercentage>0)10: opportunities.applyDiscount(discountPercentage, unitOfWork);11: // Create Invoices from the given opportunities12: 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 Invoices20: unitOfWork.commitWork();

Sample B: Service and Unit Of Work

Page 17: DF12 - Applying Enterprise Application Design Patterns on Force.com

Sample B: Unit of Work : Commit Work01: public with sharing class SObjectUnitOfWork02: {03: public void commitWork()04: {05: // Wrap the work in its own transaction 06: Savepoint sp = Database.setSavePoint();07: try08: {09: // Insert by type10: 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 type16: 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: // Rollback26: Database.rollback(sp);27: // Throw exception on to caller28: throw e;29: }30: }31: }

Page 18: DF12 - Applying Enterprise Application Design Patterns on Force.com

/** * 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 SObjectSelector02: {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__c09: };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: }

Sample B: Data Mapper : Simple

Page 19: DF12 - Applying Enterprise Application Design Patterns on Force.com

/** * 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: }

Sample B: Data Mapper : Complex

Page 20: DF12 - Applying Enterprise Application Design Patterns on Force.com

/** * 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 state05: SObjectDomain.triggerHandler(Opportunities.class);06: }

Sample B: Domain Model : Trigger and Domain Class

01: public with sharing class Opportunities extends SObjectDomain02: {03: public Opportunities(List<Opportunity> sObjectList)04: {05: super(sObjectList); // Classes are initialized with lists to enforce bulkification throughout06: }07: 08: public override void applyDefaults()09: {10: for(Opportunity opportunity : (List<Opportunity>) Records) // Apply defaults to Opportunities11: {12: opportunity.DiscountType__c = OpportunitySettings__c.getInstance().DiscountType__c;13: }14: }15: }

Page 21: DF12 - Applying Enterprise Application Design Patterns on Force.com

/** * Domain class method implements * the discount logic, note * bulkification is enforced here. * Unit of Work for database * updates. While delegating to * the domain class responsible * for Opportunity Products to * apply discounts as needed to * product lines. */

01: public void applyDiscount(Decimal discountPercentage, SObjectUnitOfWork unitOfWork)02: {03: // Calculate discount factor04: Decimal factor = Util.calculateDiscountFactor(discountPercentage);05: // Opportunity lines to apply discount to06: List<OpportunityLineItem> linesToApplyDiscount = new List<OpportunityLineItem>();07: // Apply discount 08: for(Opportunity opportunity : (List<Opportunity>) Records)09: {10: // Apply to the Opportunity Amount?11: if(opportunity.OpportunityLineItems.size()==0)12: {13: // Adjust the Amount on the Opportunity if no lines14: opportunity.Amount = opportunity.Amount * factor;15: unitOfWork.registerDirty(opportunity);16: }17: else18: {19: // Collect lines to apply discount to20: linesToApplyDiscount.add(opportunity.OpportunityLineItems);21: }22: }23: // Apply discount to lines24: OpportunityLineItems lineItems = new OpportunityLineItems(linesToApplyDiscount);25: lineItems.applyDiscount(discountPercentage, unitOfWork);26: }

Sample B: Domain Model : Business Logic

Page 22: DF12 - Applying Enterprise Application Design Patterns on Force.com

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.

Summary

Page 23: DF12 - Applying Enterprise Application Design Patterns on Force.com

Java Code : {SourceObject__c} ListenerResources• 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 DetailsGitHub: https://github.com/financialforcedev

Twitter: andyinthecloud

Page 24: DF12 - Applying Enterprise Application Design Patterns on Force.com
Page 25: DF12 - Applying Enterprise Application Design Patterns on Force.com