我有以下情况:
我已经在Scala编写了一段时间,在那里我会写一些像
abstract class Action
case class RenameAction(id: Int, newTitle: String) extends Action
case class StartAction(time: Instant) extends Action
这样我就可以编写像
这样的函数了def process(action: Action) = action match {
case RenameAction(id, title) => ...
case StartAction(time) => ...
}
我的问题是:在C#中解决这个问题的最佳实践/最惯用的方法是什么?
我将描述一些想法:
第一种可能性:直接翻译
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;
...
}
}
这显然有效,但对我来说却很笨拙。 (它只是一个没有时间压力的小型私人项目,而且我喜欢编写那些我很高兴的代码;)
第二种可能性:使用枚举,我可以这样做:
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;
}
}
这样做的好处是动作类型的数量是固定的,但object[] parameters
再次感到笨拙。
答案 0 :(得分:4)
如评论中所述,C#7在交换机中的类型上具有模式匹配。你可以写
public void Process(MyAction action)
{
if (action is RenameAction)
{
RenameAction ra = action as RenameAction;
...
}
else if (action is StartAction)
{
StartAction sa = action as StartAction;
...
}
更简明扼要
public void Process(MyAction action)
{
switch(action)
{
case RenameAction ra:
...
case StartAction sa:
...
case null:
...
default:
...
}
等等。
请注意,您还可以添加约束:
case RenameAction ra when (ra.TargetName != null):
这就是说:出于某些原因,开启一组可能的子类型被许多人认为是一种糟糕的编程习惯。例如(1)如果创建了新的子类型,那么每个开关都必须更新,(2)如果行为因子类型而异,那么该行为应该在子类型中捕获,而不是在子类型外部的代码中捕获,依此类推
答案 1 :(得分:2)
两种提议的方式(switch
es和if
s)具有相同的缺陷:每次引入新的动作类型时,您都必须修改开关或其他 - 如果构造。你可以在自己的宠物项目上工作,但它不是一个灵活,可维护的设计的例子。过了一会儿,这些开关往往变得非常庞大和可怕。更糟糕的是,没有人能够扩展您无法访问代码的功能(即将其作为库引用)。
请记住SOLID principles,尝试执行以下操作:
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
成员,或映射到某些数据库配置表或其他内容的常量)和工厂。在方法上将有一个配置类
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;
}
}
另一种方法是为每个动作类型存储lambda函数。在这种情况下,不需要工厂,但代码变得更难测试。
另一种方法包括使用一些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;
}
}
对于私人项目来说,这似乎是一个巨大的开销。但是这种方法的优势在于大多数类只编写(和测试)一次,并且在添加新操作时永远不会改变。所以它们可以独立开发。