4

The gist of what I'm trying to do is get an instance of the appropriate user service, then pass it whatever subtype of User we're working with.

Models:

public class User
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Student : User
{
    public int Grade { get; set; }
}

public class Staff : User
{
    public DateTime HireDate { get; set; }
}

Services:

public interface IUserService<T> where T : User
{
    void Save(T user);
}

public class StudentService : IUserService<Student>
{
    public void Save(Student user)
    {
        // Student-specific code
    }
}

public class StaffService : IUserService<Staff>
{
    public void Save(Staff user)
    {
        // Staff-specific code
    }
}

My first try was to new up the matching service and up-cast. I found out that didn't work since it would violate type safety: e.g., the compiler wouldn't be able to tell if I were passing in a compatible user later on.

public void Process(User user)
{
    IUserService<User> service;

    if (user is Staff)
        service = (IUserService<User>)new StaffService(); // InvalidCastException
    else if (user is Student)
        service = (IUserService<User>)new StudentService(); // InvalidCastException
    else
        throw new ArgumentException("...");

    service.Save(user);
}

I understand why that's the case now, but I haven't been able to come up with an alternative that still allows for:

  1. Enforcing a contract (any user service has to implement these methods)
  2. Calling methods in a generic way (just pass in the user object)
  3. Avoiding duplicate code

What I really don't want to do is end up with something like:

if (user is Staff)
{
    var service = new StaffService();
    service.Save(user);
}
else if (user is Student)
{
    var service = new StudentService();
    service.Save(user);
}
// etc.

Not so bad with two user types and one method call, but the actual code is more complex.

  • 1
    Isn't this a need for Dependency Injection? – johnny Dec 1 '16 at 22:59
  • If you have a stable set of user subclasses, a visitor would be reasonable. You could then write an IUserService on User that stores a IUserService for each subtype and uses the visitor interface to dispatch the appropriate service. – walpen Dec 2 '16 at 5:48
  • why not void process<T> where T : User ? – Caleth Dec 2 '16 at 9:28
  • @Caleth Exactly what I ended up changing it to. Kicking myself for missing it, but a good night's rest does wonders. Could you repost as an answer? – Eric Eskildsen Dec 2 '16 at 13:53
2

You want to match the UserService subtype to the User subtype, which is fairly simple to do with (the same) type parameter you use in IUserService.

public void Process<T>(T user) where T : User
{
    IUserService<T> service;

    if (user is Staff)
        service = new StaffService(); // No more cast
    else if (user is Student)
        service = new StudentService(); // No more cast
    else
        throw new ArgumentException("...");

    service.Save(user);
}

This still has the problem that Process has to know what Service class to instantiate, which we can get around slightly awkwardly like so:

public void Process<T, S>(T user) where T : User where S : IUserService<T>
{
    IUserService<T> service = new S(); // No more "if ... is ..." chain   
    service.Save(user);
}

However that will complicate the call sites of Process

| improve this answer | |
  • I don't think that first code snippet will compile. C# doesn't take into account the user is Staff when checking if it can assign a StaffService to service – Ben Aaronson Dec 2 '16 at 17:24
  • The whole point is that service is IUserService<Staff> because of the generic parameter – Caleth Dec 2 '16 at 20:06
  • This did it! Thanks. I ended up moving the instantiation into a factory and kept just the one type parameter. Works great. – Eric Eskildsen Dec 2 '16 at 21:21
  • @Caleth Yeah, but the compiler will want every branch to be valid for all possible T. It doesn't take into account that some branches will only be run for some values of T. It doesn't say "well we only try to assign StaffService if T is Staff so we only need to assign a StaffService to IUserService<Staff>". Try copy-pasting it into Visual Studio and you'll see. – Ben Aaronson Dec 2 '16 at 23:00

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.