在 C# 中创建的最佳实践/习语 "case classes"
Best practice / idioms in C# to create "case classes"
我有以下情况:
- 用户可以执行许多不同的操作。
- 我想记住列表中执行的操作。
- 为了存储每种类型的动作,我需要存储不同的参数。 (比方说,rename 操作的 ID 和字符串或 start 操作的日期时间。)
我用 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
又感觉笨拙了。
如评论中所述,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)如果存在因子类型而异的行为,那么应该在子类型中捕获该行为,而不是在子类型外部的代码中,等等.
两种提议的方法(switch
es 和 if
s)都有相同的缺陷:每次引入新的动作类型时,您都必须修改 switch or 和另一个 else-if建造。您可能会接受它用于您自己的宠物项目,但它不是灵活、可维护设计的示例。一段时间后,这些开关往往变得非常巨大和可怕。更糟糕的是,没有人能够在无法访问您的代码(即,将其作为库引用)的情况下扩展您的功能。
牢记 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
}
}
- 调用者class应该不知道哪些特定的classes被传递给它的方法:它的责任只是运行所有的动作一一进行:
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
成员,或常量映射到某些数据库配置 table 或其他)和工厂之间有一些对应关系。其中一种方法是配置 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;
}
}
另一种方法是为每个动作类型存储一个 lambda 函数。在这种情况下不需要工厂,但代码变得更难测试。
另一种方法包括使用一些 IoC-frameworks.
- 最后,要创建一个工厂,您可以实现另一个 class
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;
}
}
对于私人项目来说,这似乎是一笔巨大的开销。但这种方法的优点是大多数 classes 只编写(和测试)一次,并且永远不会随着新操作的添加而改变。所以他们可以独立开发。
我有以下情况:
- 用户可以执行许多不同的操作。
- 我想记住列表中执行的操作。
- 为了存储每种类型的动作,我需要存储不同的参数。 (比方说,rename 操作的 ID 和字符串或 start 操作的日期时间。)
我用 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
又感觉笨拙了。
如评论中所述,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)如果存在因子类型而异的行为,那么应该在子类型中捕获该行为,而不是在子类型外部的代码中,等等.
两种提议的方法(switch
es 和 if
s)都有相同的缺陷:每次引入新的动作类型时,您都必须修改 switch or 和另一个 else-if建造。您可能会接受它用于您自己的宠物项目,但它不是灵活、可维护设计的示例。一段时间后,这些开关往往变得非常巨大和可怕。更糟糕的是,没有人能够在无法访问您的代码(即,将其作为库引用)的情况下扩展您的功能。
牢记 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
}
}
- 调用者class应该不知道哪些特定的classes被传递给它的方法:它的责任只是运行所有的动作一一进行:
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
成员,或常量映射到某些数据库配置 table 或其他)和工厂之间有一些对应关系。其中一种方法是配置 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;
}
}
另一种方法是为每个动作类型存储一个 lambda 函数。在这种情况下不需要工厂,但代码变得更难测试。
另一种方法包括使用一些 IoC-frameworks.
- 最后,要创建一个工厂,您可以实现另一个 class
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;
}
}
对于私人项目来说,这似乎是一笔巨大的开销。但这种方法的优点是大多数 classes 只编写(和测试)一次,并且永远不会随着新操作的添加而改变。所以他们可以独立开发。