Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

4Developers 2015: Przejrzysty i testowalny kod na Androidzie? Spróbujmy z Clean Architecture - Michał Charmas

279 views

Published on

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ą.

Published in: Software
  • Be the first to comment

  • Be the first to like this

4Developers 2015: Przejrzysty i testowalny kod na Androidzie? Spróbujmy z Clean Architecture - Michał Charmas

  1. 1. Testowalna aplikacja na Androida? Spróbujmy z Clean Architecture. Michał Charmas
  2. 2. O mnie • Developer aplikacji mobilnych na Androida • Trener w firmie Bottega IT Solutions • Konsultant / Freelancer
  3. 3. Photo by http://commons.wikimedia.org/wiki/User:Mattbuck / CC BY
  4. 4. • 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ąć
  5. 5. Jak żyć?
  6. 6. ioChed 2014
  7. 7. –Android Developers Blog „…the other primary goal is to serve as a practical example of best practices for Android app design and development.”
  8. 8. • 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?
  9. 9. 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)
  10. 10. Application Networking Persistance Android SDK Models / Domain Android Application Application Android SDK Networking Persistance Models / Domain import  android.* „szczegół techniczny”
  11. 11. Źródło: http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
  12. 12. • Niezależna od frameworka. • Niezależna od interfejsu użytkownika. • Niezależna od bazy danych. • Testowalna - w oderwaniu od frameworka / bazy danych / serwera.
  13. 13. 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());
 }
  14. 14. 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() {
 }
 }
  15. 15. –Robert C. Martin „A good architecture emphasizes the use-cases and decouples them from peripheral concerns.”
  16. 16. • UseCases: • AddProduct • ListProducts • ChangeProductBoughtStatus • RemoveAllBoughtProducts public  interface  UseCase<Result,  Argument>  {      Result  execute(Argument  arg)  throws  Exception;   }   public  interface  UseCaseArgumentless<Result>  {      Result  execute()  throws  Exception;   }  
  17. 17. 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      }   }  
  18. 18. 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);
 }
 }
  19. 19. 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;
 }
 }
  20. 20. • całkowicie niezależne od frameworka • pure Java • może zostać wyciągnięte do oddzielnego modułu - czysto javowego
  21. 21. Persistence / Data
  22. 22. • 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”.
  23. 23. Entities UseCases DataSource DataSourceImpl Store RESTStore InMemoryCacheStore SharedPreferencesStore [Product] [ProductList] [Product -> ProducDBEntity] [ProducDBEntity]
  24. 24. 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;      }   }  
  25. 25. • 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);
 }
  26. 26. http://upload.wikimedia.org/wikipedia/commons/1/10/20090529_Great_Wall_8185.jpg
  27. 27. 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();
        }
    }   //…   
 }  
  28. 28. 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);
    }
 }
  29. 29. Entities UseCases DataSource DataSourceImpl Store RESTStore InMemoryCacheStore SharedPreferencesStore [Product] [ProductList] [Product -> ProducDBEntity] [ProducDBEntity]
  30. 30. UI
  31. 31. Źródło: http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
  32. 32. Model View Presenter Passive View Presenter Model user events updates model view updates state changes
  33. 33. LOLCycle
  34. 34. public interface Presenter<T extends UI> {
 void attachUI(T ui);
 }
  35. 35. UI1 P1 P1 UI1 UI2 P2
  36. 36. UI1 P1 UI2UI1
  37. 37. public interface Presenter<T extends UI> {
 void attachUI(T ui);
 
 void detachUI();
 }
  38. 38. UI1 P1 UI2 P1 AsyncOperation P1
  39. 39. public interface UICommand<T> {
 void execute(T ui);
 } private Queue<UICommand<T>> commandQueue = new LinkedList<>(); UI1 P1 UI2 P1 AsyncOperation P1
  40. 40. @Singleton
 public 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));
 }
 }
 );
 }
 }
  41. 41. @Singleton
 public 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
 }
 });
 } }
  42. 42. 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();
 }
  43. 43. Pink Floyd - The Wall Cover
  44. 44. 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();
 } }
  45. 45. Entities UseCases Presenters UI ProductListUI AddProductUI [Product] [Product -> ProductViewModel] [ProductViewModel]
  46. 46. 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?
  47. 47. DI
  48. 48. 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
  49. 49. AppObjectGraph • AndroidModule • DataModule • DomainModule Application Scope PresenterActivityOG • PresenterModule • Preseneter - @Singleton onRetainCustomNonConfigurationInstance ActivityOG • ActivityModule Activity Scope
  50. 50. 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.
  51. 51. –Robert C. Martin „A good architecture allows major decisions to be deffered.”
  52. 52. https://github.com/mcharmas/shoppinglist-clean-architecture-example
  53. 53. 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
  54. 54. Na czym polega ,,zasada zależności" w Clean Architecture?
  55. 55. Q&A?

×