Dynamically calling methods in Apex
The heart of the question is how to avoid having a dependency on the managed package - needing it to always be installed - yet still be able to invoke methods when it is installed. Unfortunately Apex lacks the language features (for example Java's reflection) that allow that.
While the Type class allows instances of a class to be created using just the string name of the class, to invoke any methods a cast to an explicit (compile-time know) interface or class is needed which gets you right back into having the dependency. (Using Type allows the extension of managed package code but does not break the dependency on that managed package code.)
If the subscriber org is not managed code then you will have to add/remove classes that depend on the managed package depending on whether it is present or not. You can insulate most of your code from that by using e.g. an interface and substituting the implementation: either have one that depends on the managed package or have one that defaults the behaviour or fails if called.
If the subscribing code is also a managed package then things get harder and introducing a third piece of code is necessary. See this Breaking managed package dependencies blog post for more explanation of that. It is so painful that it would only be worth doing for a small number of major features (unless tools were created to generate the code).
PS
Calling the tooling API from Apex is possible. But it is a web service callout, a separate transaction, requires parameters to be passed in the Apex code string, doesn't allow results to be directly returned and is presumably slow. So not the semantics of a typical method call.
With the Callable interface that was introduced in Winter '19 you can now build a light weight interface for the methods you want to dynamically call from the class. It provides that loose coupling and fall back mechanisms to cover the packaging scenario.
The example below is from the docs (tweaked to show dynamic method naming):
Example class you want to dynamically call
public class Extension implements Callable {
// Actual method
String concatStrings(String stringValue) {
return stringValue + stringValue;
}
// Actual method
Decimal multiplyNumbers(Decimal decimalValue) {
return decimalValue * decimalValue;
}
// Dispatch actual methods
public Object call(String action, Map<String, Object> args) {
switch on action {
when 'concatStrings' {
return this.concatStrings((String)args.get('stringValue'));
}
when 'multiplyNumbers' {
return this.multiplyNumbers((Decimal)args.get('decimalValue'));
}
when else {
throw new ExtensionMalformedCallException('Method not implemented');
}
}
}
public class ExtensionMalformedCallException extends Exception {}
}
Unit test demonstrating the dynamic calling
@IsTest
private with sharing class ExtensionCaller {
@IsTest
private static void givenConfiguredExtensionWhenCalledThenValidResult() {
// Given
String className = 'Extension'; // Variable to demonstrate setting class name
String methodName = 'multiplyNumbers'; // Variable to demonstrate setting method name
Decimal decimalTestValue = 10;
// When
Callable extension = (Callable) Type.forName(className).newInstance();
Decimal result = (Decimal) extension.call(methodName, new Map<String, Object> { 'decimalValue' => decimalTestValue });
// Then
System.assertEquals(100, result);
}
}