Architektura aplikacji a współdzielenie kodu między aplikacjami dla platformy desktopowej i mobilnej

Autorzy: Tomasz Soroka

04.09.2017
Tworzenie aplikacji wymaga połączenia twardej wiedzy analitycznej, programistycznej z  umiejętnością tworzenia zaawansowanych, lecz jednocześnie prostych w obsłudze  rozwiązań, gdzie końcowy użytkownik powie: „wow – ten produkt rozwiązuje moje  problemy”.  Niezależnie od tego, czy jesteś programistą, analitykiem czy odpowiadasz za stronę  biznesową projektu, musisz wiedzieć, że budowanie czegokolwiek ma sens tylko i wyłącznie
wtedy, gdy w centrum wykonywanych działań jest klient i jest to wspólna praca wszystkich  zaangażowanych osób. Kończą – a może już skończyły – się czasy, gdy developerzy tworzyli  rozwiązanie na podstawie specyfikacji, którą analityk przygotował bez porozumienia,  zapisanej na setkach stron dokumentacji. Nic odkrywczego? Niby tak, tylko że to już nie jest  agile, scrum, scrumban development… to połączenie świata biznesowego ze światem  technologii. To łączenie metodyk zwinnego budowania biznesu w metodykami zwinnego  wytwarzania oprogramowania. Nie ma agile bez lean, nie ma backlogu bez zweryfikowanych  hipotez... Nawet najlepsze zwinne podejście nie doprowadzi do końcowego sukcesu, jeżeli  budowana aplikacja będzie bazowała na wymaganiach, które pozostaną niezweryfikowanymi  hipotezami.

W niniejszym artykule opowiem, jak przygotować architekturę wieloplatformowej aplikacji,  która działa na urządzeniach mobilnych oraz desktopie. W obecnej wersji wpieramy platformy ios i android oraz Windows 7 (WPF). W przyszłości aplikacja będzie dostosowana  także do wymagań Windows 10 (UWP), korzystając z .NET Core oraz prawdopodobnie  postanie wersja na Mac Os z wykorzystaniem Xamarin.mac. Architektura ta nie powstałaby
prawidłowo, gdyby nie to, iż klient oraz zespół developerski cały czas skupiał się na tym, aby stworzyć rozwiązanie, które będzie zaspokajało potrzeby użytkowników – to w pierwszym punkcie. Następnie potrzeby klienta – właściciela produktu, a w ostatnim potrzeby zespołu
programistycznego – tak aby rozwój aplikacji mógł przebiegać długofalowo i płynnie. Aby to  osiągnąć, już na samym początku trzeba zadać sobie pytanie o to, co chcemy osiągnąć i w jaki sposób. Głównym zadaniem aplikacji CharCRM jest obsługa procesu zamawiania towarów z branży
optycznej. Ze względu na specyficzne know how oraz wymagania biznesowe, których nie  spełniało żadne z rozwiązań obecnych na rynku, klient zdecydował się na stworzenie dedykowanej aplikacji.

Jak prawidłowo powinniśmy zbierać wymagania biznesowe?

Podczas pierwszego spotkania z klientem otrzymaliśmy szereg ekranów, dla platformy iOS
iPad – rozrysowanych z bardzo dużym poziomem szczegółów. Na całym zespole zrobiło to
duże wrażenie. Właściciel projektu naprawdę się napracował.



Figure 1 Makieta z listą klientów



Figure 2 Makieta pokazująca proces dodawania okularów do zamówienia



