0

I'm struggling with the design of a method CallSystemX() that needs to accept a parameter of multiple Types. To get around it I created an interface called IRequest to be able to pass different types of objects into my method, but that introduced new problems since I have a builder that needs to know the 'real' underlying type to be able to do its job.

Here is a contrived example:

public interface IRequest
{
    string Key { get; set; }
    string[] Names();
}
class User : IRequest
{
    public string id { get; set; }
    public string Key { get; set; }

    public string[] Names() { return new string[] { "a", "b", "c" }; }
    //...
}
class Company : IRequest
{
    public string Adress { get; set; }
    public string Key { get; set; }

    public string[] Names() { return new string[] { "a", "b", "c" }; }
    //...
}

/// <summary>
/// This method will be called from MANY places throughout the application
/// </summary>
public List<T> CallSystemX(IRequest request) where T : class
{
    log.Debug($"processing request {request.Key}");

    // Alternative 1. Static object with code for building output.
    // Problem: BuildOutputForSystemX() does not know the underlying type
    var output = StaticBuilder.BuildOutputForSystemX(request, otherData, otherData2);

    // Alternative 2. IRequest offers a method.
    // Problem: Why should 'Company' or 'User' have code related to some other system, 
    // thus unrelated to its own class
    var output = request.BuildOutputForSystemX(otherData, otherData2);

    // Alternative 3. Injection
    // feels weird. The injected object still does not know the underlying type of 'request'
    // but still better the alternative 2.
    var output = request.BuildOutputForSystemX(StaticBuilder, otherData, otherData2);

    return CallLowLevelFunc(output);
}

Goals:

  • I would like to avoid duplicating CallSystemX() for each type
  • The Builder can not work with IRequest objects. It needs to work with the 'real' datatype. In this example: User and Company
  • It is ok to change the signature of CallSystemX. Perhaps add/change/remove parameters
  • If at all possible avoid reflection

I looked at a proxy-pattern but I am not sure how that would solve my problem.

2
  • Pass the type name to CallSystemX() as a string. May 5 at 20:14
  • And then use a Factory Method to instantiate the correct type. May 5 at 20:24
2

In the vast majority of cases, if you try to find out a specific type after polymorphically casting it to a more general type, you should not have been casting it to that general type to begin with.
That's not to say there aren't exceptions to this, but they are few and far between, and upcasting is more often (and very easily) misused as a crutch to persist a bad class design, that it will always raise a red flag during code review.

The Builder can not work with IRequest objects. It needs to work with the 'real' datatype. In this example: User and Company

The first question is why? What makes the handling of users and companies unique?
The second question then is what makes it not unique enough that you're trying to merge both approaches into one IRequest-handling method?

If the builder has separate methods for User and Company, i.e.:

public class SystemXBuilder
{
    public SystemXObject BuildOutput(Company c)
    {
        // ...
    }

    public SystemXObject BuildOutput(User u)
    {
        // ...
    }
}

Then you really should have separate methods which call these builder methods. Since there's no shared ancestry between the builder methods, trying to merge them on a higher level (i.e. in the CallSystemX(IRequest) method) is causing more problems than it solves. It will lead to either OCP violations or expensive reflection logic.

You've been very light on explaining exactly what the builder does with Company and User. If it were to only access IRequest-based properties, you could clearly just use your IRequest type in your builder. You say you can't, so I assume you're trying to access other properties as well.

The next question is if these builder-accessed properties can be interfaced, e.g:

public interface IBuildable
{
    public int BuildPriority { get; }
    public int BuildName { get; }
}

Which you can then implement on your types:

public class User : IRequest, IBuildable
{
    public string id { get; set; }

    //IRequest
    public string Key { get; set; }
    public string[] Names() { return new string[] { "a", "b", "c" }; }

    //IBuildable
    public int BuildPriority => 1;              // High prio
    public int BuildName => $"USER_{this.id}"; 
}

public class Company : IRequest, IBuildable
{
    public string Address { get; set; }

    //IRequest
    public string Key { get; set; }
    public string[] Names() { return new string[] { "a", "b", "c" }; }

    //IBuildable
    public int BuildPriority => 3;                       // Low prio
    public int BuildName => $"BUILDING_{this.Address}"; 
}

