Monday, March 4, 2024

Salesforce Apex: Observer Pattern

The Observer Pattern is a behavioral design pattern where an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods. This pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. This is particularly useful for creating a publish-subscribe mechanism to enable loose coupling between the system components.

In the context of CPQ, the Observer Pattern can be useful in scenarios where changes to certain objects (like a Quote or a Product) need to trigger updates in related objects or execute certain business logic (such as recalculating prices or applying discounts).

Here is an example illustrating the Observer Pattern in Salesforce Apex:

Step 1: Create an observer interface.

public interface IQuoteObserver {
    void update(Quote quote);
}

Step 2: Create the subject class that observers can subscribe to.

public class Quote {
    private List<IQuoteObserver> observers = new List<IQuoteObserver>();
    private Decimal totalPrice;

    // Constructor
    public Quote(Decimal totalPrice) {
        this.totalPrice = totalPrice;
    }

    // Method to subscribe observers
    public void subscribe(IQuoteObserver observer) {
        observers.add(observer);
    }

    // Method to unsubscribe observers
    public void unsubscribe(IQuoteObserver observer) {
        observers.remove(observer);
    }

    // Method to notify observers of changes
    public void notifyObservers() {
        for (IQuoteObserver observer : observers) {
            observer.update(this);
        }
    }

    // Method to change the price and notify observers
    public void setTotalPrice(Decimal newPrice) {
        this.totalPrice = newPrice;
        notifyObservers();
    }

    // Getter for totalPrice
    public Decimal getTotalPrice() {
        return totalPrice;
    }
}

Step 3: Create concrete observers that react to changes.

public class DiscountApplier implements IQuoteObserver {
    private Decimal discountRate;

    public DiscountApplier(Decimal discountRate) {
        this.discountRate = discountRate;
    }

    public void update(Quote quote) {
        Decimal newPrice = quote.getTotalPrice() * (1 - discountRate);
        System.debug('Applying discount: New Price is ' + newPrice);
        // Discount logic can be applied here, and the quote can be updated accordingly
    }
}

public class TaxCalculator implements IQuoteObserver {
    private Decimal taxRate;

    public TaxCalculator(Decimal taxRate) {
        this.taxRate = taxRate;
    }

    public void update(Quote quote) {
        Decimal taxAmount = quote.getTotalPrice() * taxRate;
        System.debug('Calculating tax: Total Tax is ' + taxAmount);
        // Tax calculation logic can be applied here, and the quote can be updated accordingly
    }
}

Step 4: Use the Quote class and attach observers.

Quote quote = new Quote(1000.00);

// Create observers
DiscountApplier discountApplier = new DiscountApplier(0.1); // 10% discount
TaxCalculator taxCalculator = new TaxCalculator(0.15); // 15% tax

// Subscribe observers to the quote
quote.subscribe(discountApplier);
quote.subscribe(taxCalculator);

// Change the quote's price and notify observers
quote.setTotalPrice(1200.00); // This will automatically notify the observers

In the example above, we have a Quote class, which is the subject that maintains a list of observers. The DiscountApplier and TaxCalculator are observers that implement the IQuoteObserver interface. When the setTotalPrice method on the Quote is called, it triggers the notifyObservers method, which in turn calls the update method on each observer, allowing them to react to the price change.

The Observer Pattern is widely used in Salesforce CPQ for keeping various parts of the application in sync. It helps in situations where you have multiple pieces of logic that need to respond to changes in the data model, facilitating a clean separation of concerns.

2nd Example

In Salesforce CPQ, a common use case that can benefit from the Observer pattern is when multiple aspects of a Quote need to be recalculated or updated based on changes to Quote Line Items. For example, you might need to update the Quote's total price, apply discounts, and recalculate taxes when a Quote Line Item is modified.

Let's create an example with multiple observers that handle different updates: one for recalculating the total price, another for applying discounts, and a third one for recalculating taxes.

Firstly, define the Observer interface:

public interface QuoteObserver {
    void calculateUpdates(List<Id> quoteIds);
}

Create a Subject class that Quote Line Items can notify:

public class QuoteLineItemSubject {
    private static List<QuoteObserver> observers = new List<QuoteObserver>();

    public static void attach(QuoteObserver observer) {
        observers.add(observer);
    }