Wszystko wydawało się w tym momencie bardzo proste – tworzymy dokumentację na
podstawie makiet, opisujemy działanie logiki i rozpoczynamy realizację projektu. Klient
bardzo dokładnie wie czego chce.
Podczas prac przenoszenia wiedzy utrwalonej na makietach na język, który będzie
zrozumiały dla programistów, okazało się, że logika działania aplikacji na niektórych ekranach
nie jest tak oczywista, że pewne procesy trwają dużo dłużej niż mogły by – przynajmniej
zdaniem analityka zaangażowanego w prace.
Wchodząc coraz bardziej w szczegóły, okazało się, że analizujemy bardzo duży projekt, z
wieloma elementami, które nie są szczegółowo opisane, i gdzieś po drodze gubimy
najważniejsze problemy docelowych użytkowników, które mamy zamiar rozwiązać.
Wchodzimy w bardzo duże szczegóły, pojawiają się pytania, na które nikt nie jest w stanie
udzielić odpowiedzi – jesteśmy zbyt daleko od użytkowników, od rynku.
Dlaczego to robimy? Jaki ma to sens? Zaczęły się rozmowy z właścicielem produktu i okazało
się, że myślał on, iż firma programistyczna potrzebuje tak wielu detali, aby dokładnie
oszacować koszt wykonania rozwiązania, tak aby aplikację można było stworzyć w modelu
sztywnego budżetu (fixed price). Wielu z nas wie, jak niewdzięczne i trudne to zadanie,
wymagające wykonania bardzo dużej ilości pracy, rozrysowania każdego ekranu, procesu na
poszczególne elementy i przyjęcia wielu założeń dotyczących tego jak aplikacja ma działać.
Po drugie co stanie się, gdy rozwiązanie, które stworzymy w ten sposób nie będzie
odpowiadało prawdziwym potrzebom użytkowników?
Na szczęście właściciel produktu jest oddanym zwolennikiem metod zwinnych i growth
hackingu – zaczęliśmy więc rozmawiać o tym, co powinniśmy zrobić, aby projekt ruszył, tak
aby zminimalizować wszelkie ryzyka po obu stronach . Zaczęliśmy działać jako zespół
skupiony tylko i wyłącznie na dostarczeniu wartości, rozwiązywaniu najważniejszych
problemów użytkowników przyszłego rozwiązania. I wiecie co? Okazało się, że przy
otwartym podejściu obu stron nie jest to wcale trudne.
Zaczęliśmy wspólnie analizować zidentyfikowane problemy klientów i w ten sposób powstał
zakres prac składający się na MVP (ang. Minimum Viable Product), z którym Product Owner
może zacząć testować aplikację wraz z pierwszymi użytkownikami. Czym jest MVP? Często
ludzie nie rozumieją prawdziwego znaczenia tych słów. Budujemy MVP tylko po to, aby
osiągnąć konkretny, założony, osiągalny cel, tak aby móc podjąć decyzję co do kolejnych
kroków – tworzyć minimalny przyrost, badać go z użytkownikami, którzy będą korzystać z
aplikacji, wyciągać wnioski i … tak w kółko aż osiągniemy zadowalający efekt.

MVP
W momencie zdefiniowania MVP okazało się, że aplikacja nie powstanie na platformę iOS
Ipad. Okazało się, po wstępnej analizie i weryfikacji rozwiązania z użytkownikami, że
powinniśmy skupić się na wersji na smartfony z systemem operacyjnym Android –
najczęściej używanym przez przyszłych mobilnych użytkowników, oraz wykonaniu aplikacji
desktop wspierającej Windows 7, z której będą korzystać pracownicy biurowi oraz kadra
zarządzająca. Wybór platformy Windows 7 nie był przypadkowy – polityka firmy zakłada
wsparcie tego systemu przez kilka kolejnych lat. Co więcej – powinniśmy zacząć od wersji
desktop, ze względu na lepiej zidentyfikowane problemy po stronie pracowników biurowych.
Odkrycie tego faktu, było bardzo ważne – udało się tego dokonać tylko i wyłącznie dlatego,
że właściciel produktu zaczął bardzo mocno pracować z pierwszymi użytkownikami aplikacji i
weryfikować z nimi hipotezy, które wcześniej razem zdefiniowaliśmy.
Wkrótce także okazało się, że powinniśmy skupić się na kilku podstawowych
funkcjonalnościach, na których najbardziej zależy pierwszym użytkownikom – prostym
weryfikowaniu i analizowaniu danych, które spływać będą z telemetrii danych obecnie
używanych w terenie. Szybko okazało się, że z pozostałymi słabo działającymi
funkcjonalnościami uwzględnionymi w MVP użytkownicy mogą spokojnie żyć, bez tej
najważniejszej – bardzo dobrze wykonanej i dopracowanej aplikacja nie ma żadnego sensu.
Kolejnym wymogiem było to, aby aplikacja w przyszłości mogła zostać przeniesiona na
platformy mobilne – ale dopiero gdy będzie działać prawidłowo na desktopie, tak aby
zastąpić telemetrią i udostępnić część funkcjonalności z wersji desktopowej użytkownikom
mobilnym. Zwróciliśmy uwagę na to, że odkryliśmy ważne wymaganie, które z punktu
widzenia użytkownika nie zostało zdefiniowane – wykonajmy aplikację na platformy mobilne
w taki sposób, aby wykorzystała kod aplikacji desktopowej, z dokładnością do tych
funkcjonalności, które pokrywają się między platformami.

