Managed Package Integration without Extensions or Dependencies

Suppose I am developing a managed package and I want to build some additional packages that can integrate to it. Let’s call the main package Package A.

I want to create a separate package (Package B). When Package B is installed in an org where Package A is deployed, I’d like to build “awareness” into Pacakage A so that it knows that Package B is installed. This shouldn’t be to difficult since I can do some describes and look for the SObjects in Package B to know if it’s installed.

IMPORTANT NOTE: Package B should NOT require that Package A is installed. Package B can be used on its own without Package A.

Now the tricky (maybe impossible) part…

I’d like for Package B to expose an API for integration to Package A or any other application in the future. Ideally, Package A would be able to access the global classes to provide the integration. For example, maybe one of the triggers in Package A would call a static method from Package B like PackageBClass.doSomething();

Is there any way to do this? I thinks not. I can’t build Package B references into Package A or it would create a dependency (extension). I also can’t do the reverse for the same reason. They need to be two independent packages that can work together if need be. I looked at the new Apex Type Methods but this wouldn’t work because there would have to be a dependency between the apps for a shared Interface.

The only thing that I can think of that might work is a third package that depends on both Package A and Package B that acts like a proxy. This is less than ideal though.

Any thoughts or suggestions?

Answer

I think there’s really 3 approaches:

  1. COMMON 3RD PACKAGE: Include a common interface as part of a 3rd package that both Package A and B depend on. This way you can consistently dynamically instantiate instances of classes that implement that common interface and run “API logic” by calling methods that are common to all implementations of that interface from within either Package A or B.

  2. LEVERAGE NATIVE INTERFACES: You can also leverage built-in Apex interfaces such as Schedulable or Process.Plugin, which expose methods that all of your API classes can implement, providing a consistent API invocation structure.

  3. WILD AND CRAZY: As another possibility, here’s an insane solution that avoids the need for extensions, dependencies, or a common interface. It’s all based on storing your “API” within the constructors or properties of user-defined Apex classes. It is better expressed in code than in words:

    // Define your package 'API' in one or more classes
    
    global class MyPackageAPI {
    
    // Define /expose your API 'methods' in global subclasses
    // Here, our package 'API' defines 3 methods
    //
    // 1. insertAccount            - inserts a test account
    // 2. insertAccountWithName    - inserts an account with a given name
    //    PARAMS: accountName(String) - the name of the account to insert
    // 3. insertMultipleObjects      - inserts several objects passed as params
    //    PARAMS: account(JSON serialized Account SObject) - Account to insert
    //            contacts(JSON serialized List<Contact>) - List<Contact> to insert
    //            cases(JSON serialized List<Case>) - List<Case> to insert
    
    
    // Paradigm 1: when you don't need parameters to be passed,
    // you can execute your API methods right from a Method constructor
    global class InsertAccount() {
        // The constructor of your subclass defines the API method
        global InsertAccount(){
           // Run your logic in here.
           insert new Account(Name='ZachExecutableAccount');
        }
    }
    
    // Paradigm 2
    // when you DO need parameters to be passed in,
    // define properties on your subclass,
    // with any logic being executed as a 'side-effect' of the setters
    
    global class InsertAccountWithName() {
        // The properties of your method define the parameters and logic
        global String accountName {
           global get; global set {
               // Setters are passed an implicit value parameter
               // Which we'll use as part of our logic
               insert new Account(Name=value);
           } 
        }
    }
    
    // Paradigm 3: 
    // 
    // Pass in multiple arguments,
    // such as an account, contacts, and cases.
    global class InsertMultipleObjects {
       // Makes sure that we haven't run our logic yet
       private boolean runAlready;
       global String account {
          get; global set {
            runAlready = false; 
            account = value;
            InsertAll();
          }
       }
       global String contacts {
          get; global set {
            runAlready = false;
            contacts = value;
            InsertAll();
          }
       }
       public String cases {
          get; global set {
            runAlready = false;
            cases = value;
            InsertAll();
          }
       }
       private void InsertAll(){
         if (this.runAlready == false 
         && this.account != null 
         && this.contacts != null 
         && this.cases != null) {
            this.runAlready = true;
            // Convert our data from JSON into SObjects,
            // then insert
            System.Type accType = System.Type.forName('Account'); 
            Account a = (Account) JSON.deserialize(this.account,accType);
            insert a;
            List<Contact> people = (List<Contact>) JSON.deserialize(this.contacts,List<Contact>.class);
            for (Contact c : people) c.AccountId = a.Id;
            insert people;
            List<Case> casesList = (List<Case>) JSON.deserialize(this.cases,List<Case>.class);
            for (Case c : casesList) c.AccountId = a.Id;
            insert casesList;
        }
    }
    
      // **********************
      // UNIT TESTS
      // **********************
    
      // Exercise the InsertMultipleObjects() API method
      public static testMethod void TestInsertMultipleOBjects() {
        // Create some test data
        Account account = new Account(Name='UnitTestAccount');
        List<Contact> contacts = new List<Contact>{
        new Contact(FirstName='Johan',LastName='Strauss'),
        new Contact(FirstName='Sebastian',LastName='Bach')
        };
        List<Case> cases = new List<Case>{
        new Case(Subject='Broken Harpischord',Priority='Critical',Status='New'),
        new Case(Subject='Missing child',Priority='High',Status='New')
        };
    
        // Get a reference to our API method
        System.Type t = Type.forName('MyPackageAPI.InsertMultipleObjects');
        // "Call" our API Method, passing in some parameters
        Object o = JSON.deserialize('{'
            + '"account":'+JSON.serialize(account)+','
        + '"contacts":'+JSON.serialize(contacts)+','
        + '"cases":'+JSON.serialize(cases)+''
      +'}',t);
    
        // Verify that our API method worked
        System.assertEquals(1,[select count() from Account where Name = :account.Name]);
        System.assertEquals(2,[select count() from Contact where Account.Name = :account.Name]);
        System.assertEquals(2,[select count() from Case where Account.Name = :account.Name]);
       }
    }
    

SUMMARY

In essence, your API framework consists of two parts:

  1. API METHOD DISCOVERY – using System.Type, e.g.

    // Query for an API method
    System.Type apiMethod = System.Type.forName(namespace.apiClassName.apiMethodName);
    // Does the method exist?
    boolean apiMethodExists = apiMethod != null;
    
  2. API METHOD INVOCATION, using JSON.deserialize (if your method has parameters) or Type.newInstance (if your method does NOT need parameters):

    //
    // WITHOUT PARAMETERS
    //
    Object apiCall = apiMethod.newInstance();
    
    //
    // WITH PARAMETERS
    //
    
    // Run the method, if it exists
    Object apiCall = JSON.deserialize(
         '{"param1":"hello world","param2":"foobar"}',
         apiMethod
    );
    // Did the call succeed?
    boolean callSucceeded = apiCall != null;
    

To dynamically determine whether a given package’s ‘API’ is exposed, you can also leverage SOQL queries on the ApexClass object, possibly with a predetermined naming convention:

List<ApexClass> apis = [
   select Name, NamespacePrefix 
   from ApexClass 
   where Name = 'MyPackageAPI'
];

Attribution
Source : Link , Question Author : Kevin O’Hara , Answer Author : zachelrath

Leave a Comment