在 C# 中创建的最佳实践/习语 "case classes"

Best practice / idioms in C# to create "case classes"

我有以下情况:

我用 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)如果存在因子类型而异的行为,那么应该在子类型中捕获该行为,而不是在子类型外部的代码中,等等.

两种提议的方法(switches 和 ifs)都有相同的缺陷:每次引入新的动作类型时,您都必须修改 switch or 和另一个 else-if建造。您可能会接受它用于您自己的宠物项目,但它不是灵活、可维护设计的示例。一段时间后,这些开关往往变得非常巨大和可怕。更糟糕的是,没有人能够在无法访问您的代码(即,将其作为库引用)的情况下扩展您的功能。

牢记 SOLID principles,尝试执行以下操作:

  1. 定义一个您的所有操作都将实现的接口。可能真的很简单,像这样:

    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
        }
    }
  1. 调用者class应该不知道哪些特定的classes被传递给它的方法:它的责任只是运行所有的动作一一进行:

    public class ActionExecutor
    {
        // ommit details
        public void Execute(IEnumerable actions)
        {
            foreach (var action in actions)
            {
                action.Do();
            }
        }
    }
  1. 假设不同的动作可能有完全不同的输入参数集,我建议也为它们引入抽象工厂:

    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"]);
        }
    }
  1. 然后,您必须在操作类型(它们可能被定义为 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.

  1. 最后,要创建一个工厂,您可以实现另一个 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 只编写(和测试)一次,并且永远不会随着新操作的添加而改变。所以他们可以独立开发。