Jak rozsądnie rozwijać multiplatformową aplikację, aby
ograniczać koszty i minimalizować problemy?
Bardzo szybko uświadomiliśmy sobie, że prawidłowa architektura będzie bardzo istotnym
elementem w powodzeniu całego przedsięwzięcia. Założyliśmy, że musimy wybrać
technologie, które pozwolą na długofalowy rozwój całego rozwiązania w następnych latach,
jednocześnie minimalizując koszty z tym związane. Klient chciał uniknąć sytuacji, w której
tworzone rozwiązanie składa się z wielu komponentów, które nie są ze sobą powiązane – np.
aplikacja mobilna stworzona w technologiach natywnych w języku Objective-c, podczas gdy
aplikacja desktopowa wykonana jest w technologiach Microsoft.
Wiadomo, że bardzo trudno rozwija i utrzymuje się takie projekty, gdyż w takim przypadku
każdy element musi być rozwijany przez osobny zespół developerów, każda funkcja
implementowana i utrzymywana osobno, a w sumie od strony funkcjonalnej musimy
uzyskać ten sam efekt.

Projekt od strony funkcjonalnej cały czas był na etapie MVP, ale od strony architektury
mogliśmy założyć dużo więcej. Chcielibyśmy wybrać najlepsze rozwiązania, tak aby w
pewnym momencie nie okazało się, że stoimy pod ścianą i z jakiegoś powodu trzeba
refaktoryzować 70% kodu.
Z tego względu odrzuciliśmy technologie web czy hybrydowe do jego stworzenia.
Wiedzieliśmy, że proces analizy danych jest na tyle skomplikowany, że aplikacja hybrydowa,
a co gorsza webowa w przypadku urządzeń mobilnych nie pozwoli na tak zaawansowaną
interakcję i użyteczność, aby stworzyć rozwiązanie, które pokochają użytkownicy.

Architektura rozwiązania
Pytanie, na które musieliśmy znaleźć odpowiedź, to czy możliwe jest współdzielenie kodu
między natywną aplikacją desktopową a aplikacją mobilną?
Odpowiedzią na nie było użycie odpowiedniego frameworka. Zdecydowaliśmy się na
wykonanie aplikacji, korzystając z wzorca MVVM, który jest domyślnym dla WPF. Integruje
się on bardzo prosto z MVVMCross, który był kluczem do tego aby aplikacja działała na
platformach mobilnych. Dodatkowo technologia WPF bardzo ułatwia migrację w przyszłości
do UWP, tak aby w pełni wspierać wymagania Windows 10. W projekcie bardzo szeroko
wykorzystujemy biblioteki PCL, które ułatwiają development.

MVVM w bardzo ładny sposób porządkuje architekturę i powoduje, że aplikacja jest
rozwijana w logiczny sposób. MvvmCross pozwala na współdzielenie kodu między
platformami – także mobilnymi. Poniższy schemat pokazuje, jak wyglądała architektura na
bardzo wysokim poziomie.



Figure 3 Architektura stworzonego rozwiązania

Backend został przygotowany w taki sposób, aby był gotowy na skalowanie w przyszłości.
Stworzyliśmy wspólne api, które integrowało wszystkie wymagane kompnenty.
Dodatkowo udało nam się wyodrębnić sporą część wspólnego kodu – kodu, który jest
identyczny zarówno dla aplikacji Desktop – w naszym wypadku WPF, a także dla platform
mobilnych – Android i w przyszłości iOS.
Do stworzenia aplikacji na platformy mobilne skorzystaliśmy z technologii Xamarin, która jest
kompatybilna z wzorcem MVVM i bardzo dobrze wspiera MvvmCross, co udało nam się
sprawdzić w boju w wielu dużych projektach dotyczących aplikacji mobilnych. Głównym
powodem wykorzystania tej technologii było połączenie wymagania działania na wielu
platformach mobilnych oraz tego aby aplikacja była natywna.

