4Developers 2015: Przejrzysty i testowalny kod na Androidzie? Spróbujmy z Clean Architecture -...
Transcript of 4Developers 2015: Przejrzysty i testowalny kod na Androidzie? Spróbujmy z Clean Architecture -...
Testowalna aplikacja na Androida?
Spróbujmy z Clean Architecture.
Michał Charmas
O mnie
• Developer aplikacji mobilnych na Androida
• Trener w firmie Bottega IT Solutions
• Konsultant / Freelancer
Photo by http://commons.wikimedia.org/wiki/User:Mattbuck / CC BY
• 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ąć
Jak żyć?
ioChed 2014
–Android Developers Blog
„…the other primary goal is to serve as a practical example of best practices for Android app design
and development.”
• 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?
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)
Application
Networking
Persistance
Android SDK Models / Domain
Android Application
Application
Android SDKNetworking
Persistance
Models / Domain
import android.* „szczegół techniczny”
Źródło: http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
• Niezależna od frameworka.
• Niezależna od interfejsu użytkownika.
• Niezależna od bazy danych.
• Testowalna - w oderwaniu od frameworka / bazy danych / serwera.
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()); }
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() { } }
–Robert C. Martin
„A good architecture emphasizes the use-cases and decouples them from peripheral concerns.”
• UseCases:
• AddProduct
• ListProducts
• ChangeProductBoughtStatus
• RemoveAllBoughtProducts
public interface UseCase<Result, Argument> { Result execute(Argument arg) throws Exception; }
public interface UseCaseArgumentless<Result> { Result execute() throws Exception; }
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 } }
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); } }
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; } }
• całkowicie niezależne od frameworka
• pure Java
• może zostać wyciągnięte do oddzielnego modułu - czysto javowego
Persistence / Data
• 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”.
Entities
UseCases
DataSource
DataSourceImpl
StoreRESTStore
InMemoryCacheStoreSharedPreferencesStore
[Product] [ProductList]
[Product -> ProducDBEntity] [ProducDBEntity]
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; } }
• 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); }
http://upload.wikimedia.org/wikipedia/commons/1/10/20090529_Great_Wall_8185.jpg
public class SharedPreferencesProductEntityStore implements ProductEntityStore { private final SharedPreferences sharedPreferences; private final EntityJsonMapper entityJsonMapper;
//… @Inject public SharedPreferencesProductEntityStore(SharedPreferences sharedPreferences, EntityJsonMapper entityJsonMapper) { this.sharedPreferences = sharedPreferences; this.entityJsonMapper = entityJsonMapper; } @Override public List<ProductEntity> getAllProduct() { try { return entityJsonMapper.fromJson( sharedPreferences.getString(SP_PRODUCT_ENTITIES, „[]” )); } catch (JSONException e) { handleJSONError(e); return Collections.emptyList(); } }
//… }
public class SharedPreferencesProductEntityStoreTest extends AndroidTestCase { private SharedPreferencesProductEntityStore store; @Override public void setUp() throws Exception { super.setUp(); store = new SharedPreferencesProductEntityStore( getAndClearSharedPreferences(), new SharedPreferencesProductEntityStore.EntityJsonMapper() ); } public void testShouldStoreEmptyList() throws Exception { store.storeAllProducts(Collections.<ProductEntity>emptyList()); assertEquals(store.getAllProduct().size(), 0); }}
Entities
UseCases
DataSource
DataSourceImpl
StoreRESTStore
InMemoryCacheStoreSharedPreferencesStore
[Product] [ProductList]
[Product -> ProducDBEntity] [ProducDBEntity]
UI
Źródło: http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
Model View Presenter
Passive View
Presenter
Model
user events
updates model
view updates
state changes
LOLCycle
public interface Presenter<T extends UI> { void attachUI(T ui); }
UI1
P1 P1
UI1 UI2
P2
UI1
P1
UI2UI1
public interface Presenter<T extends UI> { void attachUI(T ui); void detachUI(); }
UI1
P1
UI2
P1
AsyncOperation
P1
public interface UICommand<T> { void execute(T ui); }
private Queue<UICommand<T>> commandQueue = new LinkedList<>();
UI1
P1
UI2
P1
AsyncOperation
P1
@Singletonpublic class ProductListPresenter extends BasePresenter<ProductListPresenter.ProductListUI> { @Override protected void onFirstUIAttachment() { updateProductList(true); } public void productStatusChanged(long productId, boolean isBought) { } public void onAddNewProduct() { } public void onRemoveBoughtProducts() { } private void updateProductList(boolean withProgress) { if (withProgress) { execute(new ShowProgressCommand(), true); } asyncUseCase.wrap(listProductsUseCase).subscribe( new Action1<ProductList>() { @Override public void call(ProductList products) { List<ProductViewModel> viewModels = mapper.toViewModel(products); executeRepeat(new PresentContentCommand(viewModels)); } } ); } }
@Singletonpublic class AddProductPresenter extends BasePresenter<AddProductPresenter.AddProductUI> { private final AddProductUseCase addProductUseCase; private final AsyncUseCase asyncUseCase; @Inject public AddProductPresenter(AddProductUseCase addProductUseCase, AsyncUseCase asyncUseCase) { this.addProductUseCase = addProductUseCase; this.asyncUseCase = asyncUseCase; } public void onProductNameComplete(final String productName) { asyncUseCase.wrap(addProductUseCase, productName) .subscribe( new Action1<Product>() { @Override public void call(Product product) { executeRepeat(new NavigateBackCommand()); } }, new Action1<Throwable>() { @Override public void call(Throwable throwable) { if (throwable instanceof ValidationException) { executeOnce(new ShowValidationErrorCommand()); } // TODO: handle unknown error } }); } }
private static class ShowValidationErrorCommand implements UICommand<AddProductUI> { @Override public void execute(AddProductUI ui) { ui.showValidationError(); } } private static class NavigateBackCommand implements UICommand<AddProductUI> { @Override public void execute(AddProductUI ui) { ui.navigateBack(); } } public interface AddProductUI extends UI { void navigateBack(); void showValidationError(); }
Pink Floyd - The Wall Cover
public class AddProductFragment extends PresenterFragment<AddProductUI> implements AddProductUI { @Inject AddProductPresenter presenter; private EditText productNameView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setupPresenter(presenter, this); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_add_product, container, false); } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); productNameView = (EditText) view.findViewById(R.id.product_name_view); view.findViewById(R.id.product_add_button) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { presenter.onProductNameComplete(productNameView.getText().toString()); } }); } @Override public void navigateBack() { ((ProductListFragmentActivity) getActivity()).onProductAdded(); } @Override public void showValidationError() { Toast.makeText(getActivity(), "Product name cannot be empty.", Toast.LENGTH_SHORT).show(); } }
Entities
UseCases
Presenters
UI ProductListUI
AddProductUI
[Product]
[Product -> ProductViewModel] [ProductViewModel]
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?
DI
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
AppObjectGraph • AndroidModule • DataModule • DomainModule Application Scope
PresenterActivityOG • PresenterModule • Preseneter - @Singleton onRetainCustomNonConfigurationInstance
ActivityOG • ActivityModule Activity Scope
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.
–Robert C. Martin
„A good architecture allows major decisions to be deffered.”
https://github.com/mcharmas/shoppinglist-clean-architecture-example
http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
http://blog.8thlight.com/uncle-bob/2011/09/30/Screaming-Architecture.html
https://vimeo.com/43612849
https://vimeo.com/80533536
http://tpierrain.blogspot.com/2013/08/a-zoom-on-hexagonalcleanonion.html
Na czym polega ,,zasada zależności" w Clean Architecture?
Q&A?