将方法调用变成可观察事件,好主意吗?

Turning a method call into an observable event, good idea?

我正在学习如何将 IObservable 合并到我的代码中。下面是一个简单的 class 的两种不同方法,它打印出 IObservable<string> 中的最新单词。哪种方法更清洁?我不喜欢 WordPrinterWithCache,因为它引入了额外的状态(_lastWord)变量,有趣的代码现在分散在整个 class 中。我更喜欢 WordPrinterWithExtraSubject 因为有趣的代码已本地化到构造函数。但大多数情况下,它似乎更稳定地发挥作用并且 "reactive";我对两个 "events" 的组合做出反应:(1) 发出一个新词,以及 (2) PrintMostRecent 方法的调用。

但是我读到,在并非绝对必要时使用 Subjects 是不可取的,我正在引入一个不必要的 Subject<Unit>。本质上,我在这里所做的是从方法调用中生成一个可观察对象,这样我就可以在更多地方使用可观察对象组合器函数。我喜欢使用可观察组合器和订阅而不是使用 "old-style" 方法调用树来构建我的代码的想法。我不知道这是不是个好主意。

public class WordPrinterWithCache
{
    string _lastWord = string.Empty;

    public WordPrinterWithCache(IObservable<string> words)
    {
        words.Subscribe(w => _lastWord = w);
    }

    public void PrintMostRecent() => Console.WriteLine(_lastWord);
}

public class WordPrinterWithExtraSubject
{
    Subject<Unit> _printRequest = new Subject<Unit>();

    public WordPrinterWithExtraSubject(IObservable<string> words)
    {
        _printRequest
            .WithLatestFrom(words.StartWith(string.Empty), (_, w) => w)
            .Subscribe(w => Console.WriteLine(w));
    }

    public void PrintMostRecent() => _printRequest.OnNext(Unit.Default);
}

我代码中的实际情况是我有一个 ICommand。当用户调用命令(可能通过单击按钮)时,我想对特定可观察对象的 最新值 采取行动。例如,ICommand 可能代表 Delete,而我想删除列表中由 IObservable<Guid> 表示的所选项目。为每个可观察对象保留一堆 "last emitted value" 缓存变量变得很丑陋。

我倾向于的方法是 ICommand 实现,类似于您在下面看到的。这允许我编写像 deleteCommand.WithLatestFrom(selectedItems,(d,s)=>s).Subscribe(selected=>delete(selected));

这样的代码
public class ObservableCommand : ICommand, IObservable<object>
{
    bool _mostRecentCanExecute = true;
    Subject<object> _executeRequested = new Subject<object>();

    public ObservableCommand(IObservable<bool> canExecute)
    {
        canExecute.Subscribe(c => _mostRecentCanExecute = c);
    }

    public event EventHandler CanExecuteChanged; // not implemented yet

    public bool CanExecute(object parameter) => _mostRecentCanExecute;

    public void Execute(object parameter) => _executeRequested.OnNext(parameter);

    public IDisposable Subscribe(IObserver<object> observer) => _executeRequested.Subscribe(observer);
}

虽然我不确定我是否会使用术语 "turning a method call into an observable event",但我一直是使用完全声明式、函数式和 Rx 驱动代码的粉丝。

您对 ICommand 实现的描述与我几年前写的非常接近,此后我使用了很多次并且非常成功。此外,这已成为我称为 "Reactive Behaviors" 的模式的基础,它提供了许多好处。来自我的博客:

  • Promotes behavior driven development and unit testing.
  • Promotes functional and thread safe programming practises.
  • Reduces the risk of (and if done well, can eliminate) side effects as specific behaviors are isolated in a single well named method.
  • Stops 'code rot' as all behavior is encapsulated within specifically named methods. Want new behaviour? Add a new method. Don't want a specific behavior anymore? Just removed it. Want a specific behavior to change? Change the one method and know that you haven't broken anything else.
  • Provides concise mechanisms for aggregating multiple inputs and promotes asynchronous processes to first-class status.
  • Reduces the need for utility classes as data can be passed through the pipeline as strongly typed anonymous classes.
  • Prevents memory leaks as all behaviors return a disposable that when disposed removes all subscriptions and disposed all managed resources.

您可以阅读全文on my blog

因此,作为一般规则(准则),我强烈建议不要将 IObservable<T> 作为方法的参数。明显的警告是,如果该方法是一个新的 Rx 运算符,例如SelectMySpecialBufferDebounce

这里的理论是IObservable<T>是一种回调机制。它允许某些东西回调到另一个它一无所知的东西。但是在这种情况下,您知道 IObservable<T>(参数)和其他东西(WordPrinterWithCache)。 那么为什么会有这个额外的间接层呢?将值推送到 IObservable<T> 的到底是什么,可以改为调用 WordPrinterWithCache 实例的方法。

在这种情况下,只需调用另一个方法

public class WordPrinterWithCache
{
    private string _lastWord = string.Empty;

    public void SetLastWord(string word)
    {
        _lastWord = word;
    }

    public void PrintMostRecent() => Console.WriteLine(_lastWord);
}

现在这个 class 开始看起来毫无意义,但这可能没问题。简单就好。

使用 Rx 来帮助您分层。

上游层依赖于下游层。他们将直接调用下游层的方法(发出命令)。

下游层将无法访问上游层。因此,要向他们公开数据,他们可以 return 来自方法的值,或者可以公开回调。 GoF Observer 模式、.NET 事件和 Rx 是向上游层提供回调的方法。