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:
- Enforcing a contract (any user service has to implement these methods)
- Calling methods in a generic way (just pass in the user object)
- 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 Answer 1
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
-
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 aStaffService
toservice
Ben Aaronson– Ben Aaronson2016年12月02日 17:24:06 +00:00Commented Dec 2, 2016 at 17:24 -
The whole point is that service is IUserService<Staff> because of the generic parameterCaleth– Caleth2016年12月02日 20:06:23 +00:00Commented Dec 2, 2016 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– Eric Eskildsen2016年12月02日 21:21:32 +00:00Commented Dec 2, 2016 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 ofT
. It doesn't say "well we only try to assignStaffService
ifT
isStaff
so we only need to assign aStaffService
toIUserService<Staff>
". Try copy-pasting it into Visual Studio and you'll see.Ben Aaronson– Ben Aaronson2016年12月02日 23:00:21 +00:00Commented Dec 2, 2016 at 23:00
void process<T> where T : User
?