Artykuł w wersji anglojęzycznej jest dostępny na blogu firmy Grape Up
Jeden z produktów jakie ostatnio rozwijaliśmy w naszym zespole, jest aplikacją desktopową opartą o rozbudowaną bibliotekę, która dostarczała spójne API do komunikacji, opartej o różne protokoły związane z telekomunikacją, co sprawiało że generowała naprawdę dużo sygnałów.
Architektura biblioteki
Większość klas w tej bibliotece pasowała do takiego wzorca:
public class Class1 { public void SetDescription(string description, CompletionHandler handler) { /* Some logic here */ } public void SetSize(int size, CompletionHandler handler) { /* Some logic here */ } public string Description { get; private set; } public int Size { get; private set; } public event EventHandler DescriptionChanged; public event EventHandler SizeChanged; }
- Metody SetDescription i SetSize wysyłają wartość przekazaną jako pierwszy parametr i wywołują metodę przekazaną jako Handler gdy serwer potwierdzi albo odrzuci żądanie
- Właściwości Description i Size zwracają ostatnią znaną wartość
- Zdarzenia DescriptionChanged i SizeChanged są uruchamiane gdy serwer powiadomi aplikację o zmianie wartości – niezależnie czy zmiana była spowodowana przez wywołanie metody Set* czy przez inne interakcje z serwerem np. działanie drugiej aplikacji
Biblioteka przetwarza wszystkie żądania na swojej własnej wewnętrznej puli wątków co ma swoje zalety i wady. Podstawową zaletą był fakt iż biblioteka, sama z siebie, nie wpływała na działanie wątku głównego aplikacji. Wadą był fakt iż wszystkie callbacki i zdarzenia podnoszone przez bibliotekę były uruchamiane na innym wątku, co wymuszało opracowanie jakiegoś mechanizmu do komunikacji między wątkami.
Ręczne zarządzanie wątkami – Nie chcesz tego robić
Już od początku rozwoju aplikacji zorientowaliśmy się, że ręczna komunikacja między wątkami kończy się powstaniem dużej ilości długiego i powtarzalnego kodu, który wygląda mniej więcej tak:
public class SomeOtherClass : IDisposable { private readonly SomeClass _someObject; private readonly DispatcherService _dispatcherService; public SomeOtherClass( SomeClass someObject, DispatcherService dispatcherService) { _someObject = someObject; _dispatcherService = dispatcherService; _someObject.DescriptionChanged += SomeObjectOnDescriptionChanged; _someObject.SizeChanged += SomeObjectOnSizeChanged; } private void SomeObjectOnDescriptionChanged(object sender, EventArgs e) { _dispatcherService.Invoke(LoadState); } private void SomeObjectOnSizeChanged(object sender, EventArgs e) { _dispatcherService.Invoke(LoadState); } private void LoadState() { PropertyVisibleOnUI = $"Description: {_someObject.Description} Size: {_someObject.Size}"; } public string PropertyVisibleOnUI { get; set; } public void Dispose() { _someObject.DescriptionChanged -= SomeObjectOnDescriptionChanged; _someObject.SizeChanged -= SomeObjectOnSizeChanged; } }
Kod staje się jeszcze bardziej złożony jeżeli założymy, że niektóre właściwości klasy są złożonymi obiektami, które mają właściwości, zdarzenia, metody asynchroniczne. A takich warstw może być znacznie więcej.
API do obsługi wiadomości tekstowych napisane w tej konwencji mogło wyglądać tak:
public interface IConversationService { event EventHandler ConverstionStarted; event EventHandler ConversationEnded; IEnumerable Conversations { get; } } public interface IConversation { event EventHandler MessageAdded; void SendMessage(string body, CompletionHandler handler); IEnumerable Messages { get; } } public interface IMessage { event EventHandler IsReadChanged; bool IsMine { get; } string Body { get; } bool IsRead { get; } }
Koniecznie potrzebowaliśmy lepszego rozwiązania. W drugiej części artykułu opiszę jak wzorzec event-aggregator pomógł nam pokonać te problemy.
2 odpowiedzi do “Nasza droga do Reactive Extensions – cz. 1 – Wstęp”