Usage Guide and Examples - mouttaqui/AOF GitHub Wiki
This section provides practical examples of how to use the Apex Orbit Framework (AOF) to implement business logic for a specific SObject. We will use the Account
SObject as an example, building upon the AOF_AccountSelector.cls
, AOF_AccountDomain.cls
, and AccountTrigger.trigger
components that might have been provided as part of the framework examples.
To integrate a new SObject (e.g., Contact
) with AOF, you would typically create the following components:
-
Trigger:
ContactTrigger.trigger
-
Domain Class:
AOF_ContactDomain.cls
(extendingAOF_Application_Domain
) -
Selector Class:
AOF_ContactSelector.cls
(extendingAOF_Application_Selector
) -
Service Class (Optional):
AOF_ContactService.cls
(implementingAOF_Application_Service
or a more specific interface if needed for complex, cross-object logic).
Let's walk through an example for the Account
SObject.
As per the AOF pattern, the trigger itself is minimal and delegates to AOF_TriggerHandler
.
// File: /home/ubuntu/AccountTrigger.trigger
trigger AccountTrigger on Account (
before insert, after insert,
before update, after update,
before delete, after delete,
after undelete
) {
// Instantiate the generic trigger handler, passing the SObjectType and the trigger operation.
AOF_TriggerHandler handler = new AOF_TriggerHandler(Account.SObjectType, Trigger.operationType);
// Execute the handler logic
handler.run();
}
Note on AOF_TriggerHandler
Dispatch: The base AOF_TriggerHandler
provided calls its own virtual methods (e.g., this.beforeInsert()
). For this to work with AOF_AccountDomain
, the AOF_TriggerHandler
's context-specific methods (or its run
method) need to be adapted to instantiate and call the correct SObject Domain class. A common pattern is to use a factory or naming convention.
Modified AOF_TriggerHandler
(Conceptual Dispatch within virtual methods):
One way to make the AOF_TriggerHandler
truly generic and dispatch to SObject-specific domain classes is to modify its virtual context methods. For example, the beforeInsert()
method in AOF_TriggerHandler
could be modified or be part of a more sophisticated dispatch in the run
method:
// Inside AOF_TriggerHandler.cls (Conceptual modification for dispatch)
// This is a simplified example; a robust solution might use a map or custom metadata for class resolution.
protected override void beforeInsert() {
if (this.sObjectType == Account.SObjectType) {
new AOF_AccountDomain(this.newRecords, this.oldMap).onBeforeInsert();
} else if (this.sObjectType == Contact.SObjectType) {
// new AOF_ContactDomain(this.newRecords, this.oldMap).onBeforeInsert();
}
// ... other SObjects
}
// Similar overrides for afterInsert, beforeUpdate, etc.
This dynamic instantiation based on sObjectType
is crucial for the handler to correctly delegate to the specific domain logic (e.g., AOF_AccountDomain
, AOF_ContactDomain
). The AOF_TriggerHandler
provided earlier will need this type of dispatch logic added to its virtual methods or its run()
method to correctly call the SObject-specific domain classes like AOF_AccountDomain
.
This class contains Account-specific business logic.
// File: /home/ubuntu/AOF_AccountDomain.cls
/**
* @description Domain class for the Account SObject.
* Contains SObject-specific logic for Accounts, such as validation rules or complex calculations.
* Part of the Apex Orbit Framework (AOF).
*/
public with sharing class AOF_AccountDomain extends AOF_Application_Domain {
public AOF_AccountDomain(List<Account> newRecords, Map<Id, Account> oldRecordsMap) {
super(newRecords, oldRecordsMap);
}
public AOF_AccountDomain(Map<Id, Account> oldRecordsMap) {
super(oldRecordsMap, Account.SObjectType); // Explicitly pass SObjectType for delete context
}
public override void onBeforeInsert() {
List<Account> accountsToProcess = (List<Account>) this.records;
for (Account acc : accountsToProcess) {
// Example 1: Set a default description if it's blank
if (String.isBlank(acc.Description)) {
acc.Description = Account.SObjectType.getDescribe().getLabel() + " created via AOF Framework";
}
// Example 2: Validation - Ensure all new accounts have a Name
if (String.isBlank(acc.Name)) {
// Using the addError utility from AOF_Application_Domain
// For field-specific error: addError(acc, Account.Name, "Account Name cannot be blank.");
// For record-level error if field token not readily available or general error:
acc.addError("Account Name cannot be blank (Record Level Error).");
}
// Example 3: Set a default value for a custom field if not provided
// Assuming Active__c is a Picklist(Yes/No) or Checkbox
// if (acc.Active__c == null) {
// acc.Active__c = "Yes"; // Or true for a checkbox
// }
}
}
public override void onAfterInsert() {
System.debug("AOF_AccountDomain: onAfterInsert called for " + this.records.size() + " accounts.");
// Example: Create a follow-up Task for each new Account using UnitOfWork
// This part requires the AOF_TriggerHandler to provide access to its UoW instance.
// For this example, we'll assume UoW is obtained via a context or passed in.
// See section 9.2.4 for a more detailed UoW example.
}
public override void onBeforeUpdate() {
List<Account> updatedAccounts = (List<Account>) this.records;
// oldMap is already available as this.oldMap from the base class constructor
for (Account updatedAcc : updatedAccounts) {
Account oldAcc = (Account)this.oldMap.get(updatedAcc.Id);
// Example 1: Prevent updates to the AccountNumber field if it already has a value and is being changed.
if (String.isNotBlank(oldAcc.AccountNumber) && updatedAcc.AccountNumber != oldAcc.AccountNumber) {
updatedAcc.AccountNumber.addError("Account Number cannot be changed once set.");
}
// Example 2: If Industry changes from X to Y, update another field.
// Using the fieldHasChanged utility from AOF_Application_Domain
if (fieldHasChanged(updatedAcc.Id, Account.Industry)) {
if (updatedAcc.Industry == "Technology" && oldAcc.Industry == "Agriculture") {
updatedAcc.Rating = "Hot"; // Example dependent field update
}
}
}
}
public override void onAfterUpdate() {
System.debug("AOF_AccountDomain: onAfterUpdate called for " + this.records.size() + " accounts.");
// Example: If SLA Expiration Date changes, log a message or call a service
for (SObject sObj : this.records) {
Account updatedAcc = (Account)sObj;
// Assuming SLAExpirationDate__c is a field on Account
// if (fieldHasChanged(updatedAcc.Id, Account.SLAExpirationDate__c)) {
// System.debug("SLA Expiration Date changed for Account: " + updatedAcc.Name + ".");
// // Potentially call: new AOF_NotificationService().notifySlaChange(updatedAcc.Id, updatedAcc.SLAExpirationDate__c);
// }
}
}
public override void onBeforeDelete() {
// oldMap is available as this.oldMap
System.debug("AOF_AccountDomain: onBeforeDelete called for " + this.oldMap.size() + " accounts.");
// Example: Prevent deletion if Account has open Opportunities.
// This requires querying Opportunities, so we'd use AOF_OpportunitySelector.
Set<Id> accountIdsToDelete = this.oldMap.keySet();
// AOF_OpportunitySelector oppSelector = new AOF_OpportunitySelector();
// List<Opportunity> openOpps = oppSelector.selectOpenOpportunitiesForAccounts(accountIdsToDelete);
// Map<Id, List<Opportunity>> openOppsByAccountId = new Map<Id, List<Opportunity>>();
// for(Opportunity opp : openOpps){
// if(!openOppsByAccountId.containsKey(opp.AccountId)){
// openOppsByAccountId.put(opp.AccountId, new List<Opportunity>());
// }
// openOppsByAccountId.get(opp.AccountId).add(opp);
// }
// for (Id accId : accountIdsToDelete) {
// if (openOppsByAccountId.containsKey(accId) && !openOppsByAccountId.get(accId).isEmpty()) {
// ((Account)this.oldMap.get(accId)).addError("Cannot delete Account with open Opportunities.");
// }
// }
// The actual query and selector instantiation would be needed here.
}
// Other methods like onAfterDelete, onUndelete can be implemented similarly.
}
This class handles all SOQL queries for Accounts.
// File: /home/ubuntu/AOF_AccountSelector.cls
/**
* @description Selector class for the Account SObject.
* Responsible for all SOQL queries related to Accounts, ensuring FLS and CRUD checks.
* Part of the Apex Orbit Framework (AOF).
*/
public with sharing class AOF_AccountSelector extends AOF_Application_Selector {
public AOF_AccountSelector() {
super(Account.SObjectType);
// Optionally, define a default set of fields to query
// this.fieldsToQuery.addAll(new List<String>{"Id", "Name", "AccountNumber", "Industry", "Type", "Description"});
// Or build dynamically based on all queryable fields:
// this.fieldsToQuery.addAll(getSObjectFields(Account.SObjectType, true));
}
/**
* @description Selects Account records by a set of Ids.
* @param ids Set of Account Ids.
* @return List of Account records.
*/
public List<Account> selectByIds(Set<Id> ids) {
if (ids == null || ids.isEmpty()) {
return new List<Account>();
}
// Build query dynamically to include fields from this.fieldsToQuery
// String soqlQuery = "SELECT " + String.join(getFieldsToQuery(Account.SObjectType), ",") + " FROM Account WHERE Id IN :ids WITH SECURITY_ENFORCED";
// return (List<Account>) Database.query(soqlQuery);
// Simplified for example, assuming fieldsToQuery is populated or using default fields
return [SELECT Id, Name, AccountNumber, Industry, Type, Description
FROM Account WHERE Id IN :ids WITH SECURITY_ENFORCED];
}
/**
* @description Selects active Account records of a specific type.
* @param accountType The type of account (e.g., "Prospect", "Customer").
* @return List of active Account records.
*/
public List<Account> selectActiveAccountsByType(String accountType) {
if (String.isBlank(accountType)) {
return new List<Account>();
}
// String soqlQuery = "SELECT " + String.join(getFieldsToQuery(Account.SObjectType), ",") + " FROM Account WHERE Type = :accountType AND Active__c = 'Yes' WITH SECURITY_ENFORCED";
// return (List<Account>) Database.query(soqlQuery);
// Assuming Active__c is a field on Account
return [SELECT Id, Name, AccountNumber, Industry, Type, Description
FROM Account WHERE Type = :accountType WITH SECURITY_ENFORCED]; // Add Active__c = 'Yes' if applicable
}
/**
* @description Selects Accounts that have no parent Account.
* @return List of parentless Account records.
*/
public List<Account> selectParentlessAccounts() {
// String soqlQuery = "SELECT " + String.join(getFieldsToQuery(Account.SObjectType), ",") + " FROM Account WHERE ParentId = null WITH SECURITY_ENFORCED";
// return (List<Account>) Database.query(soqlQuery);
return [SELECT Id, Name, ParentId FROM Account WHERE ParentId = null WITH SECURITY_ENFORCED];
}
// Add other specific query methods as needed.
}
Imagine in AOF_AccountDomain.onAfterInsert()
, you want to create a related Contact
for each new Account
that is of type "Technology Partner".
// Inside AOF_AccountDomain.cls
public override void onAfterInsert() {
List<Contact> contactsToCreate = new List<Contact>();
// The AOF_TriggerHandler creates a UoW instance. This UoW instance needs to be made available
// to the Domain class, typically by passing it into the Domain constructor or a specific method call.
// For this example, let's assume the AOF_TriggerHandler has been modified to pass its `uow`
// instance when it calls the domain methods, or the domain class has a way to access it.
// One common pattern:
// In AOF_TriggerHandler's run method or specific context methods:
// AOF_AccountDomain domain = new AOF_AccountDomain(this.newRecords, this.oldMap);
// domain.setUnitOfWork(this.uow); // Requires a setter in AOF_Application_Domain
// domain.onAfterInsert();
// For this example, we'll assume `this.uow` is accessible in the domain class after being set.
// If AOF_Application_Domain has `protected AOF_Application_UnitOfWork uow;` and a setter.
// This example assumes such a mechanism is in place.
AOF_Application_UnitOfWork uowInstance = getUnitOfWorkFromHandler(); // Placeholder for actual UoW retrieval mechanism
for (SObject sObj : this.records) {
Account newAcc = (Account)sObj;
if (newAcc.Type == 'Technology Partner') {
Contact newPartnerContact = new Contact(
LastName = newAcc.Name + ' Contact',
AccountId = newAcc.Id,
Email = 'contact@' + newAcc.Name.replaceAll('\\s+', '').toLowerCase() + '.com'
);
contactsToCreate.add(newPartnerContact);
}
}
if (!contactsToCreate.isEmpty() && uowInstance != null) {
uowInstance.registerNew(contactsToCreate);
// The UoW.commitWork() will be called by the AOF_TriggerHandler at the end of the 'after' context.
}
}
// Conceptual method to get UoW - actual implementation depends on how it's passed
private AOF_Application_UnitOfWork getUnitOfWorkFromHandler() {
// This is highly conceptual. In a real scenario:
// 1. UoW is passed to Domain constructor.
// 2. UoW is passed to each domain method (e.g., onAfterInsert(AOF_Application_UnitOfWork uow)).
// 3. Domain has a reference to the TriggerHandler and calls handler.getUnitOfWork().
// The AOF_TriggerHandler already has `public AOF_Application_UnitOfWork getUnitOfWork()`.
// The challenge is for the Domain instance to get a reference to *that specific handler's* UoW.
// Simplest is for the handler to pass it when calling domain methods.
// If the handler calls `new AOF_AccountDomain(this.newRecords, this.oldMap, this.uow).onAfterInsert();`
// then the domain constructor needs to accept and store `this.uow`.
// For now, this is a placeholder for that mechanism.
if(Trigger.handler instanceof AOF_TriggerHandler){
return ((AOF_TriggerHandler)Trigger.handler).getUnitOfWork();
}
return null;
}
Important Note on UoW Access: The example above uses a conceptual getUnitOfWorkFromHandler()
. In the current AOF design, the AOF_TriggerHandler
creates and owns the AOF_Application_UnitOfWork
instance. To use it within Domain or Service classes, this UoW instance must be passed to them (e.g., via constructor, method parameter, or a setter). The AOF_TriggerHandler
has a public AOF_Application_UnitOfWork getUnitOfWork()
method. The dispatch mechanism in the handler would need to make this UoW instance available to the domain class methods.
If an error occurs, use AOF_ErrorHandlerService
.
// Inside AOF_AccountDomain.cls or AOF_AccountService.cls
try {
// Some risky operation
Integer riskyCalculation = 10 / 0; // This will throw a MathException
} catch (Exception e) {
// Log the error using the service
List<Id> accountIds = new List<Id>();
// 'this.records' is from AOF_Application_Domain, available if in a domain method context
if (this.records != null) {
for(SObject acc : this.records) {
if(acc.Id != null) accountIds.add(acc.Id);
}
}
AOF_ErrorHandlerService.logError(e, 'AOF_AccountDomain', 'performRiskyOperation', accountIds, Account.SObjectType.getDescribe().getName(), 'Critical');
// Optionally, add a user-facing error if in a 'before' context and appropriate
// if (Trigger.isBefore) { // This check is conceptual here, context is important
// for(SObject acc : this.records) {
// acc.addError('A critical error occurred while processing your request. Please contact support.');
// }
// }
}
If you have logic that spans multiple objects or is a reusable business process, you'd use a service.
AOF_AccountService.cls
(Conceptual)
public with sharing class AOF_AccountService implements AOF_Application_Service {
private AOF_Application_UnitOfWork uow;
private AOF_AccountSelector accountSelector;
// private AOF_ContactSelector contactSelector; // If interacting with Contacts
public AOF_AccountService(AOF_Application_UnitOfWork uowInstance) {
this.uow = uowInstance;
this.accountSelector = new AOF_AccountSelector();
// this.contactSelector = new AOF_ContactSelector();
}
// Example service method
public void escalateHighValueAccounts(Set<Id> accountIds) {
List<Account> accountsToEscalate = accountSelector.selectByIds(accountIds);
List<Task> tasksToCreate = new List<Task>();
for (Account acc : accountsToEscalate) {
// Assuming AnnualRevenue and CustomerPriority__c are fields on Account
// if (acc.AnnualRevenue > 1000000) {
// acc.CustomerPriority__c = 'High'; // Mark for update
// this.uow.registerDirty(acc);
// Task escalationTask = new Task(
// Subject = 'Escalate High Value Account: ' + acc.Name,
// WhatId = acc.Id,
// OwnerId = UserInfo.getUserId(), // Assign to current user or a queue
// Status = 'Not Started',
// Priority = 'High'
// );
// tasksToCreate.add(escalationTask);
// }
}
if(!tasksToCreate.isEmpty()){
this.uow.registerNew(tasksToCreate);
}
// DML commit is handled by the caller (e.g., TriggerHandler or another service that calls commitWork)
}
}
Invoking the Service:
The AOF_TriggerHandler
or a Domain class method could instantiate and call this service. The AOF_Application_UnitOfWork
instance would typically be passed from the handler to the service.
This concludes the basic usage examples. The key is to place logic in the correct layer:
-
Triggers: Minimal, delegate to
AOF_TriggerHandler
. -
AOF_TriggerHandler
: Orchestrates, dispatches to Domain (needs UoW passing logic). - Domain Classes: SObject-specific rules, validations, calculations.
- Selector Classes: All SOQL queries.
- Service Classes: Cross-object logic, complex processes, UoW interactions.
-
AOF_Application_UnitOfWork
: Register all DML. -
AOF_ErrorHandlerService
: Log all exceptions.