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.
0 comments:
Post a Comment