Advertisement
Advertisement

More Related Content

Viewers also liked(19)

Advertisement

DF12 - Applying Enterprise Application Design Patterns on Force.com

  1. Applying Enterprise Application Design Patterns on Force.com Andrew Fawcett, FinancialForce.com, CTO @andyinthecloud
  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
  4. Andrew Fawcett CTO @andyinthecloud
  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
  13. Sample B: Class Diagram
  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

  1. 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
  2. Timing: 1 minuteNOTE: Method should return to Opportunity page?
  3. Timing: 1 minute
  4. Timing: 1 minuteNOTE: Probably easier to just say DiscountApproved__c == true and then do the UnitPrice adjustment?
  5. Timing: 1 minute
  6. Timing: 1 minute
  7. Timing: 2 minutes
  8. Timing: 2 minutes
  9. Timing: 1 minutes
  10. Timing: 2 minutes
  11. Timing: 4 minutes
  12. Timing: 4 minutes
  13. Timing: 1 minuteNOTE: Need to make the above field list reflect that used by the Sample A queries ideally.
  14. Timing: 1 minute
  15. Timing: 2 minutes
  16. Timing: 3 minutes
  17. Timing: 3 minutes
  18. Timing: Code Walkthrough 8 minutes
Advertisement