Ogólne założenie współdzielenia kodu opiera się na tym, że wszystko do poziomu view
modelu jest takie same dla każdej z platform. Interfejs użytkownika tworzymy osobno dla
każdej z platform, czyli wykorzystując WPF dla Windows desktop, UWP dla Windows 10,
natywny interfejs dla Android czy iOS. Bardzo ciekawe jest to iż deklaracje XAML są bardzo
podobne we wszystkich wymienionych platformach, co bardzo ułatwiło prace nad
rozwiązaniem.

Kolejnym celem w projektowaniu architektury było takie dobranie komponentów i takie ich
umiejscowienie, aby nie duplikować kodu czy logiki biznesowej w całym projekcie.
Chcieliśmy maksymalnie dużo rzeczy umieścić w backendzie oraz w kodzie współdzielonym.
Dzięki temu:
 Jeżeli potrzebujemy zmienić funkcjonalność, zmieniamy ją w jednym miejscu.
 Jeżeli pojawia się błąd – wystarczy naprawić go w części wspólnej, przekompilować
aplikacje na każdą z platform i przetestować.
 Większość unit testów dotyczy testowania na poziomie backendu lub części wspólnej

Podczas trwania projektu spotkaliśmy się z kilkoma wyzwaniami, które były głęboko
związane z tym, w jaki sposób miała działać aplikacja.
SYNCHRONIZACJA DANYCH
Dużym wyzwaniem okazał się sposób synchronizacji danych w całej aplikacji – urządzenia
miały mieć możliwość synchronizacji z każdego miejsca na świecie, a dodatkowo wszystkie
operacje wykonywane przez innych użytkowników z tej samej firmy miały być dostępne w
„real time” dla każdego. Wykorzystaliśmy do tego celu rozwiązanie cloudowe (tzw. chmurę),
gdzie umieściliśmy zaimplementowane wcześniej API, które odpowiedzialne jest za całą
logikę systemu wraz z połączeniem do bazy danych oraz innych serwisów i dostawców
zintegrowanych w aplikacji.
We względu na wielkość aplikacji i stopnia skomplikowania procesów w niej zachodzących
postawiliśmy na sprawdzone rozwiązania – API zostało stworzone przy użyciu technologii
ASP.NET WebAPI, oraz relacyjną bazę danych opartą na MS SQL. W całym rozwiązaniu
brakowało tylko jednego, bardzo istotnego komponentu – wywołań asynchronicznych ze
strony serwera dla dedykowanej grupy użytkowników, które miałyby informować ich o
zmianie danych podczas wykonywanych operacji przez innych. Tutaj z pomocą przyszedł nam
znany i sprawdzony SignalR, który pozwolił nam aktualizować dane użytkowników w oparciu
o dane pobrane z serwera tylko w sytuacjach, kiedy faktycznie było to konieczne. Atutem
tego rozwiązania była biblioteka kliencka dla SignalR, która również została wykorzystana dla
wszystkich platform – cała implementacja znalazła się w Corze, dzięki czemu po stronie
platformowej musieliśmy obsłużyć tylko odpowiednie zdarzenia i zaktualizować widoki.

ZEWNĘTRZNE SERWISY
Oprócz standardowych rozwiązań technicznych aplikacja musiała korzystać również z
zewnętrznych serwisów, które umożliwiały kupowanie subskrypcji produktu, wysyłania
smsów oraz maili wraz z odpowiednim rejestrowaniem wszystkich operacji dla danego
konta. Dla subskrypcji produktu postawiliśmy na system Recurly, który w prosty i przejrzysty
sposób pozwala na zarządzanie własnym kontem dla użytkowników z poziomu aplikacji

