Dobre praktyki w projektowaniu aplikacji mobilnych Arkadiusz Waśniewski
Wprowadzenie Spróbujemy odpowiedzieć na pytanie jak powinna wyglądać dobrze napisana aplikacja dla platformy.NET Compact Framework Główne pole zainteresowań to wzorce projektowe (design patterns)
Porządek spotkania (50 minut) Przedstawienie założeń Opis szkieletu aplikacji Tworzenie obiektów Dostęp do danych Obsługa formularzy Przykładowe rozwiązanie
Terminologia (testy) Stub – klasa zawierająca metody, które nic nie robią. Główne zadanie takiej klasy to umożliwienie kompilacji programu Fake – klasa zawierająca metody, które zwracają ściśle określone wartości, np. wpisane na sztywno w kod klasy Mock – klasa, dla której możemy określić jakie metody czy właściwości mogą być wywoływane, jakie wartości mają być przyjmowane i zwracane
Założenia Napisać lub zanalizować aplikację mobilną Aplikacja składa się z wielu formularzy Jeden formularz może mieć różne zastosowanie (np. formularz z DataGrid) Dane składowane są w zewnętrznym pliku lub plikach na urządzeniu mobilnym Do operowania na danych mamy dedykowany silnik bazodanowy
Założenia c.d. Ilości danych, które wykorzystujemy są rzędu setek lub tysięcy rekordów Aplikacja musi być wydajna i możliwie łatwa w modyfikacji Istnieje konieczność posiadania wielu wersji dla różnych klientów Aplikacja ma działać pod Windows Mobile for Pocket PC i Windows CE
Konsekwencje założeń Formularze wielokrotnie używane powinny być umieszczone w pamięci podręcznej Każdy z wybranych systemów operacyjnych musi mieć własny zestaw formularzy ze względu na duże różnice w sposobie prezentacji
Konsekwencje założeń c.d. Rezygnujemy z przechowywania danych zewnętrznych w plikach XML (dobra wydajność jedynie do rozmiaru kilku KB) Rezygnujemy z wykorzystania wewnętrznie obiektu DataSet (wydajność) Dane, na których będzie operować aplikacja będą odwzorowane w obiekty (encje) i kolekcje obiektów
Szkielet aplikacji Szukamy rozwiązania, które umożliwi odseparowanie formularzy od reszty aplikacji. Jako podstawę rozważań przyjmujemy dwa podstawowe w tej dziedzinie wzorce jakimi są Model-View- Controller oraz Model-View-Presenter
Model-View-Controller Model – odpowiedzialny za logikę i stany biznesowe View – będący warstwą prezentacji Controller – odpowiedzialny za sterowanie przepływem
Model-View-Presenter Model – odpowiedzialny za logikę i stany biznesowe View – będący warstwą prezentacji Presenter – będący mediatorem pomiędzy widokiem a modelem
MVC a MVP We wzorcu MVC widok informuje kontroler o zdarzeniu. Kontroler wywołuje metody modelu, który informuje widok o zmianach We wzorcu MVP widok komunikuje się tylko w prezenterem, który wykonuje żądania korzystając z metod modelu
Jaki wzorzec wybieramy? Model-View-Presenter wzbogacony o klasy obsługujące konkretne przypadki użycia
Tworzenie obiektów Słowo kluczowe new Metoda fabryki, fabryka abstrakcyjna Registry Singleton Inversion of Control oraz Dependency Injection Service Locator
Inversion of Control i Dependency Injection Tworzenie instancji zleca się obiektowi (kontenerowi), który zna zależności pomiędzy klasami. Zazwyczaj powiązania te definiuje się w plikach konfiguracyjnych w formacie XML Mobile Composite UI Application Block wraz z Mobile ObjectBuilder firmy Microsoft opisuje zależności korzystając z atrybutów
Dependency Injection Obiekty zależne oznaczane są dla tej przykładowej implementacji atrybutami public SelectCustomerPresenter( [ServiceDependency] ShellService shell, [ServiceDependency] ICustomerRepository customerRepository) { this.shell = shell; this.customerRepository = customerRepository; } SelectCustomerPresenter presenter = WorkItem.Items.AddNew (); Utworzenie nowej instancji klasy
Service Locator Oparty o wzorzec Singleton Dostarcza obiekt umiejący odnaleźć dowolną usługę wykorzystywaną przez aplikację Może być statyczny lub dynamiczny
Service Locator c.d. class ObjectLocator { private BusinessEntityFactory entities; private RepositoryFactory repositories; private ObjectDictionary services; private TypedDictionary views; private IViewManager viewManager; #region Wzorzec Singleton private static readonly ObjectLocator instance = new ObjectLocator(); private ObjectLocator() { entities = new BusinessEntityFactory(); repositories = new RepositoryFactory(); services = new ObjectDictionary(); views = new TypedDictionary (); viewManager = new FormViewManager(); } #endregion
Service Locator c.d. public static BusinessEntityFactory Entities { get { return instance.entities; } } public static RepositoryFactory Repositories { get { return instance.repositories; } } public static ObjectDictionary Services { get { return instance.services; } } public static TypedDictionary Views { get { return instance.views; } } public static IViewManager ViewManager { get { return instance.viewManager; } }
Dostęp do danych Bridge – wzorzec mostu, którego zadaniem jest usunięcie powiązań pomiędzy abstrakcją (interfejsem obiektu) a implementacją Umożliwia podpięcie różnych silników baz danych Umożliwia testowanie bez konieczności posiadania rzeczywistej bazy danych
Dostęp do danych c.d. interface IRepository { } interface IRepository : IRepository { EntityList GetList(); } class CustomerRepository : IRepository { public EntityList GetList() { EntityList list = new EntityList (); string sql = "SELECT Id, Code, Barcode, Name1, Name2, " + "LocationId, TaxNumber, StatisticNumber, CustomerBranchId, " + "CustomerCategoryId, CustomerGroupId, Phone1, Phone2, Fax, " + " , Web, Description, IsActive FROM Customer";... return list; }
Dostęp do danych – Bridge interface IDataService { EntityList GetList(); } class CustomerRepository : IRepository { private readonly IDataService provider; public CustomerRepository(IDataService provider) { this.provider = provider; } public EntityList GetList() { return provider.GetList(); } Możemy również zdefiniować domyślny konstruktor korzystający z Service Locator
Dostęp do danych – Bridge class Repository : IRepository { private readonly IDataService provider; public Repository(IDataService provider) { this.provider = provider; } public EntityList GetList() { return provider.GetList(); } class CustomerRepository : Repository { public CustomerRepository(IDataService provider) : base(provider) { }
Dostęp do danych – wywołanie Warianty wywołania repozytorium przy wykorzystaniu wzorca Service Locator IRepository repository = ObjectLocator.Repositories.GetCustomerRepository(); IRepository repository = ObjectLocator.Repositories.Get >();
Formularze Proces tworzenia formularza powoduje odczuwalne dla użytkownika opóźnienia zwłaszcza jeśli konieczne jest załadowanie lub przygotowanie danych Formularze wielokrotnie wykorzystywane muszą mieć odpowiednio utworzony lub odtworzony stan
Formularze c.d. Wyświetlenie formularza może odbywać się na dwa sposoby: metodą Show() lub ShowDialog() Aktywowanie formularza niemodalnego wywołującego formularz modalny! Jak wyświetlić formularz wielokrotnego zastosowania przy pomocy ShowDialog() tak aby ekran nie migotał
Przykład
Przykład – założenia Definiujemy interfejs wspólny dla wszystkich widoków Każdy interfejs widoku wie jaki prezenter go obsługuje Widoku są rejestrowane w systemie w powiązaniu z interfejsami, które implementują Każdy prezenter wie, z jakiego interfejsu widoku będzie korzystać (wyświetlanie!)
Przykład – założenia c.d. Każdy prezenter posiada skojarzony ze sobą interfejs umożliwiający dowolnemu kontrolerowi zarządzanie prezenterem (w ramach dowolnego przypadku użycia) Interfejsy implementowane przez kontroler nie powinny być widoczne dla obiektów wywołujących kontroler Wyświetlaniem widokami zarządca odpowiedni obiekt
Interfejs bazowy widoku Lifetime – czas życia widoku Presenter – obiekt kontrolujący widok interface IView { string Title { set; } Lifetime Lifetime { get; set; } Presenter Presenter { get; set; }
Przykładowy interfejs widoku interface ILoginView : IBaseView { string Username { get; set; } string Password { get; set; } void FocusOnUsername(); void FocusOnPassword(); void ShowErrorMessage(string message); }
Rejestracja widoków public override void AddViews() { #if ((PocketPC || WindowsCE || Smartphone)) ObjectLocator.Views.AddNew (); ObjectLocator.Views.Add ( Lifetime.SingleCall); ObjectLocator.Views.Add ( Lifetime.SingleCall); ObjectLocator.Views.Add ( Lifetime.SingleCall); ObjectLocator.Views.Add ( Lifetime.SingleCall); ObjectLocator.Views.AddNew (); #endif }
Przykładowy prezenter sealed class LoginPresenter : BasePresenter { public LoginPresenter(ILoginPresenterController controller) : base(controller) {} protected override void OnInitialize() { base.OnInitialize(); View.Title = "Logowanie do programu"; Command loginCommand = new Command("Zaloguj", this.LogIn); Command closeCommand = new Command("Zamknij", Controller.OnCancel); View.AddActionCommand(loginCommand); View.AddActionCommand(closeCommand); View.Username = string.Empty; View.Password = string.Empty; View.FocusOnUsername(); } private void LogIn() {... } }
Przykładowy interfejs prezentera dla kontrolera interface ILoginPresenterController : IPresenterController { void OnCancel(); void OnLogIn(); }
Przykładowa implementacja interfejsu prezentera Poniższy przykład wykorzystuje implementację jawną (explicitly) w odróżnieniu od niejawnej (implicitly). Dzięki temu obiekt wywołujący kontroler widzi jedynie metody publiczne lub wewnętrzne tegoż kontrolera #region ILoginPresenterController Members void ILoginPresenterController.OnCancel() { ObjectLocator.ViewManager.Exit(); } void ILoginPresenterController.OnLogIn() { ObjectLocator.ViewManager.Show(this.Items.Get ()); } #endregion
Formularze – interfejs klasy zarządzającej interface IViewManager { string Title { set; } TPresenter Display (TPresenter presenter, params object[] parameters) where TPresenter : Presenter; TPresenter Display (TPresenter presenter, Action action, params object[] parameters) where TPresenter : Presenter; void Exit(); TPresenter Show (TPresenter presenter, params object[] parameters) where TPresenter : Presenter; TPresenter Show (TPresenter presenter, Action action, params object[] parameters) where TPresenter : Presenter; }
Przykładowy test [Test] public void LogInWithCorrentUsernameAndPassword() { DynamicMock controllerMock = new DynamicMock( typeof(ILoginPresenterController)); DynamicMock viewMock = new DynamicMock( typeof(ILoginView)); LoginPresenter presenter = new LoginPresenter( (ILoginPresenterController)controllerMock.MockInstance, (ILoginView)viewMock.MockInstance); viewMock.ExpectAndReturn("get_Username", "admin"); viewMock.ExpectAndReturn("get_Password", "1415"); controllerMock.Expect("OnLogIn"); base.InvokeMethod(presenter, "LogIn"); viewMock.Verify(); controllerMock.Verify(); }
Więcej informacji Driven-Design-Patterns- Examples/dp/
Dziękuję za uwagę Można zadawać pytania...