2

As I implied in a recent question, I know just enough Apex to be dangerous when trying to implement more complex concepts. (And by "complex" I mean "complex to me" because so many here are so complex...

Dynamically Determine Calling Context?

Design Patterns - Singleton applied to Apex w/o describe call limits

...that it is hard for me to comprehend some things...yet.)

I also have limited knowledge/understanding of various patterns (e.g., the Singleton discussed in the question linked above). (Patterns are actually my intended next focus on Trailhead.) So I have tried to look over some things, including (but not limited to):

Best practice with big classes Apex

https://www.jamessimone.net/blog/joys-of-apex/organizing-invocable-and-static-code/ (I did not follow most of this one, tbh.)

Sometimes I see mention of having classes that are too big; other times mention of having too many classes. I do know enough and have enough life experience to know that the pendulum can swing too far one way at times, and that often you must delve into the fuzzy logic between each end in order to determine what is good/better/best/correct in any given circumstance.

(My main takeaway from a semester of an undergraduate senior-level Intro to Operating Systems course a few years ago was: "It depends.")


What I am (thinking that I) want to do is have one utility class that houses a number of invocable methods which can be called from Flows.

Reasoning

If we have a handful of useful utility methods which we want accessible to Flows, we have to create an equal number of classes each with an InvocableMethod. My gut tells me that this would be more frustrating to maintain.

Having separate classes would make it easier to find the method you want in the search box in the Apex Action Flow element (as long as your class was named well and your method label was descriptive). But if wanting to call something from a non-Flow context, having separate classes might make it harder to determine whether you already have a utility method to do what you need.

So it makes sense to me to try to implement what I'm thinking, but please tell me how I am mistaken.


I can't just create a class full of invocable methods, since only one InvocableMethod is allowed per class. So I would need to go a different route.

I don't think that I can use a Callable interface because A) that requires (String, Map<String, Object>) input parameters and invocable methods can only take at most one input parameter, and B) I don't believe we can yet create/use an actual Map in a Flow.

So I have essentially created my own version of a Callable implementation. Here is a proof of concept which is working so far:

public class UtilitiesInvocable {
    public class Requests {
        @InvocableVariable(label='Method Name' required=true)
        public String methodName;

        @InvocableVariable(label='Input String')
        public String inputString;

        @InvocableVariable(label='Delimiter')
        public String delimiter;
    }

    public class Results {
        @InvocableVariable
        public String outputString;

        @InvocableVariable
        public List<String> outputStringList;
    }

    @InvocableMethod(label='Methods: getOrgBaseUrl, splitString')
    public static List<Results> methodParser(List<Requests> requestList) {
        List<Results> responseWrapper = new List<Results>();
        for (Requests req : requestList) {
            Results response = new Results();
            switch on req.methodName {
                when 'getOrgBaseUrl' {
                    response.outputString = getOrgBaseUrl();
                }
                when 'splitString' {
                    response.outputStringList = splitString(req.delimiter, req.inputString);
                }
                when else {
                    response.outputString = 'Method not found: ' + req.methodName;
                }
            }
            responseWrapper.add(response);
        }
        return responseWrapper;
    }

    public static String getOrgBaseUrl() {
        String urlString = System.Url.getOrgDomainUrl().toExternalForm();
        return urlString;
    }

    public static List<String> splitString(String delimiter, String stringToSplit) {
        List<String> stringList = stringToSplit.split(delimiter);
        return stringList;
    }
}

At first I tested with, and intended for, the individual methods to all return Results...

    public static Results getOrgBaseUrl () {
        Results response = new Results();
        response.outputString = System.Url.getOrgDomainUrl().toExternalForm();
        return response;
    }

...figuring that would be more uniform and easy to deal with within the methodParser switch statement. It worked, but after further thought, doing that would make calling these methods directly from a non-Flow context much more cumbersome because a UtilitiesInvocable.Results object would have to be instantiated before calling the method.

Further Thoughts, Considerations, Comments

  • If the only input that is ever "allowed" to be required is the methodName, then the class should easily be expanded as needed without breaking existing usage.

  • If the number of inputs grows very large due to having multiple methods and/or complex methods that have a large number of parameters, it could get cumbersome or confusing in a Flow because the Apex Action element lists EVERY available input variable.

    If the variables were named generically enough in the class, some could be reused among methods, but it could still get "busy" and their genericity might add to the confusion.

    This class would definitely need to be very well documented - especially for Admins or other users who don't hang out in the developer world.

  • In our use case, I suspect we would have at most 5 to 10 small utility methods inside of this class.

  • If the available methods are listed in the label of the invocable method annotation, they are searchable/visible in the search box of the Apex Action element:

    enter image description here

    If the number of methods increases greatly, it is not pretty, but it still functions at least out to this many characters:

    enter image description here


Additional question if you think that this worth attempting to implement

  • Instead of returning an outputString that states the requested method was not found, would it be better so something else (e.g., throw a custom exception)?
4
  • I'd have 1 class per method and delegate to library classes to do the work (i.e., make the invocable classes facades onto methods in one or more utility worker classes)
    – cropredy
    Nov 23 '21 at 17:23
  • I'm not sure that I'm fully following you - sorry. ("I know just enough to be dangerous.") Using my example code: Are you saying: Have a specific getOrgUrl invocable method inside its own class, but just "redirect" the call from the Flow to a different non-invocable utility class which includes the "real" getOrgUrl method, which returns results to the invocable, which returns to the Flow? Then similarly have a separate class + splitString invocable method that calls the "real" splitString method in the utility class?
    – Moonpie
    Nov 23 '21 at 17:39
  • 1
    I guess what I'm saying is that you are more likely to have dozens of useful methods that your Apex code base uses that you can group into a few classes (e.g. CollectionUtil, UrlUtil, ..) but only a handful of these methods are actually useful for flows and those have separate invocable classes, 1/method. Invocable methods should be lightweight and delegate work to service layers that are agnostic how they are called. Also, exploit the libraries in unofficalsf.com
    – cropredy
    Nov 23 '21 at 17:45
  • Classes that are full of utility methods are an anti-pattern. You should keep classes focused on specific roles and responsibilities. If there are "utility methods" that can be shared, group them into focused classes.
    – Phil W
    Nov 23 '21 at 21:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.