You cannot add or remove method from generic interface based on the type. Also, CLR doesn’t support higher kinded types (as of now). Let’s say that we want to encode state of an object in its type and disallow some operations depending on the state.
In our example we will use issue. We can assign issue if it is unassigned. We cannot assign it for the second time. We can move issue from backlog to sprint (so it is a todo item) but we cannot do it for the second time. Of course, those requirements are just for an example.
Let’s go with the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public interface IAssignedState { } public class Assigned : IAssignedState { Assigned() {} } public class Unassigned : IAssignedState{ public Unassigned() {} } public interface IAssignable< T,U> where T : IAssignedState { U assign(T t); } public interface IBacklogState { } public class InBacklog : IBacklogState { public InBacklog() {} } public class Todo : IBacklogState { Todo() {} } public interface ITodoable< T, U> where T : IBacklogState { U addToSprint(T t); } public class Issue< TAssignableState, TBacklogState> : IAssignable< TAssignableState, Issue< Assigned, TBacklogState>>, ITodoable< TBacklogState, Issue< TAssignableState, Todo>> where TAssignableState : IAssignedState where TBacklogState : IBacklogState { public Issue< Assigned, TBacklogState> assign(TAssignableState t) { return new Issue< Assigned, TBacklogState>(); } public Issue< TAssignableState, Todo> addToSprint(TBacklogState t){ return new Issue< TAssignableState, Todo>(); } } |
The trick here is to use phantom types, types which we cannot create. In this example those are Assigned
and Todo
. So we can do this:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Program { public static void onlyUnassignedIssues< TBacklogState>(Issue< Unassigned, TBacklogState> a) where TBacklogState : IBacklogState{ } public static void Main() { Issue< Unassigned, InBacklog> unassigned = new Issue< Unassigned, InBacklog>(); onlyUnassignedIssues(unassigned); Issue< Assigned, InBacklog> assigned = unassigned.assign(new Unassigned()); } } |
But we cannot do this:
1 2 3 4 5 6 7 |
onlyUnassignedIssues(assigned); // I cannot pass assigned issue Issue< Assigned, InBacklog> assigned2 = assigned.assign(new Assigned()); // I cannot assign issue again Issue< Unassigned, Todo> todo = unassigned.addToSprint(new InBacklog()); // I can first move it to sprint Issue< Assigned, Todo> assignedTodo = todo.assign(new Unassigned()); // And then assign Issue< Assigned, Todo> movedToSprintAgain = assignedTodo.addToSprint(new Todo()); // But I cannot move it to sprint again |
If we didn’t use generics, we would need to use a lot of inheritance: AssignableTodoableIssue
, TodoableAssignedIssue
, AssignableTodoIssue
, AssignedTodoIssue
. Imagine now adding more state.
You can find similar solution in Scala in Action, look for phantom types.