Живые приложения с rx
TRANSCRIPT
Почему Reac,ve Extensions? • Отзывчивость приложений • Упрощение кода работы со временем • Упрощение управления потоками • Упрощение подписки-‐отписки для событий • Тестируемость кода • LINQ для событий и потоков данных • Наличие реализаций для других языков программирования
2
IEnumerable<T> vs IObservable<T>
3
Преимущества Rx перед событиями
• События однопоточны. Все обработчики будут вызываться по очереди в потоке который запустил событие.
• Если в одном из обработчиков события будет выброшено исключение все остальные обработчики не будут вызваны
• Все подписки на события необходимо отписывать. Для этого придется хранить ссылку на метод для отписки и объект
• Rx поддерживает async/await для запросов требующих ожидания конкретного элемента
• Rx предоставляет методы для управления временем и потоками, а также тестовые реализации управляющих объектов.
• Rx предоставляет методы для связи нескольких потоков данных в один
• Rx предоставляет возможность обрабатывать данные аналогично LINQ-‐запросу в функциональном стиле.
4
Основные интерфейсы Rx
• IObservable<T> IDisposable Subscribe(IObserver<T> observer);
• IObserver<T> void OnNext(T value); void OnError(Exception error); void OnCompleted();
• IScheduler • ISubject<T> : IObservable<T>, IObserver<T>
5
Простой класс с Rx public class LogoutManager { public IObservable<Unit> Logout { get; private set; } public IObserver<Unit> UserActionsObserver { get; private set; } public IObserver<Unit> LogoutCommandsObserver { get; private set; } public LogoutManager(TimeSpan timeout) { var userActionsSubject = new Subject<Unit>(); var logoutSubject = new Subject<Unit>(); UserActionsObserver = userActionsSubject.AsObserver(); LogoutCommandsObserver = logoutSubject.AsObserver(); Logout = userActionsSubject .StartWith(Unit.Default) .Throttle(timeout) .Merge(logoutSubject); } }
6
Потоки данных в LogoutManager
Logout = userActionsSubject .StartWith(Unit.Default) .Throttle(timeout) .Merge(logoutSubject);
7
Интеграция Rx с существующими событиями
• Короткая: Observable.FromEventPattern(source, "PropertyChanged");
• Типобезопасная: Observable.FromEventPattern
<PropertyChangedEventHandler,PropertyChangedEventArgs>(
handler => source.PropertyChanged += handler,
handler => source.PropertyChanged -‐= handler)
• Промежуточные варианты с указанием типов только
для части операндов 8
Текстовый фильтр на Rx
9
Текстовый фильтр на Rx Observable.FromEventPattern <TextChangedEventHandler, TextChangedEventArgs>( handler => filterTextBox.TextChanged += handler, handler => filterTextBox.TextChanged -‐= handler) .Select(x => x.Sender) .Cast<TextBox>() .Select(x => x.Text) .Where(x => x.Length > 2)
.Subscribe(FilterCollection);
10
Расширенный фильтр на Rx
• Исходная коллекция также может меняться • Пока пользователь продолжает ввод не имеет смысла
перезапускать поиск • Логика фильтрации может быть достаточно сложна и
отнимать много времени • Пользователь должен видеть результаты по мере их
поступления, а не после завершения фильтрации • Пользователь может начать вводить фильтр, а затем
вернуть старый. Повторную фильтрацию при этом не надо запускать
11
Расширенный фильтр на Rx
public interface IReactiveCollection<TItem> :
INotifyPropertyChanged, IDisposable
{
// Источник данных (полная коллекция) TItem[] Source { get; set; } // Отображаемая часть данных (отфильтрованная коллекция) TItem[] View { get; }
// Строка фильтра string Filter { get; set; } }
12
Расширенный фильтр на Rx public ReactiveCollection(Func<TItem, string, bool> filterFunc) { var buffer = new ObservableCollection<TItem>(); _subscription = this.PropertyChangedAsObservable(x => x.Filter) .Throttle(TimeSpan.FromMilliseconds(200)) .Where(_ => string.IsNullOrWhiteSpace(Filter) || Filter.Length > 2) .DistinctUntilChanged(_ => Filter) .Merge(this.PropertyChangedAsObservable(x => x.Source)) .Do(_ => buffer.Clear()) .Select(_ => Source.EmptyIfNull() .ToObservable() .Where(x => filterFunc(x, Filter)) .Buffer(TimeSpan.FromMilliseconds(200)) .Do(x => x.ForEach(buffer.Add))) .Switch() .Subscribe(_ => View = buffer.ToArray()); }
13
Потоки данных в расширенном фильтре
this.PropertyChangedAsObservable(x => x.Filter) .Throttle(TimeSpan.FromMilliseconds(200)) .Where(_ => string.IsNullOrWhiteSpace(Filter) || Filter.Length > 2) .DistinctUntilChanged(_ => Filter) .Merge(this.PropertyChangedAsObservable(x => x.Source))
14
Потоки данных в расширенном фильтре
.Select(_ => Source.EmptyIfNull() .ToObservable() .Where(x => filterFunc(x, Filter)) .Buffer(TimeSpan.FromMilliseconds(200)) .Do(x => x.ForEach(buffer.Add))) .Switch()
15
Управление потоками в Rx public ReactiveCollection(Func<TItem, string, bool> filterFunc, ) { var buffer = new ObservableCollection<TItem>(); _subscription = this.PropertyChangedAsObservable(x => x.Filter) .Throttle(TimeSpan.FromMilliseconds(timeout)) .Where(_ => string.IsNullOrWhiteSpace(Filter) || Filter.Length > 3) .DistinctUntilChanged(_ => Filter) .Merge(this.PropertyChangedAsObservable(x => x.Source)) .Do(_ => buffer.Clear()) .Select(_ => Source.EmptyIfNull() .ToObservable( ) .Where(x => filterFunc(x, Filter)) .Buffer(TimeSpan.FromMilliseconds(timeout)) .Do(x => x.ForEach(buffer.Add))) .Switch() .ObserveOn(observeOn) .Subscribe(_ => View = buffer.ToArray()); }
IScheduler filterScheduler, IScheduler observeOn
filterScheduler
16
Применение фильтра на Rx
17
Проблемы реализации при помощи событий
• Необходимость реализации задержки для событий – для того, чтобы не загружать списки клиентов и счетов много раз
• Постоянные подписки и отписки на события элементов коллекции клиентов, отсутствие отписок быстро приведет к утечке памяти в приложении.
• Код для управления потоками – фильтрация в отдельном потоке, вывод результатов в другом
18
Применение фильтра на Rx Companies = new ReactiveCollection<Company>((x, y) =>
string.IsNullOrWhiteSpace(y) || x.Name.Contains(y)) { Source = CompanyList }; Clients = new ReactiveCollection<Client>((x, y) =>
string.IsNullOrWhiteSpace(y) || x.Name.Contains(y));
Accounts = new ReactiveCollection<Account>(SlowFilter);
private bool SlowFilter(Account x, string y) { Thread.Sleep(random.Next(50, 250)); return string.IsNullOrWhiteSpace(y) || x.Number.ToString().Contains(y); }
19
Применение фильтра на Rx Companies.Source.Select(x => x.PropertyChangedAsObservable(y => y.Selected))
.Merge()
.Throttle(TimeSpan.FromMilliseconds(200))
.ObserveOnDispatcher()
.Subscribe(_ => Clients.Source = GetClients(Companies.Source
.Where(x => x.Selected)
.Select(x => x.Id)));
Clients.PropertyChangedAsObservable(x => x.Source)
.Select(_ => Clients.Source)
.Select(x => x.Select(y => y.PropertyChangedAsObservable(z => z.Selected))
.Merge()
.Throttle(TimeSpan.FromMilliseconds(100)))
.Switch()
.ObserveOnDispatcher()
.Subscribe(_ => Accounts.Source = GetAccountSource(Clients.Source .Where(x => x.Selected)
.Select(x => x.Id))); 20
А что с тестированием?
• Управление временем • Управление потоками • Буферизация • Скорость выполнения тестов
21
Тестирование Rx при помощи интерфейса IScheduler
public class LogoutManager { public IObservable<Unit> Logout { get; private set; } public IObserver<Unit> UserActionsObserver { get; private set; } public IObserver<Unit> LogoutCommandsObserver { get; private set; } public LogoutManager(TimeSpan timeout, ) { var userActionsSubject = new Subject<Unit>(); var logoutSubject = new Subject<Unit>(); UserActionsObserver = userActionsSubject.AsObserver(); LogoutCommandsObserver = logoutSubject.AsObserver(); Logout = userActionsSubject.StartWith(Unit.Default) .Throttle(timeout, ) .Merge(logoutSubject); } public LogoutManager(TimeSpan timeout) : this(timeout, Scheduler.Default) { } }
IScheduler scheduler
scheduler
22
Тестирование Rx
[Test]
public void OnInactivity_Logout()
{
var testScheduler = new TestScheduler();
var manager = new LogoutManager(TimeSpan.FromMinutes(5), testScheduler);
manager.Logout.Subscribe(_ => Assert.Pass());
testScheduler.AdvanceBy(TimeSpan.FromMinutes(6).Ticks);
Assert.Fail();
}
23
Тестирование Rx
[Test]
public void OnActivityWithinThreshold_DoNotLogout()
{
var testScheduler = new TestScheduler();
var manager = new LogoutManager(TimeSpan.FromMinutes(5), testScheduler);
manager.Logout.Subscribe(_ => Assert.Fail());
for (var i = 0; i < 10; i++)
{
manager.UserActionsObserver.OnNext(Unit.Default);
testScheduler.AdvanceBy(TimeSpan.FromMinutes(3).Ticks);
}
}
24
Тестирование Rx
[Test] public void OnActivityCommand_LogoutImmediately() { var manager = new LogoutManager(TimeSpan.FromMinutes(5)); manager.Logout.Subscribe(_ => Assert.Pass()); manager.LogoutCommandsObserver.OnNext(Unit.Default); Assert.Fail(); }
25
Тестирование Rx public ReactiveCollection(Func<TItem, string, bool> filterFunc) : this(filterFunc, NewThreadScheduler.Default, DispatcherScheduler.Current, ) { }
public ReactiveCollection(Func<TItem, string, bool> filterFunc, IScheduler scheduler) : this(filterFunc, scheduler, scheduler, scheduler) { }
public ReactiveCollection(Func<TItem, string, bool> filterFunc,
IScheduler filterScheduler, IScheduler observeOn, ) {
var buffer = new ObservableCollection<TItem>();
_subscription = this.PropertyChangedAsObservable(x => x.Filter)
.Throttle(TimeSpan.FromMilliseconds(timeout), timerScheduler) .Where(_ => string.IsNullOrWhiteSpace(Filter) || Filter.Length > 3)
.DistinctUntilChanged(_ => Filter) .Merge(this.PropertyChangedAsObservable(x => x.Source))
.Do(_ => buffer.Clear())
.Select(_ => Source.EmptyIfNull()
.ToObservable(filterScheduler)
.Where(x => filterFunc(x, Filter))
.Buffer(TimeSpan.FromMilliseconds(200), timerScheduler) .Do(x => x.ForEach(buffer.Add)))
.Switch()
.ObserveOn(observeOn) .Subscribe(_ => View = buffer.ToArray());
}
Scheduler.Default
IScheduler timerScheduler
26
Тестирование Rx
[Test] public void OnFilter_FilterView() { var testScheduler = new TestScheduler(); var collection = new ReactiveCollection<string>(
(x, y) => x.Contains(y), testScheduler) { Source = new[] {"client", "bad client", "item", "second item"}, }; collection.Filter = "item"; testScheduler.Start(); Assert.IsTrue(new[] {"item", "second item"}.SequenceEqual(collection.View)); }
27
Тестирование Rx
[Test] public void OnFilter_RunFiltrationInAnotherThread() { var testScheduler = new TestScheduler(); var filterThreadId = 0; new ReactiveCollection<int>((x, y) => { filterThreadId = Thread.CurrentThread.ManagedThreadId; return true; }, NewThreadScheduler.Default, testScheduler, testScheduler) collection.Source = new int[10]; testScheduler.Start(); Assert.AreNotEqual(Thread.CurrentThread.ManagedThreadId, filterThreadId); }
28
Тестирование Rx
[Test] public void OnSlowFilter_BufferResults() { var testScheduler = new TestScheduler(); var filterCounter = 0; var collection = new ReactiveCollection<int>((x, y) => { if (++filterCounter%3 == 0) testScheduler.Sleep(TimeSpan.FromMilliseconds(250).Ticks); return true; }, testScheduler) collection.Source = new int[10]; collection.PropertyChangedAsObservable(x => x.View) .Take(3) .Subscribe(x => Assert.IsTrue(collection.View.Length%3 == 0)); testScheduler.Start(); Assert.AreEqual(10, collection.View.Length); }
29
Тестирование Rx [Test] public void OnSourceChanged_CallFilter_EvenWithinThreshold() { var testScheduler = new TestScheduler(); var filterCounter = 0; var collection = new ReactiveCollection<int>((x, y) => { filterCounter++; return true; }, testScheduler) for (var i = 0; i < 10; i++) { collection.Source = new int[1]; testScheduler.AdvanceBy(TimeSpan.FromMilliseconds(100).Ticks); }
Assert.AreEqual(10, filterCounter); }
30
Тестирование Rx [Test] public void OnSameFilter_DoNotCallFilter() { var testScheduler = new TestScheduler(); var filterCounter = 0; var collection = new ReactiveCollection<int>((x, y) => { filterCounter++; return true; }, testScheduler) collection.Source = new int[1]; collection.Filter = "firstFilter"; testScheduler.AdvanceBy(TimeSpan.FromMilliseconds(250).Ticks); filterCounter = 0; for (var i = 0; i < 10; i++) { collection.Filter = "secondFilter"; testScheduler.AdvanceBy(TimeSpan.FromMilliseconds(150).Ticks); collection.Filter = "firstFilter"; testScheduler.AdvanceBy(TimeSpan.FromMilliseconds(250).Ticks); } Assert.AreEqual(0, filterCounter); }
31
Тестирование Rx [TestCase(100, false)] [TestCase(150, false)] [TestCase(250, true)] [TestCase(500, true)] public void OnMultipleChangesWithinThreshold_DoNotCallFilter(
int threshold, bool runFilter) { var testScheduler = new TestScheduler(); var filterCounter = 0; var collection = new ReactiveCollection<int>((x, y) => { filterCounter++; return true; }, testScheduler) {Source = new int[1]}; testScheduler.AdvanceBy(TimeSpan.FromMinutes(1).Ticks); filterCounter = 0; for (var i = 0; i < 10; i++) { collection.Filter += "filter"; testScheduler.AdvanceBy(TimeSpan.FromMilliseconds(threshold).Ticks); }
if (runFilter) Assert.AreNotEqual(0, filterCounter); else Assert.AreEqual(0, filterCounter); } 32