Michał Charmas
Language: Polish
Pisanie dobrego oprogramowania na platformę Android jest trudnym zadaniem.
Jednym z dużych problemów, zwłaszcza w przypadku sporych aplikacji, może być podział
logiki aplikacji tak, aby nasze Activity czy Fragmenty nie były nią przeładowane
oraz aplikacja była podatna na testowanie jednostkowe. Szukając pomysłu na
architekturę aplikacji, która będzie dobrze się skalowała wraz z rozwojem projektu,
natknąłem się na Clean Architecture zaproponowaną przez Boba C. Martina.
Podczas prezentacji zobaczymy czy i jak CA sprawdza się w przypadku
aplikacji mobilnych na Androida i na co pozwala jej zastosowanie. Oczywiście nie pominiemy
takich kluczowych kwestii jak pogodzenie tego wszystkiego z wszechobecną na Androidzie
asynchronicznością.
5. • Kwestie techniczne / optymalizacyjne - raczej łatwe i dobrze opisane
• Jak żyć?
• jak bezboleśnie wprowadzać zmiany w aplikacji?
• jak nie psuć wcześniej działających ficzerów wprowadzonymi zmianami?
• jak testować?
• jak dzielić odpowiedzialność?
• jak osiągnąć mniejszy coupling?
• jakich patternów / biliotek używac żeby to osiągnąć
8. –Android Developers Blog
„…the other primary goal is to serve as a practical
example of best practices for Android app design
and development.”
9. • Gdzie zasada pojedynczej odpowiedzialności?
• Logika domenowa w UI
• Logika UI pomieszana z asynchronicznym pobieraniem danych
• Callbacks everywhere
• Mapowanie kursorów na obiekty biznesowe w UI
• Activity i Fragmenty po 1000+ linii
• Całkowite uzależnienie od frameworka (import android.*)
• Testy?
10. A
2
B
4
A
2
B
4
A
2
B
4
2 * 4 = 8
2 + 4 = 6
*
* J.B. Rainsberger - Integrated Tests Are A Scam (https://vimeo.com/80533536)
16. • Niezależna od frameworka.
• Niezależna od interfejsu użytkownika.
• Niezależna od bazy danych.
• Testowalna - w oderwaniu od frameworka / bazy
danych / serwera.
17.
18. public class Product {
private final long id;
private final String name;
private boolean isBought;
public Product(long id, String name, boolean isBought){
this.id = id;
this.name = name;
this.isBought = isBought;
}
public void markBought() {
this.isBought = true;
}
public void markNotBought() {
this.isBought = false;
}
//getters
}
@Test public void testShouldBeBoughtWhenMarkedAsBought() throws Exception {
Product product = new Product(0, "sample name", false);
product.markBought();
assertEquals(true, product.isBought());
}
19. public class ProductList implements Iterable<Product> {
public Product addProduct(String productName) {
}
public int removeBoughtProducts() {
}
public Product getProduct(long id) {
}
public int size() {
}
@Override public Iterator<Product> iterator() {
}
}
20. –Robert C. Martin
„A good architecture emphasizes the use-cases and
decouples them from peripheral concerns.”
21. • UseCases:
• AddProduct
• ListProducts
• ChangeProductBoughtStatus
• RemoveAllBoughtProducts
public
interface
UseCase<Result,
Argument>
{
Result
execute(Argument
arg)
throws
Exception;
}
public
interface
UseCaseArgumentless<Result>
{
Result
execute()
throws
Exception;
}
22. public
class
AddProductUseCaseTest
{
private
AddProductUseCase
useCase;
@Before
public
void
setUp()
throws
Exception
{
useCase
=
new
AddProductUseCase();
}
@Test
public
void
testShouldAddProduct()
throws
Exception
{
useCase.execute("Sample
product");
//TODO:
verify
saved
}
}
23. public interface ProductsDataSource {
ProductList getProductList();
void saveProductList(ProductList products);
}
public class AddProductUseCaseTest {
private static final String PRODUCT_NAME = "Sample product";
@Mock ProductsDataSource productsDataSourceMock;
@Mock ProductList mockProducts;
private AddProductUseCase useCase;
@Before public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
useCase = new AddProductUseCase(productsDataSourceMock);
}
@Test public void testShouldAddProduct() throws Exception {
when(productsDataSourceMock.getProductList()).thenReturn(mockProducts);
useCase.execute(PRODUCT_NAME);
verify(mockProducts, times(1)).addProduct(PRODUCT_NAME);
verify(productsDataSourceMock).saveProductList(mockProducts);
}
}
24. public class AddProductUseCase implements UseCase<Product, String> {
private final ProductsDataSource productsDataSource;
@Inject
public AddProductUseCase(ProductsDataSource productsDataSource) {
this.productsDataSource = productsDataSource;
}
@Override
public Product execute(final String productName) {
if (productName == null || productName.trim().isEmpty()) {
throw new ValidationException("Product name cannot be empty");
}
ProductList productList = productsDataSource.getProductList();
Product product = productList.addProduct(productName);
productsDataSource.saveProductList(productList);
return product;
}
}
25. • całkowicie niezależne
od frameworka
• pure Java
• może zostać
wyciągnięte do
oddzielnego modułu -
czysto javowego
27. • Domena nie wie o sposobie utrwalania danych.
• Sposób utrwalania danych nie powinien operować na
obiektach domeny (wyciekająca abstrakcja od środka).
• Wyznaczenie granicy.
• Trzymanie się „zasady zależności”.
29. public
class
ProductEntity
{
private
final
long
id;
private
final
String
name;
private
final
boolean
isBought;
public
ProductEntity(long
id,
String
name,
boolean
isBought)
{
this.id
=
id;
this.name
=
name;
this.isBought
=
isBought;
}
public
long
getId()
{
return
id;
}
public
String
getName()
{
return
name;
}
public
boolean
isBought()
{
return
isBought;
}
}
30. • ProductDataSourceImpl - implementacja oderwana
od zewnętrznych bibliotek
• Zależy tylko od ProductEntityStore i Mappera
(Product -> ProductEntity)
• Łatwe do zmockowania aby przetestować w izolacji.
public interface ProductEntityStore {
List<ProductEntity> getAllProduct();
void storeAllProducts(List<ProductEntity> entities);
}
53. Zapisywanie stanu
• Input użytkownika jest stanem - nie model danych.
• Stan == Parcelable
• UI Zapisuje swój własny stan - EditText, ListView z
automatu.
• Powinniśmy trzymać stan w Presenterach?
55. Kontener DI
• ma konstruktor @Inject - wiem jak go
znaleźć - tworzę i zwracam
• nie wiem skąd go wziąć? - sięgam do
modułu
Moduły
Litania metod dostarczających
zależności.
Dagger
daj obiekt
ObjectGraph
@Singleton
57. Wady / Zalety?
• Overhead dla małych aplikacji - na pewno wiadomo, że nie urośnie?
• Duża ilość wywołań metod.
• Mniej optymalne rozwiązanie pamięciowo - tworzy i zwalnia się
dużo obiektów, fragmentacja pamięci.
• Samo podejście i ,,zasada zależności” słuszne - umożliwiają
testowanie.