Живые приложения с rx

32

Upload: gosharp

Post on 22-Aug-2015

101 views

Category:

Technology


3 download

TRANSCRIPT

Page 1: Живые приложения с Rx
Page 2: Живые приложения с Rx

Почему  Reac,ve  Extensions?  •  Отзывчивость  приложений  •  Упрощение  кода  работы  со  временем  •  Упрощение  управления  потоками  •  Упрощение  подписки-­‐отписки  для  событий  •  Тестируемость  кода  •  LINQ  для  событий  и  потоков  данных  •  Наличие  реализаций  для  других  языков  программирования  

2  

Page 3: Живые приложения с Rx

IEnumerable<T>  vs  IObservable<T>  

3  

Page 4: Живые приложения с Rx

Преимущества  Rx  перед  событиями  

•  События  однопоточны.  Все  обработчики  будут  вызываться  по  очереди  в  потоке  который  запустил  событие.  

•  Если  в  одном  из  обработчиков  события  будет  выброшено  исключение  все  остальные  обработчики  не  будут  вызваны  

•  Все  подписки  на  события  необходимо  отписывать.  Для  этого  придется  хранить  ссылку  на  метод  для  отписки  и  объект  

•  Rx  поддерживает  async/await  для  запросов  требующих  ожидания  конкретного  элемента  

•  Rx  предоставляет  методы  для  управления  временем  и  потоками,  а  также  тестовые  реализации  управляющих  объектов.    

•  Rx  предоставляет  методы  для  связи  нескольких  потоков  данных  в  один  

•  Rx  предоставляет  возможность  обрабатывать  данные  аналогично  LINQ-­‐запросу  в  функциональном  стиле.  

4  

Page 5: Живые приложения с Rx

Основные  интерфейсы  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  

Page 6: Живые приложения с Rx

Простой  класс  с  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  

Page 7: Живые приложения с Rx

Потоки  данных  в  LogoutManager  

 Logout  =  userActionsSubject                            .StartWith(Unit.Default)                            .Throttle(timeout)                            .Merge(logoutSubject);  

7  

Page 8: Живые приложения с Rx

Интеграция  Rx  с  существующими  событиями  

•  Короткая:  Observable.FromEventPattern(source,  "PropertyChanged");  

•  Типобезопасная:  Observable.FromEventPattern  

<PropertyChangedEventHandler,PropertyChangedEventArgs>(

             handler  =>  source.PropertyChanged  +=  handler,  

             handler  =>  source.PropertyChanged  -­‐=  handler)  

•  Промежуточные  варианты  с  указанием  типов  только  

для  части  операндов       8  

Page 9: Живые приложения с Rx

Текстовый  фильтр  на  Rx  

9  

Page 10: Живые приложения с Rx

Текстовый  фильтр  на  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  

Page 11: Живые приложения с Rx

Расширенный  фильтр  на  Rx  

•  Исходная  коллекция  также  может  меняться  •  Пока  пользователь  продолжает  ввод  не  имеет  смысла  

перезапускать  поиск  •  Логика  фильтрации  может  быть  достаточно  сложна  и  

отнимать  много  времени  •  Пользователь  должен  видеть  результаты  по  мере  их  

поступления,  а  не  после  завершения  фильтрации  •  Пользователь  может  начать  вводить  фильтр,  а  затем  

вернуть  старый.  Повторную  фильтрацию  при  этом  не  надо  запускать  

11  

Page 12: Живые приложения с Rx

Расширенный  фильтр  на  Rx  

 public  interface  IReactiveCollection<TItem>  :      

     INotifyPropertyChanged,  IDisposable

{

   //  Источник  данных  (полная  коллекция)                  TItem[]  Source  {  get;  set;  }                    //  Отображаемая  часть  данных  (отфильтрованная  коллекция)                  TItem[]  View  {  get;  }    

               //  Строка  фильтра                  string  Filter  {  get;  set;  }              }  

12  

Page 13: Живые приложения с Rx

Расширенный  фильтр  на  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  

Page 14: Живые приложения с Rx

Потоки  данных  в  расширенном  фильтре  

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  

Page 15: Живые приложения с Rx

Потоки  данных  в  расширенном  фильтре  

   .Select(_  =>  Source.EmptyIfNull()                            .ToObservable()                            .Where(x  =>  filterFunc(x,  Filter))                            .Buffer(TimeSpan.FromMilliseconds(200))                            .Do(x  =>  x.ForEach(buffer.Add)))                  .Switch()  

15  

Page 16: Живые приложения с Rx

Управление  потоками  в  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  

Page 17: Живые приложения с Rx

Применение  фильтра  на  Rx  

17  

Page 18: Живые приложения с Rx

Проблемы  реализации  при  помощи  событий  

•  Необходимость  реализации  задержки  для  событий  –  для  того,  чтобы  не  загружать  списки  клиентов  и  счетов  много  раз  

•  Постоянные  подписки  и  отписки  на  события  элементов  коллекции  клиентов,  отсутствие  отписок  быстро  приведет  к  утечке  памяти  в  приложении.  

•  Код  для  управления  потоками  –  фильтрация  в  отдельном  потоке,  вывод  результатов  в  другом  

18  

Page 19: Живые приложения с Rx

Применение  фильтра  на  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  

Page 20: Живые приложения с Rx

Применение  фильтра  на  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  

Page 21: Живые приложения с Rx

А  что  с  тестированием?  

•  Управление  временем  •  Управление  потоками  •  Буферизация    •  Скорость  выполнения  тестов  

21  

Page 22: Живые приложения с Rx

Тестирование  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  

Page 23: Живые приложения с Rx

Тестирование  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  

Page 24: Живые приложения с Rx

Тестирование  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  

Page 25: Живые приложения с Rx

Тестирование  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  

Page 26: Живые приложения с Rx

Тестирование  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  

Page 27: Живые приложения с Rx

Тестирование  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  

Page 28: Живые приложения с Rx

Тестирование  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  

Page 29: Живые приложения с Rx

Тестирование  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  

Page 30: Живые приложения с Rx

Тестирование  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  

Page 31: Живые приложения с Rx

Тестирование  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  

Page 32: Живые приложения с Rx

Тестирование  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