The only reason I would create a shared base class for NonDeterministicFunction is if there is logic that would/could be shared across all implementations of these non deterministic functions.
You can still use your abstract class as the base class as an option as well.
Personally, what you've got I would have implemented it as an interface (the invoke method) and use an abstract class to serve as the base class (if needed) to only host the common methods across all classes that inherit from it.
If you use your abstract class of Function to serve as base for both NonDeterministic and Deterministic functions you are exposing implementation that only concerns one side of those functions (Ex: deterministic classes would be able to see/implement nondeterministic methods (potentially) if you use the abstract Function class for both types of deterministic functions).
So i would put the NonDeterministic specific implementation into INonDeterministicFunctions interface and the rest in IDeterministicFunctions. Then any shared logic would go into the Function base class. Then any shared logic specific to NonDeterministicFunctions could go into a NonDeterministic base class.
If i understood you right, this is something like what i would refactor to
public IFunction
{
string Invoke(string[] args)
}
public abstract class BaseFunction
{
}
public abstract class NonDeterministicFunction : BaseFunction
{
}
public abstract class DeterministicFunction : BaseFunction
{
}
public abstract class DateTimeFunction : DeterministicFunction
{
private CultureInfo _culture;
protected DateTimeFunction(CultureInfo culture)
{
_culture = culture;
}
protected CultureInfo Culture => _culture;
}
public class UtcNowFunction : DateTimeFunction, IFunction
{
public string Invoke(string[] args)
{
DateTime now = DateTime.NowUtc;
string result = now.ToString(Culture);
if (args?.Length > 0)
{
string format = args[0];
result = now.ToString(format, Culture);
}
return result;
}
}
The reason for the interface is that it describes the implementation of what the class should be for whatever inherits from it. Given the implementation is specific to the class, to me it seems best served as an interface.
You may find that the NonDeterministic/Deterministic classes aren't needed and you can just make do with 1 BaseFunction class and have interfaces for the deterministic nondeterministic specific implementations.
But as im sure you know there are 10 different ways to do everything so my suggestion may not be 100% what you want and possibly over engineered.