Virtual/Abstract Classes and Interfaces in SFDC

In Salesforce development, the ability to build flexible, maintainable, and scalable applications relies heavily on implementing object-oriented programming principles effectively.

Two critical concepts that facilitate this are virtual/abstract classes and interfaces.

In this blog post, we’ll explore these concepts in depth and see how they can be applied in Salesforce Apex to create robust solutions.

Virtual Classes in Salesforce

Virtual classes in Apex serve as foundational templates that other classes can extend. They allow you to define methods that subclasses can override, providing a way to implement common functionality while enabling customization.

Key Characteristics of Virtual Classes

  • Declared using the virtual keyword
  • Can contain implemented methods and properties
  • Can be instantiated directly (unlike abstract classes)
  • Can have methods marked as virtual that subclasses can override

Virtual Class Example

public virtual class PaymentProcessor {
    // Regular method that will be inherited as-is
    public Decimal calculateTax(Decimal amount) {
        return amount * 0.1; // 10% tax
    }
    
    // Virtual method that can be overridden by subclasses
    public virtual Decimal processPayment(Decimal amount) {
        // Default implementation
        System.debug('Processing standard payment of $' + amount);
        return amount;
    }
    
    // Another virtual method
    public virtual Boolean validatePayment(Decimal amount) {
        return amount > 0;
    }
}

Extending a Virtual Class

public class CreditCardProcessor extends PaymentProcessor {
    // Override the virtual method with custom implementation
    public override Decimal processPayment(Decimal amount) {
        // Custom implementation for credit card processing
        System.debug('Processing credit card payment of $' + amount);
        Decimal processingFee = amount * 0.03; // 3% fee
        return amount - processingFee;
    }
}

In this example, CreditCardProcessor inherits the calculateTax method as-is but customizes the payment processing logic with its own implementation.

Abstract Classes in Salesforce

Abstract classes take the concept of virtual classes a step further by enforcing implementation requirements on subclasses. They can’t be instantiated directly and often include abstract methods that must be implemented by any concrete subclass.

Key Characteristics of Abstract Classes

  • Declared using the abstract keyword
  • Cannot be instantiated directly
  • Can contain both implemented and abstract methods
  • Abstract methods must be implemented by subclasses

Abstract Class Example

public abstract class NotificationService {
    // Concrete method available to all subclasses
    public void logNotification(String message) {
        System.debug('Notification sent: ' + message);
        // Log to custom object, etc.
    }
    
    // Abstract method that subclasses MUST implement
    public abstract void sendNotification(String recipient, String message);
    
    // Template method pattern - defines the algorithm structure
    public void processNotification(String recipient, String message) {
        // Pre-processing
        message = formatMessage(message);
        
        // Send notification (implementation varies by subclass)
        sendNotification(recipient, message);
        
        // Post-processing
        logNotification(message);
    }
    
    // Virtual method that can be optionally overridden
    protected virtual String formatMessage(String message) {
        return '[NOTIFICATION] ' + message;
    }
}

Implementing Abstract Classes

public class EmailNotificationService extends NotificationService {
    // Must implement the abstract method
    public override void sendNotification(String recipient, String message) {
        // Email-specific implementation
        Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
        email.setToAddresses(new String[] { recipient });
        email.setSubject('Notification');
        email.setPlainTextBody(message);
        
        try {
            Messaging.sendEmail(new Messaging.SingleEmailMessage[] { email });
        } catch(Exception e) {
            System.debug('Email sending failed: ' + e.getMessage());
        }
    }
    
    // Optional override of the virtual method
    protected override String formatMessage(String message) {
        return '[EMAIL NOTIFICATION] ' + message + '\n\nThank you,\nThe Salesforce Team';
    }
}
public class SMSNotificationService extends NotificationService {
    // Must implement the abstract method
    public override void sendNotification(String recipient, String message) {
        // SMS-specific implementation
        System.debug('Sending SMS to ' + recipient + ': ' + message);
        // Integration with SMS service
    }
}

Interfaces in Salesforce

Interfaces provide the most flexible form of abstraction. They define a contract that implementing classes must fulfill but provide no implementation details. This allows for creating truly pluggable components in your Salesforce applications.

Key Characteristics of Interfaces

  • Declared using the interface keyword
  • Cannot contain implemented methods, only method signatures
  • Classes can implement multiple interfaces
  • All methods in an interface are implicitly public and abstract

Interface Example