desktopowej. To rozwiązanie było podyktowane również modelem biznesowym, na którym
opiera się aplikacja – klienci płacą za kolejnych użytkowników w firmie i/lub za każdy wysłany
sms. Ponieważ wiedzieliśmy, jaka będzie przyszła skala projektu, postanowiliśmy przenieść
obsługę sms oraz email do sprawdzonych serwisów – dla pierwszego wybraliśmy Twilio,
który w pełni odpowiadał naszym oczekiwaniom. Możemy tworzyć subkonta dla każdego
nowego klienta, co istotnie wpływa na kontrolę wydatków przy wysyłaniu dużej ilości sms.
Do wysyłania masowej ilości wiadomości elektronicznych (e-mail) wykorzystaliśmy
sprawdzony już wcześniej system SendGrid, który dodatkowo został skonfigurowany, aby
korzystać z domen klienta – oznacza to tyle, że przychodzące wiadomości email mają
przekierowanie na domenę klienta (erpos.com), co dodatkowo pozytywnie wpływa na
aspekt postrzegania technicznej strony całego projektu.
Oczywiście – korzystając z rozwiązań chmurowych, trzeba liczyć się z przyszłym obciążeniem
serwera, ilością generowanych zapytań przez klientów, zależnościami do bazy danych, a
także kosztami utrzymania tej infrastruktury. Problem polegał na tym, że chcąc mieć zawsze
aktualne dane dla każdego użytkownika aplikacji, liczba zapytań do serwera wzrastała w
wykładniczym tempie – musieliśmy coś z tym zrobić. W tym celu konieczna okazała się
modyfikacja części architektury odpowiadającej za pobieranie danych z API, aby zredukować
ilość wysyłanych zapytań do minimum, otrzymując jednocześnie pełne dane konieczne do
synchronizacji. Takie rozwiązanie pozwoliło zmniejszyć liczbę ruchu na serwerze ponad 10-
krotnie!
Istotnym problemem przy aplikacjach opartych o chmurę jest… połączenie z Internetem.
Również w tym przypadku wystąpiły problemy ze stabilnością sieci oraz częstym
przerywaniem sygnału. Chcąc dostarczyć użytkownikowi aplikację, która będzie potrafiła
radzić sobie z tego typu przeszkodami, musieliśmy zaimplementować klika mechanizmów
działających w tle, które na bieżąco analizują sytuację stabilności połączenia z serwerem i w
przypadku problemów automatycznie próbują nawiązać połączenie oraz zsynchronizować
dane bez konieczności ingerencji użytkownika. Dodatkowym wyzwaniem była obsługa stanu
uśpienia i zawieszenia aplikacji lub urządzenia oraz ponownego przywrócenia wraz z
najświeższymi danymi – tutaj z pomocą również przyszła obsługa odpowiednich zdarzeń
systemowych i ponownie praca „backgroundowych” mechanizmów pozwalających na
przyjemne korzystanie z aplikacji.

Zadowoleni użytkownicy – czyli co mogliśmy zrobić lepiej w
projekcie?

Bardzo szybko okazało się, że podejście zwinne pomogło przyśpieszyć prace developerskie –
na bieżąco reagowaliśmy na informacje od early adoptersów. Informacja zwrotna była tak
pozytywna, że bardzo szybko zaczęły być dodawane nowe funkcjonalności, które nie były
zaplanowane w zakresie pierwszej wersji MVP. Zaczęliśmy gubić balans pomiędzy
dodawaniem funkcjonalności – o które prosili użytkownicy – a utrzymaniem stabilności
rozwiązania. Na szczęście bardzo szybko udało się to skorygować i do tygodniowych cykli
dodaliśmy także czas potrzebny na stabilizację kodu i wydawanie kolejnych wersji

równolegle do prowadzonych prac związanych z rozwojem funkcjonalności aplikacji. Dzięki
takiemu podejściu okazało się, iż z wersji MVP w bardzo płynny sposób przeszliśmy do
wersji, która została pełnoprawną wersją produkcyjną, którą zaczęliśmy wdrażać na szerszą
skalę.
Kluczowym elementem, który spowodował, że odnieśliśmy sukces było połączenie metodyki
zwinnego wytwarzania oprogramowania z metodyką lean startup, która pozwoliła nam na
szybką weryfikację, czy rozwiązujemy prawdziwe problemy użytkowników w taki sposób aby
dostarczone rozwiązanie było tym czego naprawdę potrzebują. Wybranie technologii
Xamarin i współdzielenie kodu między platformami ułatwiło nam wykonywanie szybkich
zmian w kodzie aplikacji desktopowej i mobilnej.