可观察流上的转换链(管道)的正确模式?

时间:2014-09-30 13:49:47

标签: c# reactive-programming

我有以下情况:

  • 给定对象流IObservable<E>
  • 处理每个E以获取E1或错误状态,在这种情况下我需要错误消息M1
  • 处理每个E1以获取E2或错误消息M2
  • ....

还有一个额外的复杂因素,即结果En和/或错误消息Mn可能依赖于所有EE1 ,...,En-1 - 不仅在En-1

鉴于这一切,是否有比我使用的更好的模式?

[编辑]根据要求,我添加了一个完整的例子;不幸的是,这个帖子非常大。

internal class Program
{
  private static void Main()
  {
    var stream = Enumerable.Range(1, 10).Select(i => new Record { Id = i }).ToObservable();

    stream
      .Select(it => new ComplexType { Item = it })
      .SelectIfOk(Process1)
      .SelectIfOk(Process2)
      .SelectIfOk(ProcessN)
      .Subscribe(DisplayResult);

    Console.ReadLine();
  }

  private static ComplexType Process1(ComplexType data)
  {
    // do some processing
    data.E1 = data.Item.Id * 10;

    // check for errors in output
    if (data.E1 == 30 || data.E1 == 70)
    {
      data.Errors.Add("Error");
    }

    return data;
  }

  private static ComplexType Process2(ComplexType data)
  {
    // do some processing
    data.E2 = (data.E1 - 3).ToString();

    // check for errors in output
    // can generate multiple errors for the same item
    if (data.E2.StartsWith("4"))
    {
      // does not only depend on the immediate precursor, E1 in this case
      data.Errors.Add("Starts with 4 -- " + data.Item.Id);
    }

    if (data.E2.StartsWith("8"))
    {
      data.Errors.Add("Starts with 8");
    }

    return data;
  }

  private static ComplexType ProcessN(ComplexType data)
  {
    // do some processing
    data.EN = "Success " + data.E2;

    // this one doesn't generate errors
    return data;
  }

  private static void DisplayResult(ComplexType data)
  {
    if (data.Errors.Any())
    {
      Console.WriteLine("{0:##0} has errors: " + string.Join(",", data.Errors));
    }
    else
    {
      Console.WriteLine("{0:##0}: {1}", data.Item.Id, data.EN);
    }
  }
}

这些是上面代码示例中使用的类:

public class Record
{
  public int Id { get; set; }
  public string FullName { get; set; }
  public string OtherStuff { get; set; }
}

public class ComplexType
{
  public Record Item { get; set; }

  // intermediary results
  public int E1 { get; set; }
  public string E2 { get; set; }

  // final result
  public string EN { get; set; }

  public List<string> Errors { get; set; }

  public ComplexType()
  {
    Errors = new List<string>();
  }
}

请注意,E1E2,...,En的类型之间没有任何关系(特别是,它们都不会继承相同的常见类型)。

SelectIfOk是一种扩展方法:

public static IObservable<T> SelectIfOk<T>(this IObservable<T> observable,
  Func<T, T> selector)
  where T : ComplexType
{
  return observable.Select(item => item.Errors.Any() ? item : selector(item));
}

运行此代码的结果是:

1: Success 7
2: Success 17
3 has errors: Error
4: Success 37
5 has errors: Starts with 4 -- 5
6: Success 57
7 has errors: Error
8: Success 77
9 has errors: Starts with 8
10: Success 97

我正在使用ComplexType,因此我可以同时携带中间结果和错误状态,它只是看起来很可疑。我已经盯着那段代码一周了(这是一个爱好项目),我一直觉得我错过了用Rx做事的正确方法。

[编辑]我忘了提到一个非常重要的事情:我必须处理流中的所有项,即使其中一些产生错误;这就是为什么我不能只使用带有异常的Subscribe重载 - 它将完成流。当出现错误时放弃一个项目(如果Process1生成错误,则Process2,...,ProcessN不再执行),但不会放弃整个流。

[编辑]另一个澄清:如果它有帮助,我想到的处理将更自然地适合TPL DataFlow库,除了我限于.NET 4.0所以我不能使用它

