问题
I'm learning how to incorporate IObservable
into my code. Below are two different approaches for a simple class that prints out the most recent word from an IObservable<string>
. Which is the cleaner approach? I don't like WordPrinterWithCache
because it introduces additional state (the _lastWord
) variable and the interesting code is now scattered throughout the class. I prefer WordPrinterWithExtraSubject
because the interesting code is localized to the constructor. But mostly it seems more consistently functional and "reactive"; I am reacting to the combination of two "events": (1) a new word being emitted, and (2) the invocation of the PrintMostRecent
method.
However I have read that using Subjects is not desirable when it is not strictly necessary and I am introducing an unnecessary Subject<Unit>
. Essentially what I'm doing here is generating an observable from a method call so I can use the observable combinator functions in more places. I like the idea of building up my code using the observable combinators and subscriptions rather than using "old-style" method invocation trees. I don't know if this is a good idea.
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);
}
The actual scenario in my code is that I have an ICommand
. When the user invokes the command (maybe by clicking a button), I want to take action on the most recent value of a specific observable. For example, maybe the ICommand
represents Delete
and I want to delete the selected item in a list that is represented by an IObservable<Guid>
. It gets ugly to keep a bunch of "last emitted value" cache variables for each observable.
The approach I'm leaning toward is an ICommand
implementation kind of like what you see below. This allows me to write code like 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);
}
回答1:
While I'm not sure I'd use the term "turning a method call into an observable event", I have been a fan of using fully declarative, functional and Rx driven code for a while now.
Your description of an ICommand implementation matches very closely with one I wrote a couple of years ago and have used many times and very successfully since. Furthermore, this has become the basis for a pattern I refer to as "Reactive Behaviors" which provides numerous benefits. From my blog:
- 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.
You can read the full article on my blog.
回答2:
So as a general rule (guideline), I strongly suggest not having an IObservable<T>
as a parameter to a method. The obvious caveat being if that method is a new Rx Operator e.g. Select
, MySpecialBuffer
, Debounce
etc.
The theory here is that IObservable<T>
is a callback mechanism. It allows something to callback to another thing that it otherwise knows nothing about. However in this case, you have something that knows about both the IObservable<T>
(parameter) and the other thing (WordPrinterWithCache
).
So why is there this extra layer of indirection? What ever it was that was pushing values to the IObservable<T>
could just instead call a method of the WordPrinterWithCache
instance.
In this case, just call a method on the other thing
public class WordPrinterWithCache
{
private string _lastWord = string.Empty;
public void SetLastWord(string word)
{
_lastWord = word;
}
public void PrintMostRecent() => Console.WriteLine(_lastWord);
}
Now this class starts to look rather pointless, but that might be ok. Simple is good.
Use Rx to help you with layering.
Upstream layers depend on down stream layers. They will directly call methods (issue commands) on the downstream layers.
Downstream layers wont have access to upstream layers. So to expose data to them they can either return values from methods, or can expose callbacks. The GoF Observer patter, .NET Events and Rx are ways to provide callbacks to upstream layers.
来源:https://stackoverflow.com/questions/41685020/turning-a-method-call-into-an-observable-event-good-idea