    public static void notifyObservers(List<Id> quoteIds) {
        for (QuoteObserver observer : observers) {
            observer.calculateUpdates(quoteIds);
        }
    }
}

Now, implement different Observers for each aspect of the Quote that needs to be updated:

public class QuoteTotalPriceCalculator implements QuoteObserver {
    public void calculateUpdates(List<Id> quoteIds) {
        // Calculate total price for each Quote and bulkify the update
        // Logic to calculate and update total price
    }
}

public class QuoteDiscountHandler implements QuoteObserver {
    public void calculateUpdates(List<Id> quoteIds) {
        // Apply discounts to each Quote and bulkify the update
        // Logic to calculate and update discounts
    }
}

public class QuoteTaxCalculator implements QuoteObserver {
    public void calculateUpdates(List<Id> quoteIds) {
        // Recalculate taxes for each Quote and bulkify the update
        // Logic to calculate and update taxes
    }
}

In a trigger on the Quote Line Item object, attach the observers and trigger the notifications:

trigger QuoteLineItemTrigger on QuoteLineItem (after insert, after update, after delete, after undelete) {
    // Collect the affected Quote IDs
    Set<Id> quoteIds = new Set<Id>();
    for (QuoteLineItem item : Trigger.isDelete ? Trigger.old : Trigger.new) {
        quoteIds.add(item.QuoteId);
    }

    // Attach the observers
    QuoteLineItemSubject.attach(new QuoteTotalPriceCalculator());
    QuoteLineItemSubject.attach(new QuoteDiscountHandler());
    QuoteLineItemSubject.attach(new QuoteTaxCalculator());
    
    // Notify observers with the updated quote IDs
    QuoteLineItemSubject.notifyObservers(new List<Id>(quoteIds));
}

Each observer's calculateUpdates method should handle bulk operations and should be implemented to query the necessary Quote and Quote Line Item records, perform the calculations, and update the Quotes in a bulkified manner.

Here is an example for the QuoteTotalPriceCalculator:

public class QuoteTotalPriceCalculator implements QuoteObserver {
    public void update(List<Id> quoteIds) {
        // Aggregate the total prices from Quote Line Items
        Map<Id, Decimal> quoteTotals = new Map<Id, Decimal>();
        for (AggregateResult ar : [
            SELECT QuoteId, SUM(TotalPrice) total
            FROM QuoteLineItem
            WHERE QuoteId IN :quoteIds
            GROUP BY QuoteId
        ]) {
            Id quoteId = (Id)ar.get('QuoteId');
            Decimal total = (Decimal)ar.get('total');
            quoteTotals.put(quoteId, total);
        }

        // Update the Quotes with the new totals
        List<Quote> quotesToUpdate = new List<Quote>();
        for (Id quoteId : quoteTotals.keySet()) {
            Quote quote = new Quote(Id = quoteId, TotalPrice = quoteTotals.get(quoteId));
            quotesToUpdate.add(quote);
        }

        // Perform a bulk update
        update quotesToUpdate;
    }
}

This approach ensures that whenever a Quote Line Item triggers an update, all related aspects of the Quote are automatically and efficiently updated through their respective observers. The pattern allows for easy extension, as you can add more observers to handle additional functionalities without modifying existing code. This makes the system more maintainable and scalable.

Aggregating the changes from all observers in a centralized manner

To ensure DML operations are only performed in one place, you can aggregate the changes from all observers in a centralized manner and perform the update once all observers have contributed their modifications. This can be accomplished by collecting the results of each observer's calculations and then applying those changes in bulk after all observers have processed.

Here's how you can modify the code:

First, modify the QuoteObserver interface to return a Map<Id, Quote> which contains the Quote Id and the modified Quote record.

public interface QuoteObserver {
    Map<Id, Quote> calculateUpdates(List<Id> quoteIds);
}

Each observer will now implement the calculateUpdates method to return the map of changes without performing any DML operations:

public class QuoteTotalPriceCalculator implements QuoteObserver {
    public Map<Id, Quote> calculateUpdates(List<Id> quoteIds) {
        // Logic to calculate total prices
        // Return a map of Quote records with updated total prices
    }
}

// Implement similar logic for QuoteDiscountHandler and QuoteTaxCalculator

In the Subject class, modify the notifyObservers method to collect all changes:

public class QuoteLineItemSubject {
    private static List<QuoteObserver> observers = new List<QuoteObserver>();