public interface Schedulable {
    void execute(SchedulableContext sc);
}

public interface DataProcessor {
    List<SObject> processRecords(List<SObject> records);
    void handleErrors(List<Error__c> errors);
}

Implementing Multiple Interfaces

public class AccountBatchProcessor implements Database.Batchable<SObject>, Database.Stateful, DataProcessor {
    private Integer recordsProcessed = 0;
    private List<Error__c> errorLogs = new List<Error__c>();
    
    // From Batchable interface
    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator('SELECT Id, Name FROM Account WHERE CreatedDate = TODAY');
    }
    
    // From Batchable interface
    public void execute(Database.BatchableContext bc, List<Account> scope) {
        List<Account> processedRecords = (List<Account>)processRecords(scope);
        
        try {
            update processedRecords;
            recordsProcessed += processedRecords.size();
        } catch(Exception e) {
            Error__c errorLog = new Error__c(
                Process_Name__c = 'AccountBatchProcessor',
                Error_Message__c = e.getMessage(),
                Stack_Trace__c = e.getStackTraceString()
            );
            errorLogs.add(errorLog);
        }
    }
    
    // From Batchable interface
    public void finish(Database.BatchableContext bc) {
        System.debug('Total records processed: ' + recordsProcessed);
        handleErrors(errorLogs);
    }
    
    // From DataProcessor interface
    public List<SObject> processRecords(List<SObject> records) {
        List<Account> accounts = (List<Account>)records;
        for(Account acc : accounts) {
            acc.Description = 'Updated by batch on ' + System.today();
        }
        return accounts;
    }
    
    // From DataProcessor interface
    public void handleErrors(List<Error__c> errors) {
        if(!errors.isEmpty()) {
            insert errors;
        }
    }
}

Real-World Implementation: A Design Pattern Example

Let’s implement a Strategy Pattern using Salesforce Apex to demonstrate how these concepts work together in a practical scenario. We’ll create a lead scoring system that can use different algorithms based on business requirements.

Define the Interface

public interface LeadScoringStrategy {
    Integer calculateScore(Lead lead);
}

Implement Different Strategies

public class IndustryBasedScoring implements LeadScoringStrategy {
    private Map<String, Integer> industryScores = new Map<String, Integer>{
        'Technology' => 30,
        'Healthcare' => 25,
        'Financial Services' => 35,
        'Retail' => 20,
        'Manufacturing' => 15
    };
    
    public Integer calculateScore(Lead lead) {
        Integer score = industryScores.containsKey(lead.Industry) ? 
                         industryScores.get(lead.Industry) : 10;
        
        // Add points for complete information
        if(String.isNotBlank(lead.Email)) score += 10;
        if(String.isNotBlank(lead.Phone)) score += 5;
        if(lead.NumberOfEmployees != null) score += 5;
        
        return score;
    }
}

public class EngagementBasedScoring implements LeadScoringStrategy {
    public Integer calculateScore(Lead lead) {
        Integer score = 0;
        
        // Query related task activities
        List<Task> activities = [SELECT Id, Subject, ActivityDate 
                                FROM Task 
                                WHERE WhoId = :lead.Id
                                ORDER BY ActivityDate DESC];
        
        // Score based on engagement 
        score += activities.size() * 5; // 5 points per activity
        
        // Score email opens or clicks (assuming custom fields)
        score += Integer.valueOf(lead.Email_Opens__c != null ? lead.Email_Opens__c : 0) * 2;
        score += Integer.valueOf(lead.Email_Clicks__c != null ? lead.Email_Clicks__c : 0) * 3;
        
        return score;
    }
}

Abstract Service Class

public abstract class LeadScoringService {
    protected LeadScoringStrategy scoringStrategy;
    
    // Constructor injection of the strategy
    public LeadScoringService(LeadScoringStrategy strategy) {
        this.scoringStrategy = strategy;
    }
    
    // Method to change strategy at runtime
    public void setScoringStrategy(LeadScoringStrategy strategy) {
        this.scoringStrategy = strategy;
    }
    
    // Template method for lead scoring process
    public List<Lead> scoreLeads(List<Lead> leads) {
        List<Lead> scoredLeads = new List<Lead>();
        
        for(Lead lead : leads) {
            Integer score = scoringStrategy.calculateScore(lead);
            lead.Lead_Score__c = score;
            
            // Common logic regardless of scoring strategy
            lead.Lead_Score_Date__c = System.today();
            lead.Lead_Score_Status__c = getLeadStatus(score);
            
            scoredLeads.add(lead);
        }
        
        return scoredLeads;
    }
    
