DF12 - Applying Enterprise Application Design Patterns on Force.com

4,577 views

Published on

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.

Published in: Technology
  • Be the first to comment

DF12 - Applying Enterprise Application Design Patterns on Force.com

  1. 1. Applying Enterprise ApplicationDesign Patterns on Force.comAndrew Fawcett, FinancialForce.com, CTO@andyinthecloud
  2. 2. 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 uncertaintiesmaterialize or if any of the assumptions proves incorrect, the results of salesforce.com, inc. could differ materially from the results expressed orimplied 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 statementsregarding strategies or plans of management for future operations, statements of belief, any statements concerning new, planned, or upgradedservices 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 functionalityfor our service, new products and services, our new business model, our past operating losses, possible fluctuations in our operating resultsand rate of growth, interruptions or delays in our Web hosting, breach of our security measures, the outcome of intellectual property and otherlitigation, risks associated with possible mergers and acquisitions, the immature market in which we operate, our relatively limited operatinghistory, our ability to expand, retain, and motivate our employees and manage our growth, new releases of our service and successfulcustomer 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 SECFilings 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 availableand may not be delivered on time or at all. Customers who purchase our services should make the purchase decisions based upon featuresthat are currently available. Salesforce.com, inc. assumes no obligation and does not intend to update these forward-looking statements.
  3. 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
  4. 4. Andrew FawcettCTO@andyinthecloud
  5. 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. 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 ProductsAgenda : Code walk throughs…  Review code sets to compare, discuss and contrast  Sample A is implemented without considering patterns…
  7. 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. 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 to05: Opportunity opportunity =06: [select Id, Name, AccountId, (select UnitPrice, Description, Quantity from OpportunityLineItems)07: from Opportunity o08: where Id = :standardController.getId()];09: // Apply discount to line items10: Util.applyDiscount(opportunity, DiscountPercentage); Transaction issue!11: // Create Invoice from line items Opportunity line changes are12: Id invoiceId = Util.createInvoice(opportunity);13: // Redirect to Invoice not rolled back on Invoice14: return new PageReference(/+invoiceId); creation failure. Automatic15: } rollback only occurs for16: catch (Exception e) { unhandled exceptions.17: ApexPages.addMessages(e);18: }
  9. 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 UnitPrice07: 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 o11: where Id = :standardController.getId()];12: // Apply discount to line items13: Util.applyDiscount(opportunity, DiscountPercentage);
  10. 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: 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 o10: 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 Invoice16: return n17: }
  11. 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 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 lines In order to ensure consistent15: List<OpportunityLineItem> lines = behavior, across this wizard, Salesforce UI and API’s, this logic needs to be placed in a trigger
  12. 12. Sample A: Create Invoice @ v2Issues Discussed • Partial database updates in error conditions • Data dependent runtime errors • Inconsistent behavior between UI’s, API’s and ToolsOther 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
  13. 13. Sample B: Class Diagram
  14. 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. 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: // SObjects 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. 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. 17. Sample B: Unit of Work : Commit Work01: public with sharing class SObjectUnitOfWork02: {03: public void commitWork()04: {05: // Wrap the work in its own transaction06: 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: }
  18. 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 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: }
  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. 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 Opportunity02: (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: }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: }
  21. 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. 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. 23. ResourcesJava 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

×