I have the following situation:
I have been coding in Scala for a while, there I would write something like
abstract class Action
case class RenameAction(id: Int, newTitle: String) extends Action
case class StartAction(time: Instant) extends Action
so that I could then write functions like
def process(action: Action) = action match {
case RenameAction(id, title) => ...
case StartAction(time) => ...
}
My question is: What is the best practice / most idiomatic way to solve this in C#?
I'll describe some thoughts I have:
1st possibility: Direct translation
public abstract class Action
{
}
public sealed class RenameAction : Action
{
public readonly int id;
public readonly string title;
public RenameAction(int id, string title)
{
this.id = id;
this.title = title;
}
}
public sealed class StartAction : Action
{
public readonly DateTime time;
public StartAction(DateTime time)
{
this.time = time;
}
}
...
public void process(Action action)
{
if (action is RenameAction)
{
RenameAction ra = action as RenameAction;
...
}
else if (action is StartAction)
{
StartAction sa = action as StartAction;
...
}
}
This obviously works, but feels clunky to me. (It's just a small private project without time pressure, and in those I like to write code that I'm happy about ;))
2nd possibility: Using an enum, I could do something like this:
public sealed class Action
{
public readonly ActionType type;
public readonly object[] parameters;
public Action(ActionType type, params object[] parameters)
{
this.type = type;
this.parameters = parameters;
}
}
enum ActionType
{
RENAME,
START
}
...
public void process(Action action)
{
switch(action.type)
{
case ActionType.RENAME:
var id = action.parameters[0] as int;
var title = action.parameters[1] as string;
...
break;
case ActionType.START:
var time = action.parameters[0] as DateTime;
...
break;
}
}
This has the benefit that the number of action types is fixed, however the object[] parameters
feels clunky again.
A programming idiom is the usual way to code a task in a specific language. For example a loop is often written like this in C: for (i=0; i<10; i++)
As noted in the comments, C# 7 has pattern matching on types in switches. You could write
public void Process(MyAction action)
{
if (action is RenameAction)
{
RenameAction ra = action as RenameAction;
...
}
else if (action is StartAction)
{
StartAction sa = action as StartAction;
...
}
more concisely as
public void Process(MyAction action)
{
switch(action)
{
case RenameAction ra:
...
case StartAction sa:
...
case null:
...
default:
...
}
and so on.
Note that you can also add constraints:
case RenameAction ra when (ra.TargetName != null):
That said: switching on a set of possible subtypes is considered to be a poor programming practice by many, for some good reasons. Such as (1) if new subtypes are created, then every switch has to be updated, (2) if there's behaviour that varies by subtype then that behaviour should be captured in the subtype, not in code external to the subtype, and so on.
Both proposed ways (with switch
es and if
s) have the same flaw: each time a new action type is introduced you are obliged to modify the switch or and another else-if construction. You may be OK with it working on a pet project of your own, but it is not an example of a flexible, maintainable design. After a while these switches tend to become really huge and dreadful. And what is even worse is that no one is able to extend you functionality not having an access to your code (i.e. having it referenced as a library).
Keeping in mind SOLID principles, try to do the following:
public interface IAction
{
void Do();
}
public class RenameAction : IAction
{
public readonly int id;
public readonly string title;
public RenameAction(int id, string title)
{
this.id = id;
this.title = title;
}
public void Do()
{
// Perform an action
}
}
public class ActionExecutor
{
// ommit details
public void Execute(IEnumerable actions)
{
foreach (var action in actions)
{
action.Do();
}
}
}
public interface IActionFactory
{
IAction Create();
}
public class RenameActionFactory : IActionFactory
{
public IAction Create(IDictionary parameters)
{
// using a dictionary - is not a best choise. Just an example
return new RenameAction(parameters["id"], parameters["title"]);
}
}
enum
members, or constants mapped to some DB configuration table, or something else) and factories. On of the ways would be have a configuration class
public class ActionFactoriesConfiguration
{
private readonly Dictionary _configuration;
public ActionFactoriesConfiguration()
{
_configuration = new Dictionary
{
{ ActionType.Rename, RenameActionFactory }
}
}
public Type GetActionFactoryType(ActionType actionType)
{
if (_configuration.ContainsKey(actionType))
{
return _configuration[actionType];
}
return null;
}
}
Another way would be storing a lambda-functions for each action type. In this case no factories are needed, but the code becomes harder to test.
Another yet way includes using some of the IoC-frameworks.
public class ActionFactoryResolver
{
private readonly ActionFactoriesConfiguration _configuration;
public ActionFactoryResolver(ActionFactoriesConfiguration configuration)
{
_configuration = configuration;
}
public IActionFactory CreateFactory(ActionType actionType)
{
var factoryType = _configuration.GetActionFactoryType(actionType);
if (factoryType != null)
return Activator.CreateInstance(actionType);
return null;
}
}
This may seem as a huge overhead for a private project. But the strong side of this approach is that most of the classes are written (and tested) only once and never get changed with new actions adding. So they can be developed independently.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With