Monday, March 4, 2024

Dependency Injection in Salesforce Apex

In Salesforce Apex, Dependency Injection (DI) is a design pattern that allows a class to receive dependencies from an external source rather than creating them itself. This makes the class more flexible, testable, and modular.

Problem Statement

In a Salesforce implementation for a Quote-to-Cash process, you may have a scenario where you need to process payments using different payment gateways (e.g., PayPal, Stripe, or a custom gateway). Implementing the code to handle different payment gateways directly within your classes can lead to tightly coupled code, which is hard to maintain and not flexible for future extensions.

How Dependency Injection Can Solve the Issue:

Dependency Injection (DI) can be used to create more maintainable and testable code by decoupling the classes that implement business logic from the classes that implement specific functionalities, like payment processing. DI allows you to inject the specific payment gateway implementation at runtime, making the code more modular and easier to extend with new payment gateways without modifying existing code.

Here's an example of how you can implement DI in Apex to solve this problem:

Step 1: Define an Interface

First, define an interface that declares the methods all payment processors should implement.

public interface IPaymentProcessor {
    Boolean processPayment(Decimal amount, String currencyCode, Map<String, Object> paymentDetails);
}

Step 2: Implement the Interface for Each Payment Gateway

Create classes that implement this interface for different payment gateways.

public class PayPalPaymentProcessor implements IPaymentProcessor {
    public Boolean processPayment(Decimal amount, String currencyCode, Map<String, Object> paymentDetails) {
        // PayPal-specific implementation
        // ...
        return true;
    }
}

public class StripePaymentProcessor implements IPaymentProcessor {
    public Boolean processPayment(Decimal amount, String currencyCode, Map<String, Object> paymentDetails) {
        // Stripe-specific implementation
        // ...
        return true;
    }
}

Step 3: Inject the Payment Processor

Create a PaymentService class that will use the payment processor. The processor is injected through the constructor.

public class PaymentService {
    private IPaymentProcessor paymentProcessor;

    // Constructor for dependency injection
    public PaymentService(IPaymentProcessor processor) {
        this.paymentProcessor = processor;
    }

    public Boolean handlePayment(Decimal amount, String currencyCode, Map<String, Object> paymentDetails) {
        return paymentProcessor.processPayment(amount, currencyCode, paymentDetails);
    }
}

Step 4: Usage

Now, you can instantiate the PaymentService with the desired payment processor dynamically.

// Example of injecting PayPalPaymentProcessor
IPaymentProcessor payPalProcessor = new PayPalPaymentProcessor();
PaymentService paymentService = new PaymentService(payPalProcessor);
Boolean result = paymentService.handlePayment(100.00, 'USD', new Map<String, Object>{'orderId' => '12345'});

// Example of injecting StripePaymentProcessor
IPaymentProcessor stripeProcessor = new StripePaymentProcessor();
paymentService = new PaymentService(stripeProcessor);
result = paymentService.handlePayment(200.00, 'USD', new Map<String, Object>{'invoiceId' => '67890'});

Benefits of Using Dependency Injection

  1. Testability: It's easier to write unit tests by mocking the IPaymentProcessor interface.
  2. Extensibility: If a new payment gateway needs to be added, you only need to create a new class that implements the IPaymentProcessor interface without changing the existing code.
  3. Maintainability: Changing the payment logic for a specific gateway does not impact other parts of the system.
  4. Loose Coupling: The PaymentService class doesn't depend on concrete payment processor implementations, making the system more flexible and robust.

Integrate Custom Metadata Types with Dependency Injection in your Apex code

Using Custom Metadata Types in Salesforce can make the code even more dynamic by allowing administrators to configure which payment processor to use without changing the code. This approach can provide greater flexibility and control from the Salesforce setup interface.

Step 1: Create a Custom Metadata Type

Create a Custom Metadata Type called PaymentGatewaySetting with the following fields:

  1. GatewayName (Text): The name of the payment gateway (e.g., "PayPal", "Stripe").
  2. ClassName (Text): The Apex class name that implements the IPaymentProcessor interface for the corresponding gateway.

Step 2: Insert Records for Each Payment Gateway

Create records for each payment gateway within the Custom Metadata Type. For example:

  • GatewayName: "PayPal", ClassName: "PayPalPaymentProcessor"
  • GatewayName: "Stripe", ClassName: "StripePaymentProcessor"

Step 3: Fetch the Configuration and Instantiate the Processor

Modify your service class to fetch the payment processor class name from the Custom Metadata and use the Type.forName method to dynamically instantiate the processor.

public class PaymentService {
    private IPaymentProcessor paymentProcessor;

    // Constructor for dependency injection is removed

    // Method to set the payment processor dynamically based on Custom Metadata
    public void setPaymentProcessor(String gatewayName) {
        PaymentGatewaySetting__mdt setting = [
            SELECT ClassName__c
            FROM PaymentGatewaySetting__mdt
            WHERE GatewayName__c = :gatewayName
            LIMIT 1
        ];

        if (setting != null) {
            Type processorType = Type.forName(setting.ClassName__c);
            if (processorType != null) {
                this.paymentProcessor = (IPaymentProcessor)processorType.newInstance();
            }
        }
    }

    public Boolean handlePayment(Decimal amount, String currencyCode, Map<String, Object> paymentDetails) {
        if (paymentProcessor == null) {
            // Handle the error - payment processor not set
            return false;
        }
        return paymentProcessor.processPayment(amount, currencyCode, paymentDetails);
    }
}

