C#中的最佳实践/习语用于创建"案例类"

时间:2017-10-26 18:43:02

标签: c# inheritance idioms

我有以下情况:

  • 用户可以执行许多不同的操作。
  • 我想记住在List中采取的行动。
  • 要存储每种类型的操作,我需要存储不同的参数。 (比方说,重命名操作的ID和字符串或开始操作的日期时间。)

我已经在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再次感到笨拙。

2 个答案:

答案 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,尝试执行以下操作:

  1. 定义您的所有操作都将实现的界面。它可能非常简单,如下:
  2. 
        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. 调用者类不应该知道将哪些特定类传递给它的方法:它的责任只是逐个运行所有操作:
    2. 
          public class ActionExecutor
          {
              // ommit details
              public void Execute(IEnumerable actions)
              {
                  foreach (var action in actions)
                  {
                      action.Do();
                  }
              }
          }
      
      1. 假设不同的动作可能有完全不同的输入参数集,我建议也为它们引入抽象工厂:
      2. 
            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成员,或映射到某些数据库配置表或其他内容的常量)和工厂。在方法上将有一个配置类
        2. 
              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. 最后,要创建工厂,您可以实现另一个类
          2. 
                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;
                    }
                }
            

            对于私人项目来说,这似乎是一个巨大的开销。但是这种方法的优势在于大多数类只编写(和测试)一次,并且在添加新操作时永远不会改变。所以它们可以独立开发。