    // ...

    public static Map<Id, Quote> notifyObservers(List<Id> quoteIds) {
        Map<Id, Quote> quotesToUpdate = new Map<Id, Quote>();

        for (QuoteObserver observer : observers) {
            Map<Id, Quote> observerUpdates = observer.calculateUpdates(quoteIds);
            for (Id quoteId : observerUpdates.keySet()) {
                Quote updatedQuote = observerUpdates.get(quoteId);
                if (quotesToUpdate.containsKey(quoteId)) {
                    // Merge changes with existing updates, ensuring no overwrites
                    // This could include summing totals, recalculating taxes, etc.
                    // You'll define the merge logic based on your business requirements
                } else {
                    quotesToUpdate.put(quoteId, updatedQuote);
                }
            }
        }

        return quotesToUpdate;
    }
}

Now, the trigger will call notifyObservers and then perform the DML operation with the collected changes:

trigger QuoteLineItemTrigger on QuoteLineItem (after insert, after update, after delete, after undelete) {
    // Collect the affected Quote IDs and attach observers
    Set<Id> quoteIds = new Set<Id>();
    for (QuoteLineItem item : Trigger.isDelete ? Trigger.old : Trigger.new) {
        quoteIds.add(item.QuoteId);
    }

    QuoteLineItemSubject.attach(new QuoteTotalPriceCalculator());
    QuoteLineItemSubject.attach(new QuoteDiscountHandler());
    QuoteLineItemSubject.attach(new QuoteTaxCalculator());

    // Notify observers and collect updates
    Map<Id, Quote> quotesToUpdate = QuoteLineItemSubject.notifyObservers(new List<Id>(quoteIds));

    // Perform a bulk update with the collected changes
    if (!quotesToUpdate.isEmpty()) {
        update quotesToUpdate.values();
    }
}

With this approach, each observer calculates the necessary updates and returns them. The Subject class aggregates these updates, ensuring that there are no conflicts or overwrites. Finally, the trigger performs a single DML operation with all the aggregated changes. This pattern keeps DML operations consolidated in one place, making the code cleaner, easier to maintain, and more efficient.

Share This:    Facebook Twitter

0 comments:

Post a Comment

Total Pageviews

My Social Profiles

View Sonal's profile on LinkedIn

Tags

__proto__ $Browser Access Grants Accessor properties Admin Ajax AllowsCallouts Apex Apex Map Apex Sharing AssignmentRuleHeader AsyncApexJob Asynchronous Auth Provider AWS Callbacks Connected app constructor Cookie CPU Time CSP Trusted Sites CSS Custom settings CustomLabels Data properties Database.Batchable Database.BatchableContext Database.query Describe Result Destructuring Dynamic Apex Dynamic SOQL Einstein Analytics enqueueJob Enterprise Territory Management Enumeration escapeSingleQuotes featured Flows geolocation getGlobalDescribe getOrgDefaults() getPicklistValues getRecordTypeId() getRecordTypeInfosByName() getURLParameters Google Maps Governor Limits hasOwnProperty() Heap Heap Size IIFE Immediately Invoked Function Expression Interview questions isCustom() Javascript Javascript Array jsForce Lightning Lightning Components Lightning Events lightning-record-edit-form lightning:combobox lightning:icon lightning:input lightning:select LockerService Lookup LWC Manual Sharing Map Modal Module Pattern Named Credentials NodeJS OAuth Object.freeze() Object.keys() Object.preventExtensions() Object.seal() Organization Wide Defaults Override PDF Reader Performance performance.now() Permission Sets Picklist Platform events Popup Postman Primitive Types Profiles Promise propertyIsEnumerable() prototype Query Selectivity Queueable Record types Reference Types Regex Regular Expressions Relationships Rest API Rest Operator Revealing Module Pattern Role Hierarchy Salesforce Salesforce Security Schema.DescribeFieldResult Schema.DescribeSObjectResult Schema.PicklistEntry Schema.SObjectField Schema.SObjectType Security Service Components Shadow DOM Sharing Sharing Rules Singleton Slots SOAP API SOAP Web Services SOQL SOQL injection Spread Operator Star Rating stripInaccessible svg svgIcon Synchronous this Token Triggers uiObjectInfoApi Upload Files VSCode Web Services XHR
Scroll To Top