Wanted: the ability to create an instance of an Apex class from the class name

Suppose you have a managed package that has an Apex class called PackageController and a Visualforce page called PackagePage. But for a particular customer the behavior of the class needs to be modified. Now this could be done using public custom setting switches (that can be set in the customer’s org that the managed package is deployed in) but that would mean that the new behavior has to be added to the managed package. Where there are many or arbitrary behavior variations this isn’t a viable approach.

One solution is to copy the source code of PackageController and PackagePage into the customer’s org and modify it for each customer. But then you no longer have an upgradeable package or any intellectual property protection or indeed a product any more.

The approach I am looking to use instead is the strategy pattern. This allows parts of the PackageController’s behavior to be plugged in. The PackageController defines one or more global interfaces and uses a factory to create instances of concrete classes that implement those interfaces and then uses those instances. That factory takes the names of the concrete classes from public custom settings and these are defaulted to the names of the standard implementation concrete classes contained in the managed package. The code would look something like this:

// Example of an interface that abstracts a piece of behavior
global interface InterestStrategy {
    Decimal calculate(Decimal price, Decimal rate, Integer days);
}
// Example of a factory method
public class Factory {
    public InterestStrategy createInterestStrategy() {
        // This creates an instance of the class named by its argument by invoking that class's no-arg constructor
        return (InterestStrategy) System.newInstance(Strategies__c.InterestStrategyClassName__c);
    }
}
// Example of a controller using a strategy
public with sharing class PackageController {
    public void calculate() {
        ...
        Decimal result = new Factory().createInterestStrategy().calculate(price, rate, days);
        ...
    }
}

Given this approach, alternate concrete class implementations of the strategy interfaces can be written in the customer’s org and configured to be used by the PackageController by setting their names in the public custom settings. This way, the new behavior goes where it should go, in the org that is using the managed package not in the managed package itself. The managed package follows the open/closed principle – it is open for extension without needing to be modified to achieve the extension.

Apex already has all the necessary mechanisms e.g. interfaces except one: the ability to create an instance of a class starting from the class name. In the above code this is represented by the non-existent method System.newInstance. The equivalent mechanism in Java is Class.forName(“ClassName”).newInstance().

I’m posting this because the lack of this feature is really hurting our ability to deliver what our customers need. I (and others) have posted the need on the IdeaExchange e.g. Allow Apex code customization of a managed package but there is no commitment from salesforce to deliver this. If you have a managed package or are thinking of creating one, I suggest that you too should push for this feature to be delivered.

PS Type.newInstance was delivered in Summer ’12; see the current Force.com documentation for more detail.

Advertisements

20 thoughts on “Wanted: the ability to create an instance of an Apex class from the class name

  1. It seems to be possible to instantiate a class by name using the JSONParser:

    public InterestStrategy createInterestStrategy() {
    JSONParser parser = JSON.createParser(‘{}’);
    Type paramType = Type.forName(Strategies__c.InterestStrategyClassName__c);
    return (InterestStrategy) parser.readValueAs(paramType);
    }

    I haven’t checked yet to see if this still works from within a managed package to construct classes not in the package, though.

    David Glick
    Groundwire Consulting

    • Unfortunately, it doesn’t seem to work from within a managed package.

      This is the error message:
      System.JSONException: Don’t know the type of the Apex object to deserialize at [Source: java.io.StringReader@f57565a; line: 1, column: 1]

      • As allowing extension of code in managed packages is central to what I need I just gave that scenario a try and that works ok. I have a global class QuestionPage in a managed package that uses the cve namespace and this test passes in an org that has the cve package installed:

        @isTest
        private class TempTest {
        
            @isTest
            static void test() {
            
                JSONParser parser = JSON.createParser('{}');
                Type paramType = Type.forName('cve.QuestionPage');
                cve.QuestionPage qp = (cve.QuestionPage) parser.readValueAs(paramType);
                
                System.assertNotEquals(null, qp);
                qp.m.put('abc', 123);
                System.assertEquals(123, qp.m.get('abc'));
            }
        }

        Any typo in the class name produces the error you got.

      • Sorry, I wasn’t quite clear enough. I had no issue creating instances of classes that are inside a managed package from outside a managed package. I was unable to create instances of classes that are outside a managed package from within a managed package.

        Maybe I’m misunderstanding your example, but it looks like there is nothing gained from it using JSONParser. Couldn’t you just do “cve.QuestionPage qp = new cve.QuestionPage();” in your test class?

        I tried something like this:

        ===== in managed package =====

        global interface IMyInterface {
            Integer GetValue();
        }
        
        global class StandardImpl implements IMyInterface {
            global Integer GetValue() { return 0; }
        }
        
        global class GetImplementation {
            global static IMyInterface GetImpl() {
                CustomSettings__c settings = CustomSettings__c.getInstance()
                if (CustomSettings__c.Implementation__c != null) {
                    return (IMyInteface)JSON.createParser('{}').readValueAs(Type.forName(settings.Implementation__c));
                }
                return new StandardImpl();
            }
        }
        

        ===== in org where managed package has been deployed =====

        global class CustomImpl implements NS.IMyInterface {
            global Integer GetValue() { return 42; }
        }
        

        Calling GetImplementation.GetImpl() from the org where the package was deployed caused the previously mentioned error. The same code worked correctly when it was copied to an unmanaged class or executed through the system log.

      • Minor typo in the above:

        CustomSettings__c settings = CustomSettings__c.getInstance()
        if (CustomSettings__c.Implementation__c != null) {
        

        should have been

        CustomSettings__c settings = CustomSettings__c.getInstance();
        if (settings.Implementation__c != null) {
        
      • Jeff, You are right my test above is bogus (and pointless). The point is to be able to create an instance of class from outside the managed package inside the managed package to support patterns like the strategy pattern. Should have paused to think…

  2. It looks like the problem was that when Type.forName(fullyQualifiedName) is called within a managed package, it looks for the the type within the managed package (unless you pass ‘OtherPackageNamespace.SomeType’).

    If you use Type.forName(String namespace, String name), and pass null for the namespace parameter, it looks for types outside of the package, and everything works!

    • Looks like some care is needed in the managed package Type.forName call to allow the extending code to be either in no namespace (source code form) or in a namespace (a different package).

      But good to know that it is possible. Thanks for taking the time to post your results back here.

  3. David Glick is on my team at Groundwire and we too wanted this for use outside of a managed package to extend virtual classes for a specific clients. The goal being an extensible managed package. I can corroborate that we ran into this same Type issue in the JSON deserializer and were able to successfully work around it with prepending the namespace prefix, just as Jeff has. We have a functional beta that we’re real world use.

    On a related note, Rich Unger, senior apex dev team member looked at this solution and didn’t tell us we were crazy. He confirmed that the constructor won’t fire, so you do have to create your own setup/initialize method that instantiates your variables. That method would need to be called from any extended classes calling into a managed package and you’d of course need to make sure you added any new variables into that method that you need in scope. You essentially need to build your own constructor. Unfortunately, you can’t call the constructor like a normal method, but that would be handy. Rich also mentioned that dynamic class instantiation is on the roadmap but no promised release date.

  4. Pingback: Simon Goodyear – Force.com Dependency Management: A First Pass

  5. I see you can use Type.getType() static method to obtain the System.Type representing the specific class and then call newInstance() on it to create an object, but probably these were introduced after this article was written?

  6. Pingback: Apex Calls Between Independent Packages | FooBarForce

  7. to achieve this I have put the instances in map as a key value pair, with values being the instance of the class. But that gave me an error. Invalid runtime conversion

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s