Dobre praktyki w projektowaniu
aplikacji mobilnych
Arkadiusz Waśniewski
arkadiusz.wasniewski@data.pl
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-ViewController 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;
}
Utworzenie nowej instancji klasy
SelectCustomerPresenter presenter =
WorkItem.Items.AddNew<SelectCustomerPresenter>();
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<IView> 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<IView>();
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<IView> 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<T> : IRepository
{
EntityList<T> GetList();
}
class CustomerRepository : IRepository<Customer>
{
public EntityList<Customer> GetList()
{
EntityList<Customer> list = new EntityList<Customer>();
string sql = "SELECT Id, Code, Barcode, Name1, Name2, " +
"LocationId, TaxNumber, StatisticNumber, CustomerBranchId, " +
"CustomerCategoryId, CustomerGroupId, Phone1, Phone2, Fax, " +
"Email, Web, Description, IsActive FROM Customer";
...
return list;
}
}
Dostęp do danych – Bridge
Możemy również zdefiniować domyślny
konstruktor korzystający z Service Locator
interface IDataService<T>
{
EntityList<T> GetList();
}
class CustomerRepository : IRepository<Customer>
{
private readonly IDataService<Customer> provider;
public CustomerRepository(IDataService<Customer> provider)
{
this.provider = provider;
}
public EntityList<Customer> GetList()
{
return provider.GetList();
}
}
Dostęp do danych – Bridge
class Repository<T> : IRepository<T>
{
private readonly IDataService<T> provider;
public Repository(IDataService<T> provider)
{
this.provider = provider;
}
public EntityList<T> GetList()
{
return provider.GetList();
}
}
class CustomerRepository : Repository<Customer>
{
public CustomerRepository(IDataService<Customer> provider)
: base(provider)
{
}
}
Dostęp do danych – wywołanie
Warianty wywołania repozytorium przy
wykorzystaniu wzorca Service Locator
IRepository<Customer> repository =
ObjectLocator.Repositories.GetCustomerRepository();
IRepository<Customer> repository =
ObjectLocator.Repositories.Get<IRepository<Customer>>();
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<LoginPresenter>
{
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<IDataGridView, DataGridForm>();
ObjectLocator.Views.AddNew<ICustomerDetailView, CustomerDetailForm>();
ObjectLocator.Views.Add<IEmailAccountDetailView, EmailAccountDetailForm>(
Lifetime.SingleCall);
ObjectLocator.Views.Add<IEmailDetailView, EmailDetailForm>(
Lifetime.SingleCall);
ObjectLocator.Views.Add<IHtmlView, HtmlForm>(
Lifetime.SingleCall);
ObjectLocator.Views.Add<ILoginView, LoginForm>(
Lifetime.SingleCall);
ObjectLocator.Views.AddNew<IMenuView, MenuForm>();
ObjectLocator.Views.AddNew<IProductDetailView, ProductDetailForm>();
#endif
}
Przykładowy prezenter
sealed class LoginPresenter : BasePresenter<ILoginView,
ILoginPresenterController>
{
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<MainMenuPresenter>());
}
#endregion
Formularze – interfejs klasy
zarządzającej
interface IViewManager
{
string Title
{
set;
}
TPresenter Display<TPresenter>(TPresenter presenter,
params object[] parameters)
where TPresenter : Presenter;
TPresenter Display<TPresenter>(TPresenter presenter,
Action<TPresenter> action, params object[] parameters)
where TPresenter : Presenter;
void Exit();
TPresenter Show<TPresenter>(TPresenter presenter,
params object[] parameters)
where TPresenter : Presenter;
TPresenter Show<TPresenter>(TPresenter presenter,
Action<TPresenter> 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
http://www.codeplex.com/smartclient
http://www.dofactory.com
http://codebetter.com/blogs/jeremy.miller
http://www.springframework.net/
http://martinfowler.com/articles/injection.html
http://martinfowler.com/eaaDev/uiArchs.html
http://www.amazon.com/Applying-DomainDriven-Design-PatternsExamples/dp/0321268202
Dziękuję za uwagę
Można zadawać pytania...