One of the big advantages of using Apex classes is the ability to leverage the power of OOP. OOP allows you to observe commonalities in data or behavior across your objects to share code or apply common processing across different objects.
An example of such a commonality in the Formula1 world are rules; every aspect of the sport has a set of rules and regulations to comply with, such as drivers owning a FIA Super License, the weight of the car they drive should be at least above a defined minimum, and ensuring that a team has not exceeded the maximum distance in testing their cars. Such compliances are checked regularly, both before and after a race.
In our FormulaForce application, we want to create a compliance framework that will check these regulations across the different objects while also providing a consistent user interface experience for the end user to verify compliance. The initial requirement is to place a Verify Compliance button on the Detail Pages on all applicable objects.
The Visualforce Controller and Service code that perform the verification process should be generic and reusable across these objects, while the actual compliance-checking logic associated with each object will be specific. By using an Apex Interface, we can define this common requirement for the applicable Domain classes to implement.
In this section, we will implement this requirement using an Apex interface and a generic service that can be passed record IDs for different Domain objects across the application. Creating such a service avoids code duplication by having to repeat the logic of handling the results within the code of each Domain class and provides a centralized service for the controller to consume regardless of the object type in scope.
By using an Apex interface and a generic service, we separate the functional concerns of implementing compliance functionality as follows:
Having this Separation of Concerns makes it easy to significantly modify or extend the ways in which the checking is invoked and presented to the user without having to revisit each of the Domain layer classes or conversely, modify the compliance logic of one object without impacting the overall implementation across the application.
Interfaces help express common attributes or behaviors across different domain classes. In this section, we will use an Apex interface to help expose the compliance-checking logic encapsulated in each of the applicable Domain classes.
In order to exercise and implement the compliance framework, the FormulaForce application has gained a few extra fields and objects. These objects, additional classes, and Visualforce pages are all included in the code samples of this chapter.
To summarize, the following steps have been taken to implement the compliance framework requirement. You can follow along with these or simply refer to the sample code of this chapter:
Cars
Domain class and CarsSelector
Selector class.ComplianceService
class and ICompliant
interface.Cars
and Drivers
Domain classes and implement the ICompliant
interface.Application
class to implement a Domain class factoryComplianceService.verify
method that utilizes the Domain factory.The last step will create two user interfaces, one for Salesforce Classic and one for Lightning Experience. The ComplianceController
class provides a generic controller class for each specific Visualforce page per object. In addition the ComplianceChecker
bundle provides a generic Lightning Component called Compliance Checker, which can be dropped on any object page.
The following sections describe in further detail the Apex coding from Step 5 onwards.
The aim is to develop a common service to handle all compliance verifications across the application. So, a new ComplianceService
class has been created with a verify
method on it, which is capable of receiving IDs from various objects. If there are any verification errors, they will be thrown as part of an exception. If the method returns without throwing an exception, the given records are compliant. The following code snippet shows the creation of a new ComplianceService
class with a verify
method:
public class ComplianceService { /** * Provides general support to verify compliance in the * application * @throws ComplianceException for any failures **/ public static void verify(Set<Id>recordIds) { // Query the given records and delegate to the // corresponding Domain class to check compliance // and report failures via ComplianceException }
Before we look further into how this service method can be implemented, take a look at the new ICompliant
interface and the verifyCompliance
method that will be implemented by the Drivers
and Cars
Domain classes:
public class ComplianceService { /** * Interface used to execute compliance checking logic * in each domain class **/ public interface ICompliant { List<VerifyResult>verifyCompliance(); } /** * Results of a compliance verification for a given record **/ public class VerifyResult { public Id recordId; public String complianceCode; public Boolean passed; public String failureReason; } }
The Drivers
Domain class implements the interface as follows (only a portion of the class is shown). It checks whether the FIA Super License field is checked and reports an appropriate compliance code and failure message if not:
public class Drivers extends ApplicationDomain implements ComplianceService.ICompliant { public List<ComplianceService.VerifyResult> verifyCompliance() { List<ComplianceService.VerifyResult> compliances = new List<ComplianceService.VerifyResult>(); for(Driver__c driver : (List<Driver__c>) Records) { ComplianceService.VerifyResult license = new ComplianceService.VerifyResult(); license.ComplianceCode = '4.1'; license.RecordId = driver.Id; license.passed = driver.FIASuperLicense__c; license.failureReason = license.passed ? null : 'Driver must have a FIA Super License.'; compliances.add(license); } return compliances; } }
The Cars
domain class (of which only a portion of the class is shown) implements the verifyCompliance
method as follows, checking whether the car is over the specified weight:
public class Cars extends ApplicationDomain implements ComplianceService.ICompliant { public List<ComplianceService.VerifyResult> verifyCompliance() { List<ComplianceService.VerifyResult> compliances = new List<ComplianceService.VerifyResult>(); for(Car__c car : (List<Car__c>) Records) { // Check weight compliance ComplianceService.VerifyResult license = new ComplianceService.VerifyResult(); license.ComplianceCode = '4.1'; license.RecordId = car.Id; license.passed = car.Weight__c !=null && car.Weight__c>= 691; license.failureReason = license.passed ? null : 'Car must not be less than 691kg.'; compliances.add(license); } return compliances; } }
The next step is to dynamically construct the associated Domain class based on the record IDs passed into the ComplianceService.verify
method. This part of the implementation makes use of a Factory pattern implementation for Domain classes.
A Wikipedia reference to the Factory pattern can be found at http://en.wikipediaedia.org/wiki/Factory_method_pattern.
The implementation uses the Apex runtime Id.getSObjectType
method to establish SObjectType
, and then via the Maps
(shown in the following code), it determines the applicable Domain and Selector classes to use (to query the records).
The Apex runtime Type.newInstance
method is then used to construct an instance of the Domain and Selector classes. The factory functionality is invoked by calling the Application.Domain.newInstance
method. The Domain
field on the Application class is implemented using the fflib_Application.DomainFactory
class as follows:
public class Application { public static final fflib_Application.DomainFactory Domain = new fflib_Application.DomainFactory( Application.Selector, // Map SObjectType to Domain Class Constructors new Map<SObjectType, Type> { Race__c.SObjectType =>Races.Constructor.class, Car__c.SObjectType =>Cars.Constructor.class, Driver__c.SObjectType =>Drivers.Constructor.class }); }
As a developer you can of course instantiate a specific Domain class directly. However, this Domain factory approach allows for the specific Domain class to be chosen at runtime based on the SObjectType
associated with the ID passed to the newInstance
method. This is key to implementing the generic service entry point as shown in the next step. The preceding initialization of the Domain factory also passes in Application.Selector
; this is another factory relating to classes that are able to query the database. The next chapter will cover the Selector pattern and the Selector factory in more detail.
The Application.Domain.newInstance
method used in the following code is used to access the Domain instance factory defined in the last step.
This method returns fflib_ISObjectDomain
, as this is a common base interface to all Domain classes. Then, using the instanceof
operator, it determines whether it implements the ICompliant
interface, and if so, it calls the verifyCompliance
method. The following code shows the implementation of a generic service:
public class ComplianceService { public static void verify(Set<Id>recordIds) { // Dynamically create Domain instance for these records fflib_ISObjectDomain domain = Application.Domain.newInstance(recordIds); if(domain instanceof ICompliant) { // Ask the domain class to verify its compliance ICompliantcompliantDomain = (ICompliant) domain; List<VerifyResult>verifyResults = compliantDomain.verifyCompliance(); if(verifyResults!=null) { // Check for failed compliances List<VerifyResult> failedCompliances = new List<VerifyResult>(); for(VerifyResultverifyResult : verifyResults) { if(!verifyResult.passed) { failedCompliances.add(verifyResult); } } if(failedCompliances.size()>0) { throw new ComplianceException( 'Compliance failures found.', failedCompliances); return; } } throw new ComplianceException( 'Unable to verify compliance.', null); } } }
Note that, if the verify
method is passed IDs for an object that does not have a corresponding Domain class, an exception is thrown from the factory. However, if a domain class is found but does not implement the interface, the preceding code simply returns without an error, though what the developer will do in this case is up to them.
In this step we will utilize the same ComplianceService.verify
method from a Visualforce Controller and a Lightning Component controller. These user interfaces are also highly generic and thus easier to apply to new objects in future releases.
A generic Visualforce Controller, ComplianceController
, has been created with a verify
action method that calls the service. Note that at no time have we yet referred to a specific Custom Object or Domain class, ensuring that this controller and the service are generic and able to handle objects of different types. The following code shows how the generic ComplianceService.verify
method is being called from the ComplianceController.verify
method:
public PageReference verify() { try { // Pass the record to the compliance service ComplianceService.verify( new Set<Id> { standardController.getId() }); // Passed! ApexPages.addMessage( new ApexPages.Message( ApexPages.Severity.Info, 'Compliance passed')); } catch (Exception e) { // Report message as normal via apex:pageMessages ApexPages.addMessages(e); // Display additional compliance failure messages? if(e instanceofComplianceService.ComplianceException) { ComplianceService.ComplianceExceptionce (ComplianceService.ComplianceException) e; for(ComplianceService.VerifyResultverifyResult :ce.failures) { ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error, String.format('{0} ({1})', new List<String> {verifyResult.failureReason,verifyResult.complianceCode }))); } } } return null; }
Visualforce pages are then created to bind this generic controller to Custom Buttons on the Driver and Car detail pages. This is the only specific reference made to objects that implement the ComplianceService.ICompliant
interface and is only needed in order to create the Custom Buttons in Salesforce Classic. The following page for the Driver object defines a button that uses the generic ComplianceController.verify
method:
<apex:pagestandardController="Driver__c"
extensions="ComplianceController" action="{!verify}">
<apex:pageMessages/>
<apex:form>
<apex:commandButton value="Cancel" action="{!cancel}"/>
</apex:form>
</apex:page>
The component described in this section can be placed on a Lightning Experience or Salesforce1 Mobile record pages for any object without any further configuration. The component utilizes the same Service as the previous Visualforce example to dynamically invoke the appropriate Domain class and apply the applicable verification process.
The following code shows the Apex Controller for the component:
public with sharing class ComplianceCheckerComponent { @AuraEnabled public static List<String> verify(Id recordId) { try { // Verify the given record for compliance ComplianceService.verify(newSet<Id> { recordId }); // Success, all good! Return null; } catch (Exception e) { // Report message as normal via apex:pageMessages List<String> messages = new List<String> { e.getMessage() }; // Display additional compliance failure messages? If(e instanceofComplianceService.ComplianceException) { ComplianceService.ComplianceExceptionce =(ComplianceService.ComplianceException) e; for(ComplianceService.VerifyResultverifyResult : ce.failures) { messages.add(String.format('{0} ({1})', new List<String> { verifyResult.failureReason, verifyResult.complianceCode })); } } return messages; } }
The following code shows the component markup, JavaScript controller and helper:
ComplianceChecker.cmp:
<aura:component implements="force:hasRecordId,flexipage:availableForRecordHome" controller="ComplianceCheckerComponent" access="global"> <aura:dependency resource="markup://force:editRecord"type="EVENT" /> <aura:attribute name="category" type="String" default="success" /> <aura:attribute name="messages" type="String[]"/> <aura:handler name="init" value="{!this}"action="{!c.onInit}"/> <aura:handler event="force:refreshView"action="{!c.onRefreshView}"/> <div class="{!'slds-box slds-theme—' + v.category}"> <aura:iteration items="{!v.messages}"var="message"> <p><ui:outputText value="{!message}"/></p> </aura:iteration> </div> </aura:component>
ComplianceCheckerControler.js:
({ onInit : function(component, event, helper) { helper.verifyCompliance(component, event, helper); }, onRefreshView : function(component, event, helper) { helper.verifyCompliance(component, event, helper); } })
ComplianceCheckerHelper.js:
({ verifyCompliance : function(component) { var action = component.get("c.verify"); action.setParams({ "recordId" : component.get("v.recordId") }); action.setCallback(this, function(response) { if(response.getState() === 'SUCCESS') { var messages = response.getReturnValue(); if(messages!=null) { component.set("v.category", "error"); component.set("v.messages", messages); } else { component.set("v.category", "success"); component.set("v.messages", ["Verified compliance" ]); } } }); $A.enqueueAction(action); } })
Unlike the Visualforce page, no specific reference to an object is made, allowing the component to be used on any page.
If you wanted to restrict the visibility of the component within the Lightning App Builder tool where users customize Lightning Experience and Salesforce1 Mobile, you can utilize the sfdc:objects
tag in your .design
file.
<sfdc:objects><sfdc:object>Driver__c</sfdc:object><sfdc:object>Car__c</sfdc:object></sfdc:objects>
Using an Apex Interface and the Factory pattern, a framework has been put in place within the application to ensure that the compliance logic is implemented consistently. This is a simple matter of implementing the Domain class interface and creating the Visualforce page or using the generic Lightning component, to enable the feature for other applicable objects in the future.
The following screenshot illustrates the Verify Compliance button on the Driver object. Ensure that the FIA Super License field is unchecked before pressing the button.
The following errors should be displayed:
The following screenshot shows the Verify Compliance button on the Car object:
The following errors are shown by the generic controller defined/created previously:
The following shows the Compliance Checker Lightning Component within the Lightning App Builder tool being placed on the Driver page:
The following shows the Compliance Checker component in place on the Driver page. In Lightning Experience, there is no button, the component is embedded in the page and reacts to the field updates the user makes. So, the verification is performed immediately.
3.129.218.45