Step 4: Usage

Now, you can set the payment processor based on the configured gateway name:

PaymentService paymentService = new PaymentService();
paymentService.setPaymentProcessor('PayPal');
Boolean result = paymentService.handlePayment(100.00, 'USD', new Map<String, Object>{'orderId' => '12345'});

In the above example, the setPaymentProcessor method dynamically selects the appropriate payment processor based on the Custom Metadata settings. This allows administrators to switch payment gateways or add new ones without deploying new Apex code.

Benefits of Combining DI with Custom Metadata:

  1. Flexibility: Payment gateways can be changed or added through Salesforce setup without modifying Apex code.
  2. Manageability: All gateway configurations are managed in one place, making it easy to view and edit settings.
  3. Scalability: As new gateways are needed, you only need to add new Custom Metadata records and implement the corresponding classes.

Combining Dependency Injection with Custom Metadata Types in this way facilitates a highly configurable and scalable solution for managing payment processors in Salesforce.

Testing PaymentService class

You can test the PaymentService class by mocking the IPaymentProcessor interface using the Stub API. The Stub API allows you to substitute method implementations with mock behavior, which is ideal for unit testing because it helps isolate the class under test from its dependencies. Here's how you can create a mock class for the IPaymentProcessor interface and use it to test the PaymentService:

Step 1: Create a Mock Class

Create a mock class that implements the StubProvider interface provided by Salesforce. This class will define the behavior of the mocked methods.

@isTest
private class MockPaymentProcessor implements System.StubProvider {
    private Boolean processPaymentReturnValue;

    public MockPaymentProcessor(Boolean returnValue) {
        this.processPaymentReturnValue = returnValue;
    }

    public Object handleMethodCall(Object stubbedObject, String stubbedMethodName, Type returnType, List<Type> parameterTypes, List<String> parameterNames, List<Object> args) {
        if (stubbedMethodName == 'processPayment' && returnType == Boolean.class) {
            return processPaymentReturnValue;
        }
        return null;
    }
}

Step 2: Write a Test Class

Now, write a test class for PaymentService. Use the Test.createStub method to create an instance of the IPaymentProcessor interface with the mock behavior.

@isTest
private class PaymentServiceTest {

    @isTest
    static void testHandlePayment() {
        // Create an instance of the mock payment processor with the desired return value (true for successful payment)
        IPaymentProcessor mockProcessor = (IPaymentProcessor)Test.createStub(IPaymentProcessor.class, new MockPaymentProcessor(true));

        // Inject the mock payment processor into the payment service
        PaymentService paymentService = new PaymentService(mockProcessor);

        // Call the method to test with some test data
        Boolean result = paymentService.handlePayment(100.00, 'USD', new Map<String, Object>{'orderId' => '12345'});

        // Assert that the payment was successful
        System.assertEquals(true, result, 'The payment should have been processed successfully.');
    }
}

In this test, we're asserting that handlePayment returns true, which is the behavior we've defined in our mock class for a successful payment processing scenario. You can also test for different scenarios by changing the return value in the MockPaymentProcessor constructor or adding more logic to the handleMethodCall method.

By mocking the IPaymentProcessor interface, we can focus on testing the behavior of the PaymentService class without needing to rely on actual implementations of the payment processor, which might have external dependencies and side effects. This allows for faster and more reliable unit tests.

Best Practices and Common Challenges implementing Dependency Injection

Best Practices

  • Use Interfaces: We defined IPaymentProcessor as an interface, which allows us to implement different payment processors without changing the dependent PaymentService class code.
  • Constructor Injection: Originally, we used constructor injection to pass the specific payment processor to PaymentService. This is a clear and direct way to handle dependencies.
  • Single Responsibility Principle: Each payment processor class, such as PayPalPaymentProcessor and StripePaymentProcessor, has a single responsibility: to process payments for its respective gateway.
  • Testability: With DI, we can easily test PaymentService by mocking the IPaymentProcessor interface, ensuring that unit tests do not rely on external systems.
  • Custom Metadata Types: By using Custom Metadata Types, we allowed for dynamic configuration of payment processors, which is a best practice for managing external configurations.
  • Documentation: Documenting how PaymentService and payment processors work together, including how to configure Custom Metadata, is crucial for maintainability.
  • Managing Dependencies: We only inject the necessary dependencies into PaymentService, avoiding unnecessary complexity.

Common Challenges

  • Limited Reflection: Apex's reflection capabilities are limited, but we used Type.forName to instantiate classes by name, which is a workaround for dynamic instantiation based on Custom Metadata.
  • Complex Configuration: As the number of payment gateways grows, managing Custom Metadata records can become complex. It's important to have a clear strategy for managing these configurations.
  • Learning Curve: Developers new to DI might need time to understand the pattern. In the PaymentService example, clear documentation and code comments can help mitigate this.
  • Over-Engineering: Adding DI where it's not necessary can overcomplicate the solution. In our case, we only introduced DI for actual needs, like varying payment gateways.
  • Testing: With DI, we must write tests for each payment processor and their interaction with PaymentService. This means more tests but also better coverage.
  • Debugging: Debugging can be more complex because the implementation details are abstracted. To mitigate this, ensure logging and error handling are in place, as they can provide insights when something goes wrong.
  • Performance Considerations: Creating new instances of payment processors could have performance impacts. In the PaymentService example, we should consider reusing processor instances if appropriate.
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