顺便说一下,我一直无法在Rx中找到任何关于错误处理的认真讨论,通常会提到Subscribe重载/ OnError来电,这就是它。有没有人建议对该主题进行深入研究?

2 个答案:

答案 0 :(得分:0)

提前了解所有ProcessN意味着您可以摆脱多个Select运算符,简化查询并揭示您的真实意图。

stream.Select(e => Process(e));

...

? Process(E e)
{
  // Process1, Process2, ...ProcessN
}

现在我们看到这不是一个被动的问题。这更像是一个交互式聚合问题。

您还没有提到您最终需要在订阅中查看哪种输出,但我只是假设它是流程的汇总结果。

要定义返回类型,我们首先需要定义ProcessN的返回类型。而不是使用你的ComplexType,我现在将使用具有更好语义的类型:

Either<E, Exception> Process(?);

因此,每个ProcessN函数都可以返回EException(不投掷)。

此外,根据您的要求,每个ProcessN必须接收正在运行的聚合的当前结果作为其输入。所以就像你上面的定义一样,我们必须更换?以及在它之前调用的ProcessN函数的返回值列表。

Either<E, Exception> Process(IList<Either<E, Exception>> results);

现在我们可以定义聚合器的返回类型(如上所述):

IList<Either<E, Exception>> Process(E e)
{
  // Process1, Process2, ...ProcessN
}

聚合器的主体可以按如下方式实现:

IList<Either<E, Exception>> Process(E e)
{
  var results = new List<Either<E, Exception>>();
  results.Add(Process1(results.AsReadOnly()));
  results.Add(Process2(results.AsReadOnly()));
  ...
  results.Add(ProcessN(results.AsReadOnly()));
  return results.AsReadOnly();
}

Rx中的错误处理

首先阅读Rx Design Guidelines

Intro to Rx有一个关于错误处理运算符的部分。

以下是有关Rx中错误处理语义和合同的一些深入讨论/评论:

(完全披露:我为所有这些特别的讨论做出了贡献。)

答案 1 :(得分:0)

当我问这个问题时,我不知道Rx中的Notification<T>类和Materialize方法。这是我提出的解决方案 - 它主要解决了#34;管道和#34;问题的方面,但我可以解决&#34;取决于中间结果&#34;使用元组的方面:

private static void Main()
{
  var source = new Subject<int>();

  source
    .Materialize()
    .SelectIfOk(Process1)
    .SelectIfOk(Process2)
    .Subscribe(it =>
      Console.WriteLine(it.HasValue
        ? it.Value.ToString()
        : it.Exception != null ? it.Exception.Message : "Completed."));

  source.OnNext(1);
  source.OnNext(2);
  source.OnNext(3);
  source.OnNext(4);
  source.OnNext(5);
  source.OnCompleted();

  Console.ReadLine();
}

private static int Process1(int value)
{
  if (value == 3)
    throw new Exception("error 1");

  // do some processing
  return value * 2;
}

private static string Process2(int value)
{
  if (value == 4)
    throw new Exception("error 2");

  // do some processing
  return value + " good";
}

private static IObservable<Notification<TR>> SelectIfOk<T, TR>(this IObservable<Notification<T>> stream,
  Func<T, TR> selector)
{
  Func<T, Notification<TR>> trySelector = it =>
  {
    try
    {
      var value = selector(it);
      return Notification.CreateOnNext(value);
    }
    catch (Exception ex)
    {
      return Notification.CreateOnError<TR>(ex);
    }
  };

  return stream.Select(it =>
    it.HasValue
      ? trySelector(it.Value)
      : it.Exception != null
        ? Notification.CreateOnError<TR>(it.Exception)
        : Notification.CreateOnCompleted<TR>());
}

如果我想使用中间结果那么,正如我所说的,我将使用元组:

private static Tuple<int, string> Process2(int value)
{
  if (value == 4)
    throw new Exception("error 2");

  // do some processing
  return Tuple.Create(value, value * 3 + " good");
}

private static string Process3(Tuple<int, string> value)
{
  return value.Item1 + " -> " + value.Item2;
}

(我需要将.SelectIfOk(Process3)添加到管道中。)

我不愿意将自己的答案标记为正确,所以我会暂时将其打开;但是,据我所知,它确实符合我的要求。