我有一个 Mediator 类,它有一个注册管道方法,该方法采用消息类型、管道过滤器类型和消息处理程序类型。

internal static void RegisterPipeLine<T1, T2, T3>()
      where T1 : IMessage
      where T2 : BaseFilter
      where T3 : IMessageHandler
  {
      var pipeLine = new PipeLine()
      {
          Filters = [typeof(T2)],
          MessageHandler = typeof(T3)
      };
      pipeLines[typeof(T1)] = pipeLine;
  }

我这样称呼

  Mediator.RegisterPipeLine<Message1, LogFilter, MessageHandler1>();

这很好,但我想有多个管道过滤器。到目前为止,我已经像这样重载了 RegisterPipeLine 方法

 internal static void RegisterPipeLine<T1, T2, T3, T4>()
        where T1 : IMessage
        where T2 : BaseFilter
        where T3 : BaseFilter
        where T4 : IMessageHandler
     {
         var pipeLine = new PipeLine()
         {
             Filters = [typeof(T2), typeof(T3)],
             MessageHandler = typeof(T4)
         };
         pipeLines[typeof(T1)] = pipeLine;
     }

我称之为

 Mediator.RegisterPipeLine<Message2, LogFilter, ValidFilter, MessageHandler2>();

但我不想为管道中想要的每个过滤器创建特定的过载。

有没有办法做得更好?

12

  • 那么会有多少差异?在某个时候会有更多的消息类型吗?或者只能有更多的过滤器和处理程序类型?


    – 

  • 2
    RegisterPipeLine首先,您的方法看起来不需要是通用的。为什么不让您的调用者只提供Type参数呢?


    – 


  • @Dai 通用约束。


    – 

  • 1
    @Dai 但这样你就需要实例了BaseFilter,这会违背目的。


    – 

  • 1
    @JohnathanBarclay…或者IEnumerable<Type> filterTypes


    – 


5 个回答
5

您可以使用类似构建器模式的东西:

public class PipelineBuilder<TMessage,TMessageHandler>() where TMessage: IMessage, TMessageHandler : IMessageHandler
{
   private List<Type> filters = new List<Type>();

   public PipeLineBuilder<TMessage,TMessageHandler> WithFilter<TFilter>() where TFilter : BaseFilter
   {
      filters.Add(typeof(TFilter));
      return this;
   }

   public void Register()
   {
      var pipeLine = new PipeLine()
      {
          Filters = filters,
          MessageHandler = typeof(TMessageHandler)
      };
      pipeLines[typeof(TMessage)] = pipeLine;
   }
}

用法是

 new PipelineBuilder<Message2, MessageHandler2>()
       .WithFilter<LogFilter>()
       .WithFilter<ValidFilter>()
       .Register();

1

  • 1
    How do you intend for the pipeLines dictionary to work in this solution?

    – 

Why don’t you prefer to use params keyword? If you do so, we can handle multiple filters without creating separate overloads for each number of filters.

internal static void RegisterPipeLine<TMessage, THandler>(params Type[] filterTypes)
    where TMessage : IMessage
    where THandler : IMessageHandler
{
    var pipeLine = new PipeLine()
    {
        Filters = filterTypes,
        MessageHandler = typeof(THandler)
    };
    pipeLines[typeof(TMessage)] = pipeLine;
}

We can call as such.

Mediator.RegisterPipeLine<Message1, MessageHandler1>(typeof(LogFilter), typeof(ValidFilter));

By doing so, we can call with any number of filter types.

3

  • main issue is that we don’t get compile time check to make sure that the params of Type are actually all derived from BaseFilter, like we do with a bit of extra work in my answer.

    – 


  • @tinmanjk, there is no excellence, and there are always drawbacks when there are advantages.

    – 

  • one can strive for it. OP could have overloaded to Func<T1,T2,T3…> style but then again wondered if something could be done…compile time.

    – 

I think this would work with adding a helper class and an interface (for covariance) to achieve full compile time safety.
The cost for me is negligible as we instantiate empty objects that won’t put too much pressure on the GC.

public class TypeOf<T> : ITypeOf<T> {
    public Type Type => typeof(T);
    public static TypeOf<T> Get => new TypeOf<T>(); // convenience
}

public interface ITypeOf<out T> {
    Type Type { get; }
}

internal static void RegisterPipeLine<T1, T2, T3>(params ITypeOf<T2>[] baseFilters)
      where T1 : IMessage
      where T2 : BaseFilter
      where T3 : IMessageHandler
  {
      var pipeLine = new PipeLine()
      {
          Filters = baseFilters.Select(x => x.Type).ToList()
          MessageHandler = typeof(T3)
      };
      pipeLines[typeof(T1)] = pipeLine;
  }

Usage is

RegisterPipeLine<,BaseFilter,>(TypeOf<LogFilter>.Get, TypeOf<ValidFilter>.Get);

Here is my take on it. Using two builders.

static void Main(string[] args)
{
    var builder = new PipelineBuilder();
    builder
        .AddPipeLine<Message1, MessageHandler1>()
            .WithFilter<LogFilter>()
            .WithFilter<ValidFilter>();
    builder
        .AddPipeLine<Message2, MessageHandler2>()
            .WithFilter<LogFilter>();

    var pipelines = builder.Build();
}

public class PipelineBuilder()
{
    private List<(Type message, Type handler, FilterBuilder builder)> items = new();

    public IFilterBuilder AddPipeLine<T1, T2>()
         where T1 : IMessage
         where T2 : IMessageHandler
    {
        var filterBuilder = new FilterBuilder();
        items.Add((typeof(T1), typeof(T2), filterBuilder));

        return filterBuilder;
    }

    public IReadOnlyDictionary<Type, PipeLine> Build()
    {
        var pipelines = new Dictionary<Type, PipeLine>();
        foreach (var (message, handler, builder) in items) 
        {
            pipelines.Add(
                message, 
                new PipeLine()
                {
                    Filters = builder.Filters,
                    MessageHandler = handler
                });
        }
        return pipelines;
    }
}

public class FilterBuilder : IFilterBuilder
{
    public List<Type> Filters { get; } = new();

    public IFilterBuilder WithFilter<T>() where T : BaseFilter
    {
        Filters.Add(typeof(T));
        return this;
    }
}

public interface IFilterBuilder
{
    IFilterBuilder WithFilter<T>() where T : BaseFilter;
}

If I were you, I would prefer to use the Chain of Responsibility pattern for filter. It’s more nature for your implementation example.

public abstract class BaseFilter
{
    public abstract bool Filter();
}
public abstract class BaseFilter<T>: BaseFilter where T : BaseFilter, new()
{
    private readonly T _innerFilter;

    protected BaseFilter()
    {
        _innerFilter = new T();
    }

    public override bool Filter(IMessage message) 
    {
        _innerFilter.Filter(message);
        return true; //// e.g.
    }
}

This will give you flexibility of using different filters in conjunction with each other.
As for example

public bool Filter(IMessage message)
{
    return _innerFilter.Filter(message) && message.Contains("test"); //// e.g.
}

Your call side of the code won’t change, and it might look like

Mediator.RegisterPipeLine<Message1, ValidFilter<LogFilter>, MessageHandler1>();

. The implementation of this pattern for your case might be different and look like builder that also described here.