    // Concrete method used by template method
    protected String getLeadStatus(Integer score) {
        if(score >= 75) return 'Hot';
        else if(score >= 50) return 'Warm';
        else if(score >= 25) return 'Cool';
        else return 'Cold';
    }
    
    // Abstract method for strategy-specific validation
    public abstract Boolean validateScoring(Lead lead);
}

Concrete Service Implementation

public class EnterpriseLeadScoringService extends LeadScoringService {
    
    public EnterpriseLeadScoringService(LeadScoringStrategy strategy) {
        super(strategy);
    }
    
    public override Boolean validateScoring(Lead lead) {
        // Enterprise-specific validation logic
        if(lead.Company == null || lead.Industry == null) {
            return false;
        }
        
        // Ensure we have contact information
        if(lead.Email == null && lead.Phone == null) {
            return false;
        }
        
        return true;
    }
    
    // Override the template method to add enterprise-specific behavior
    public List<Lead> scoreEnterpriseLeads(List<Lead> leads) {
        List<Lead> validLeads = new List<Lead>();
        
        for(Lead lead : leads) {
            if(validateScoring(lead)) {
                validLeads.add(lead);
            }
        }
        
        // Call the parent implementation to score valid leads
        return super.scoreLeads(validLeads);
    }
}

Usage Example

public class LeadScoringBatch implements Database.Batchable<SObject> {
    private LeadScoringService scoringService;
    
    public LeadScoringBatch() {
        // Select scoring strategy based on custom setting or metadata
        LeadScoringStrategy strategy;
        
        String strategyType = LeadScoringSettings__c.getInstance().Strategy_Type__c;
        
        if(strategyType == 'Industry') {
            strategy = new IndustryBasedScoring();
        } else {
            strategy = new EngagementBasedScoring();
        }
        
        scoringService = new EnterpriseLeadScoringService(strategy);
    }
    
    public Database.QueryLocator start(Database.BatchableContext BC) {
        return Database.getQueryLocator('SELECT Id, Name, Company, Industry, Email, Phone, ' +
                                        'NumberOfEmployees, Email_Opens__c, Email_Clicks__c ' +
                                        'FROM Lead WHERE IsConverted = false');
    }
    
    public void execute(Database.BatchableContext BC, List<Lead> scope) {
        List<Lead> scoredLeads = scoringService.scoreLeads(scope);
        update scoredLeads;
    }
    
    public void finish(Database.BatchableContext BC) {
        // Send completion notification
    }
}

Benefits and Best Practices

Benefits of Using Virtual/Abstract Classes and Interfaces

  1. Code Reuse: Common functionality can be defined once and reused across multiple classes.
  2. Flexibility: Systems can adapt to new requirements by swapping implementations.
  3. Testability: Abstract components make it easier to mock dependencies during testing.
  4. Maintainability: Changes can be made to implementations without affecting client code.
  5. Organization: Clear separation of concerns makes code easier to understand.

Best Practices

  1. Follow the Interface Segregation Principle: Create small, focused interfaces rather than large, monolithic ones.
  2. Favor Composition Over Inheritance: When possible, use interfaces to compose behavior rather than creating deep inheritance hierarchies.
  3. Use Abstract Classes for Template Methods: When you want to define an algorithm structure but allow steps to be customized.
  4. Document Your Abstractions: Clearly document the contract and expected behavior of abstract methods.
  5. Balance Abstraction with Pragmatism: Don’t over-engineer; use these patterns when they provide clear benefits.

Conclusion

In Salesforce development, virtual/abstract classes and interfaces provide powerful tools for creating flexible, maintainable applications. They allow developers to implement design patterns that promote code reuse, simplify testing, and enable systems to evolve.

What abstraction patterns have you found most useful in your Salesforce development projects? Share your experiences in the comments below!

Must Read

Amit Singh
Amit Singh

Amit Singh aka @sfdcpanther/pantherschools, a Salesforce Technical Architect, Consultant with over 8+ years of experience in Salesforce technology. 21x Certified. Blogger, Speaker, and Instructor. DevSecOps Champion

Articles: 299

Newsletter Updates

Enter your email address below and subscribe to our newsletter

Leave a Reply

Your email address will not be published. Required fields are marked *

Discover more from Panther Schools

Subscribe now to keep reading and get access to the full archive.

Continue reading