Managed package code can be written to allow subscribers to plug in their own implementation of a global interface. Typically this is done with the managed package providing a custom setting that allows the subscriber to specify a concrete implementation of an interface. The managed package code then creates an instance using the Type.forName method. Once a global interface is included in a managed package (i.e., published) it can no longer have methods added to it in subsequent versions, yet developers of the managed package may strongly desire to do something to that effect. This article explores a pattern that can be used by the managed package code to decouple it from the evolving plugin APIs. A simple example that uses a PaymentProcessor interface is used to illustrate the main points.
On the initial release there is a single interface, PaymentProcessor, with a single method. Subscribers can implement and specify their concrete implementation in a custom setting, PaymentProcessor__c.Service__c.
The global interface
1 2 3 |
global interface PaymentProcessor { PaymentResponse makePayment(PaymentRequest request); } |
It may be used in the managed package as follows. (The details of the Type.forName are hidden in the factory.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public with sharing class PaymentController { private final PaymentProcessor pmtProcessor; public PaymentController() { this(PaymentProcessorFactory.getProcessor()) } public PaymentController(PaymentProcessor pp) { this.pmtProcessor = pp; } public PageReference makePayment() { PaymentRequest req = getPaymentRequest(); PaymentResponse response = pmtProcesor.makePayment(req); return handleResponse(response); } // other methods in controller follow... } |
Once installed in the subscriber org, the subscriber may create their own implementation.
1 2 3 4 5 6 |
// NS__ is the namespace prefix public class PayPalPaymentProcessor implements NS__PaymentProcessor { public PaymentResponse makePayment(PaymentRequest request) { // do some Pay Pal stuff } } |
Everything is fine, until in the next release requires refunds, cancellations, and authorizations to be implemented. One possibility is to create a new plugin custom setting and use that in addition to the existing custom setting. Another possibility is to keep the one and only custom setting and allow the subscriber to update their concrete implementation to implement the new interface. That latter is explored more in detail below, but the code could be modified to accommodate the former.
A new global interface
1 2 3 4 5 |
global interface PaymentProcessor2 extends PaymentProcessor { PaymentResponse refund(PaymentRequest request); PaymentResponse cancel(PaymentRequest request); PaymentResponse authorize(PaymentRequest request); } |
Here the new interface just extends the previous one, but it doesn’t have to be done that way. It could be separate and left up to the implementing classes to implement both interfaces. One benefit of doing it this way is to make it clear to all new subscribers that they should be implementing the PaymentProcessor2 interface.
A new class is introduced to act as a middle-man between the controller and the plugin. This is an implementation of the object adapter pattern.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class PaymentProcessorAdapter { private final PaymentProcessor proc; private final PaymentProcessor2 proc2; public PaymentProcessorAdapter(PaymentProcessor p, PaymentProcessor2 p2) { proc = p; proc2 = p2; } public PaymentResponse makePayment(PaymentRequest request) { return proc.makePayment(request); } public PaymentResponse refund(PaymentRequest request) { return proc2.refund(request); // note proc2 } // similar methods for cancel and authorize and possibly more methods } |
The factory is then updated to return a PaymentProcessorAdapter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class PaymentProcessorFactory { public static PaymentProcessorAdapter getAdapter() { PaymentProcessor proc = getPaymentProcessorFromCustomSetting(); PaymentProcessor proc2 = null; if (proc instanceof PaymentProcessor2) { proc = proc2; } else { // might change proc2 = new DefaultPaymentProcessor2(); } PaymentProcessorAdapter adapter = new PaymentProcessorAdapter(proc, proc2); return adapter; } // method to get from custom setting... } |
The DefaultPaymentProcessor2 could be any number of things. It could be a Null object pattern implementation. It could implement real behavior, if the processing it did made sense universally. It could throw UnsupportedOperationExceptions. The proc2 could be set to null, and the Adapter could handle that situation, even. It depends on whatever makes the most sense for the plugin.
The controller would change to reference the adapter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public with sharing class PaymentController { private final PaymentProcessorAdapter pmtProcAdapter; public PaymentController() { this(PaymentProcessorFactory.getAdapter()); } public PaymentController(PaymentProcessorAdapter p) { this.pmtProcAdapter = p; } public PageReference makePayment() { PaymentRequest request = getPaymentRequest(); PaymentResponse response = pmtProcAdapter.makePayment(request); return handleResponse(response); } // more controller methods follow... } |
The subscriber implementation would need to change to implement the new interface.
1 2 3 4 5 6 7 8 9 10 |
public class PayPalPaymentProcessor implements NS__PaymentProcessor2 { public NS__PaymentResponse makePayment(NS__PaymentRequest request) { // do some Pay Pal stuff } public NS__RefundResponse refund(NS__PaymentRequest request) { // do some Pay Pal stuff } // similar methods for cancel and authorize } |
Additional Benefits
In the above example the adapter class implements its own method for each plugin method (i.e., a one-to-one). It does not have to, though. The methods on the adapter could be completely different and tailored to how they would make sense in the internal / managed package code. Other methods could be added to the adapter to give the calling code the ability to test if certain functionality is available.
Alternative – Add to Parameters and Return Types
In the adapter example the makePayment method took a single PaymentRequest object and returned a single PaymentResponse object. Depending on the similarity of the functions, it could be a better solution to add properties to the parameter and return objects (e.g., a transactionType to denote payment, refund, cancel, or authorize). This approach has the benefit of not requiring another interface, but makes it easier to not implement the new methods.
1 2 3 4 5 6 7 8 9 |
public class PayPalPaymentProcessor implements NS__PaymentProcessor { public NS__PaymentResponse makePayment(NS__PaymentRequest request) { if (request.transactionType == ‘payment’) { return processPayment(request); } else if (request.transactionType == ‘refund’) { return processRefund(request); } // etc… } } |
Although the method signature wasn’t changed, the preconditions and postconditions did change to be more complex. Be careful about not breaking calling code or implementing code. Refer to this table on general rules for contract compatibility (via Eclipse).
Method preconditions | Strengthen | Breaks compatibility for callers | Contract compatible for implementors |
Weaken | Contract compatible for callers | Breaks compatibility for implementors | |
Method postconditions | Strengthen | Contract compatible for callers | Breaks compatibility for implementors |
Weaken | Breaks compatibility for callers | Contract compatible for implementors | |
Field invariants | Strengthen | Contract compatible for getters | Breaks compatibility for setters |
Weaken | Breaks compatibility for getters | Contract compatible for setters |
Conclusion
This article described a way to use the object adapter pattern with plugin classes to facilitate changing plugin APIs. The pattern is general and pieces of it can be easily varied, depending on the needs of the developer and/or codebase.