Because if so, then your builder method can just use this interface to reusably handle all of its implementations:

public class SystemXBuilder
{
    public SystemXObject Build(IBuildable obj)
    {
        int prio = obj.BuildPriority;
        string name = obj.BuildName;

        // ...
    }
}

If there is no commonality between them, then there is no reusable pattern between the two, and then what would be the benefit of trying to promote reusability by merging them in a single method?

Shared abstraction only makes sense when the two (or more) involved types actually share something. When they do, you enshrine their shared features in the shared abstraction (base class, interface), and then you can write code that works with objects of that abstracted type (base class, interface).


That being said, it still can be done. While I urge you to reconsider, you can create a mapping based on type. For now, I assume that all builder methods will return a SystemXObject.

private readonly SystemXBuilder myBuilder;

private readonly Dictionary<Type, Func<object, SystemXObject>> buildMapping = new Dictionary<Type, Func<SystemXObject>>()
{
    { typeof(User),    (obj) => myBuilder.BuildUser(obj as User) },
    { typeof(Company), (obj) => myBuilder.BuildCompany(obj as Company) }
};

public List<T> CallSystemX(IRequest request)
{
    SystemXObject result = null;

    foreach(var type in buildMapping.Keys)
    {
        if(request.GetType() == type)
        {
            result = buildMapping[type](request);
            break;
        }
    }

    if(result == null)
        throw new Exception($"No build action was defined for request of type {request.GetType().Name}!");

    return CallLowLevelFunc(output);
}

But I hope you can see how easily this gets to be very complex and how this is better just outright avoided, by redesigning your class structure to correctly promote its reusability.

6
  • Thank for taking time to answer. Yes I agree. It is code smell because the need to access the underlying type. But at the same time they do share some properties that are useful inside CallSystemX(). If I call the Builder before CallSystemX then I know the underlying type, but then I need to pass the properties that is needed as separate parameters. CallSystemX(builderOutput, names, key);. That would also lead to code duplication as each caller needs to call the builder.
    – David
    May 6 at 17:01
  • I don't see it noted in your answer but the approach you are describing is the GOF Builder pattern (not to be confused with the Josh Bloch pattern of the same name.)
    – JimmyJames
    May 6 at 17:53
  • @David I think you may not have understood the approach described here. Whatever properties are required to build the object for SystemX, (or any other target) need to be defined in the Buildable interface. Then your User or Company types implement that interface OR (my preference) return an instance of a private nested object that implements it.
    – JimmyJames
    May 6 at 17:59
  • @JimmyJames, that's my worry with this proposal: classes need to implement two completely unrelated interfaces, so these classes end up with two responsibilities. The single responsibility principle is there to set off alarm bells in such situations and I'd expect hard questions at a code review over why it's being done this way rather than having a builder handle that responsibility.
    – David Arno
    May 7 at 8:12
  • @DavidArno: If the implementation of an interface were considered a responsibility in and of itself, the entire ability to implement multiple interfaces would be moot, and classes could only implement an interface. When it's an extremely large interface, or one that brings a lot of internal logic with it, it's perfectly reasonable to either abstract this away or instead have an adapter class act as the bridge between the original object and the needed interface, but OP's question is significantly too light on what constitutes a "buildable" object to make such a judgment call out of the blue.
    – Flater
    May 7 at 8:49
1

The approach I'd take would be to employ some IoC to the builder. Whether the builder is static or injected isn't important here, so I'll just refer to it as builder. The key thing is that builder only needs to be told about types - and how to build them - at runtime. In other words, builder is not responsible for knowing about types; the system at startup is responsible for educating Builder in what it needs to do for a set of types that it is told about at startup.

So at startup, you might have something like:

builder.RegisterBuildMethodForType(typeof(User), buildMethodForUser);
builder.RegisterBuildMethodForType(typeof(Company), buildMethodForCompany);
...

So now we have the code:

public List<T> CallSystemX(IRequest request) where T : class
{
    log.Debug($"processing request {request.Key}");

    var output = builder.BuildOutputForSystemX(request, otherData, otherData2);

    return CallLowLevelFunc(output);
}

builder can loop over its registered types and when it matches request to one of those types, it invokes the build method associated with it.

1
  • Thank you! This might actually be quite nice. Will try it out!
    – David
    May 6 at 17:02

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.