How to keep external references to abstract generic class abstract and generic?

79 Views Asked by At

I am trying to do something like the following -

public abstract class Utility<A, B>
{
    // Return values cannot be cached in class-scoped fields as these need to be invoked by threaded application
    public abstract A GetPrimaryData(float foo);
    public abstract B GetSecondaryData(float bar, A primaryData);

    public abstract float ProcessData(A primaryData, B secondaryData);
}

public class SpecificUtility : Utility<C, D>
{
    public override C GetPrimaryData(float foo) { /* implementation */ }

    public override D GetSecondaryData(float bar, C primaryData) { /* implementation */ }

    public override float ProcessData(C primaryData, D secondaryData) { /* implementation */ }
}

public class UserClass
{
    // Important - this script doesn't care what A or B is! Should be able to use ANY implementation of Utility.
    // As a result, we can't define UserClass as a generic of <A, B>
    public Utility<A, B> Utility;

    // This can be invoked in parallel for many tables concurrently
    public void UseCase(Table table)
    {
        // For performance reasons, retrieving utilPrimaryData in the inner loop is not feasible
        A utilPrimaryData = Utility.GetPrimaryData(GetFoo(table));

        var stuffUserCaresAbout = table.AsParallel().ForAll(row =>
        {
            B utilSecondaryData = Utility.GetSecondaryData(GetBar(row), utilPrimaryData);
            return Utility.ProcessData(utilPrimaryData, utilSecondaryData);
        });

        Consume(stuffUserCaresAbout);
    }
}

I was thinking of making an IUtility interface that Utility class implements, and the UserClass interacts with, but that doesn't actually address the issue because the UserClass needs to actually obtain the PrimaryData and SecondaryData that the Utility will then process into the value the User needs to work with.

The only solution I can think of would be to make GetPrimaryData and GetSecondaryData return generic Objects which would then be casted into the specific types that the Utility subclasses are expecting, but I find that very unsatisfying and suspect the performance implications would not be great either.

2

There are 2 best solutions below

5
StriplingWarrior On

You can use dynamic:

    public object Utility;
    public void UseCase()
    {
        var util = (dynamic)Utility;
        dynamic utilPrimaryData = util.GetPrimaryData(GetFoo());
        dynamic utilSecondaryData = util.GetSecondaryData(GetBar(), utilPrimaryData);

        float dataUserActuallyCaresAbout = util.ProcessData(utilPrimaryData, utilSecondaryData);
    }

Just be aware that you'll lose all type safety for these calls, turning any mistakes with method names, etc. into a runtime error. To reduce the surface area of possible mistakes, you could extract most of this to a generic method that gets dynamically invoked:

    public object Utility;

    public void UseCase()
    {
        float dataUserActuallyCaresAbout = GetDataUserCaresAbout((dynamic)Utility, GetFoo(), GetBar());
    }
    private static float GetDataUserCaresAbout<A, B>(Utility<A, B> utility, float foo, float bar)
    {
        var utilPrimaryData = utility.GetPrimaryData(foo);
        var utilSecondaryData = utility.GetSecondaryData(bar, utilPrimaryData);

        return utility.ProcessData(utilPrimaryData, utilSecondaryData);
    }

You may want to consider moving this code into a context that does care about the generic types, though. For example, this code has a Feature Envy smell that might be addressed by giving Utility a method that handles these details with a non-generic signature:

public interface IUtility
{
    float GetDataUserCaresAbout(float foo, float bar);
}

public abstract class Utility<A, B> : IUtility
{
    public abstract A GetPrimaryData(float foo);
    public abstract B GetSecondaryData(float bar, A primaryData);

    public abstract float ProcessData(A primaryData, B secondaryData);

    public float GetDataUserCaresAbout(float foo, float bar)
    {
        var utilPrimaryData = GetPrimaryData(foo);
        var utilSecondaryData = GetSecondaryData(bar, utilPrimaryData);

        return ProcessData(utilPrimaryData, utilSecondaryData);
    }
}
    public IUtility Utility;

    public void UseCase()
    {
        float dataUserActuallyCaresAbout = Utility.GetDataUserCaresAbout(GetFoo(), GetBar());
        Console.WriteLine(dataUserActuallyCaresAbout);
    }
1
Riad Baghbanli On

You will need a controller interface and a controller generic class to implement abstract invocation of generic methods. Consider the following code:

    public abstract class Utility<A, B>
{
    public abstract A GetPrimaryData(float foo);
    public abstract B GetSecondaryData(float bar, A primaryData);
    public abstract float ProcessData(A primaryData, B secondaryData);
}

public class SpecificUtility<C, D> : Utility<C, D>
{
    public override C GetPrimaryData(float foo) { throw new NotImplementedException(); }
    public override D GetSecondaryData(float bar, C primaryData) { throw new NotImplementedException(); }
    public override float ProcessData(C primaryData, D secondaryData) { throw new NotImplementedException(); }
}

public interface IUtilityController
{
    float UseCase(float a, float b);
}

public record class UtilityController<A, B>(Utility<A, B> utility) : IUtilityController
{
    public float UseCase(float a, float b)
    { // code that deals with A and B specific properties and methods
        A utilPrimaryData = utility.GetPrimaryData(a);
        B utilSecondaryData = utility.GetSecondaryData(b, utilPrimaryData);
        return utility.ProcessData(utilPrimaryData, utilSecondaryData);
    }
}

public class UserClass
{
    IUtilityController? _controller;
    public void SetController(IUtilityController c)
    {
        _controller = c;
    }
    
    public float? UseCase(float a, float b)
    { // code that deals with actual invocation of generic object invocation
        return _controller != null ? _controller.UseCase(a, b) : null;
    }       
}