SlideShare una empresa de Scribd logo
1 de 62
Descargar para leer sin conexión
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 SDK
Networking
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
Store
RESTStore
InMemoryCacheStore
SharedPreferencesStore
[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
Store
RESTStore
InMemoryCacheStore
SharedPreferencesStore
[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
@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));

}

}

);

}

}
@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

}

});

}
}
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?

Más contenido relacionado

Destacado

Evaluation question 4/5
Evaluation question 4/5Evaluation question 4/5
Evaluation question 4/5rhiannquinn
 
Department secretary perfomance appraisal 2
Department secretary perfomance appraisal 2Department secretary perfomance appraisal 2
Department secretary perfomance appraisal 2tonychoper6304
 
An Investigation of Practicing Carroll’s Pyramid of Corporate Social Responsi...
An Investigation of Practicing Carroll’s Pyramid of Corporate Social Responsi...An Investigation of Practicing Carroll’s Pyramid of Corporate Social Responsi...
An Investigation of Practicing Carroll’s Pyramid of Corporate Social Responsi...IOSR Journals
 
Sustaining Value Creation through Knowledge of Customer Expectations
Sustaining Value Creation through Knowledge of Customer ExpectationsSustaining Value Creation through Knowledge of Customer Expectations
Sustaining Value Creation through Knowledge of Customer ExpectationsIOSR Journals
 
Corporate Governance of Capital Market of Bangladesh
Corporate Governance of Capital Market of BangladeshCorporate Governance of Capital Market of Bangladesh
Corporate Governance of Capital Market of BangladeshIOSR Journals
 
Présentation d’entreprise
Présentation d’entreprisePrésentation d’entreprise
Présentation d’entreprisejoffrey8313
 
Impact of Trade Associations on Entrepreneurial Traits in Nigeria’s Transport...
Impact of Trade Associations on Entrepreneurial Traits in Nigeria’s Transport...Impact of Trade Associations on Entrepreneurial Traits in Nigeria’s Transport...
Impact of Trade Associations on Entrepreneurial Traits in Nigeria’s Transport...IOSR Journals
 

Destacado (9)

Evaluation question 4/5
Evaluation question 4/5Evaluation question 4/5
Evaluation question 4/5
 
Department secretary perfomance appraisal 2
Department secretary perfomance appraisal 2Department secretary perfomance appraisal 2
Department secretary perfomance appraisal 2
 
An Investigation of Practicing Carroll’s Pyramid of Corporate Social Responsi...
An Investigation of Practicing Carroll’s Pyramid of Corporate Social Responsi...An Investigation of Practicing Carroll’s Pyramid of Corporate Social Responsi...
An Investigation of Practicing Carroll’s Pyramid of Corporate Social Responsi...
 
Sustaining Value Creation through Knowledge of Customer Expectations
Sustaining Value Creation through Knowledge of Customer ExpectationsSustaining Value Creation through Knowledge of Customer Expectations
Sustaining Value Creation through Knowledge of Customer Expectations
 
Corporate Governance of Capital Market of Bangladesh
Corporate Governance of Capital Market of BangladeshCorporate Governance of Capital Market of Bangladesh
Corporate Governance of Capital Market of Bangladesh
 
Primeros discípulos
Primeros discípulosPrimeros discípulos
Primeros discípulos
 
CV- MM April 2015 -
CV- MM April 2015 -CV- MM April 2015 -
CV- MM April 2015 -
 
Présentation d’entreprise
Présentation d’entreprisePrésentation d’entreprise
Présentation d’entreprise
 
Impact of Trade Associations on Entrepreneurial Traits in Nigeria’s Transport...
Impact of Trade Associations on Entrepreneurial Traits in Nigeria’s Transport...Impact of Trade Associations on Entrepreneurial Traits in Nigeria’s Transport...
Impact of Trade Associations on Entrepreneurial Traits in Nigeria’s Transport...
 

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

Wzorce projektowe (w ASP.NET i nie tylko)
Wzorce projektowe (w ASP.NET i nie tylko)Wzorce projektowe (w ASP.NET i nie tylko)
Wzorce projektowe (w ASP.NET i nie tylko)Bartlomiej Zass
 
[PL] Jak programować aby nie zwariować
[PL] Jak programować aby nie zwariować[PL] Jak programować aby nie zwariować
[PL] Jak programować aby nie zwariowaćJakub Marchwicki
 
Patronage 2016 Windows 10 Warsztaty
Patronage 2016 Windows 10 WarsztatyPatronage 2016 Windows 10 Warsztaty
Patronage 2016 Windows 10 Warsztatyintive
 
Testy API: połączenie z bazą danych czy implementacja w pamięci
Testy API: połączenie z bazą danych czy implementacja w pamięciTesty API: połączenie z bazą danych czy implementacja w pamięci
Testy API: połączenie z bazą danych czy implementacja w pamięciThe Software House
 
Angular 4 pragmatycznie
Angular 4 pragmatycznieAngular 4 pragmatycznie
Angular 4 pragmatycznieSages
 
CDI Portable Extensions
CDI Portable ExtensionsCDI Portable Extensions
CDI Portable ExtensionsAdam Warski
 
DreamLab Academy #12 Wprowadzenie do React.js
DreamLab Academy #12 Wprowadzenie do React.jsDreamLab Academy #12 Wprowadzenie do React.js
DreamLab Academy #12 Wprowadzenie do React.jsDreamLab
 
Metaprogramowanie w JS
Metaprogramowanie w JSMetaprogramowanie w JS
Metaprogramowanie w JSDawid Rusnak
 
Seam framework in_action
Seam framework in_actionSeam framework in_action
Seam framework in_actionMichał Orman
 
NK API - Przykłady
NK API - PrzykładyNK API - Przykłady
NK API - Przykładynasza-klasa
 
Testowanie bezpieczeństwa aplikacji dedykowanych na platformę Android
Testowanie bezpieczeństwa aplikacji dedykowanych na platformę AndroidTestowanie bezpieczeństwa aplikacji dedykowanych na platformę Android
Testowanie bezpieczeństwa aplikacji dedykowanych na platformę AndroidSecuRing
 
Modularny JavaScript - meet.js
Modularny JavaScript - meet.jsModularny JavaScript - meet.js
Modularny JavaScript - meet.jsPatryk Jar
 
Systematyczny architekt na drodze ku planowanemu postarzaniu
Systematyczny architekt na drodze ku planowanemu postarzaniuSystematyczny architekt na drodze ku planowanemu postarzaniu
Systematyczny architekt na drodze ku planowanemu postarzaniuJaroslaw Palka
 
Systematic architect
Systematic architectSystematic architect
Systematic architectmagda3695
 
JDD2014: Systematyczny architekt na drodze ku postarzaniu produktu - Jarosław...
JDD2014: Systematyczny architekt na drodze ku postarzaniu produktu - Jarosław...JDD2014: Systematyczny architekt na drodze ku postarzaniu produktu - Jarosław...
JDD2014: Systematyczny architekt na drodze ku postarzaniu produktu - Jarosław...PROIDEA
 
SCAP – standaryzacja formatów wymiany danych w zakresie bezpieczeństwa IT
SCAP – standaryzacja formatów wymiany danych w zakresie bezpieczeństwa ITSCAP – standaryzacja formatów wymiany danych w zakresie bezpieczeństwa IT
SCAP – standaryzacja formatów wymiany danych w zakresie bezpieczeństwa ITRedge Technologies
 

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

Wzorce projektowe (w ASP.NET i nie tylko)
Wzorce projektowe (w ASP.NET i nie tylko)Wzorce projektowe (w ASP.NET i nie tylko)
Wzorce projektowe (w ASP.NET i nie tylko)
 
[PL] Jak programować aby nie zwariować
[PL] Jak programować aby nie zwariować[PL] Jak programować aby nie zwariować
[PL] Jak programować aby nie zwariować
 
Patronage 2016 Windows 10 Warsztaty
Patronage 2016 Windows 10 WarsztatyPatronage 2016 Windows 10 Warsztaty
Patronage 2016 Windows 10 Warsztaty
 
Testy API: połączenie z bazą danych czy implementacja w pamięci
Testy API: połączenie z bazą danych czy implementacja w pamięciTesty API: połączenie z bazą danych czy implementacja w pamięci
Testy API: połączenie z bazą danych czy implementacja w pamięci
 
Angular 4 pragmatycznie
Angular 4 pragmatycznieAngular 4 pragmatycznie
Angular 4 pragmatycznie
 
CDI Portable Extensions
CDI Portable ExtensionsCDI Portable Extensions
CDI Portable Extensions
 
DreamLab Academy #12 Wprowadzenie do React.js
DreamLab Academy #12 Wprowadzenie do React.jsDreamLab Academy #12 Wprowadzenie do React.js
DreamLab Academy #12 Wprowadzenie do React.js
 
Mobile services
Mobile servicesMobile services
Mobile services
 
Metaprogramowanie w JS
Metaprogramowanie w JSMetaprogramowanie w JS
Metaprogramowanie w JS
 
Seam framework in_action
Seam framework in_actionSeam framework in_action
Seam framework in_action
 
NK API - Przykłady
NK API - PrzykładyNK API - Przykłady
NK API - Przykłady
 
Testowanie bezpieczeństwa aplikacji dedykowanych na platformę Android
Testowanie bezpieczeństwa aplikacji dedykowanych na platformę AndroidTestowanie bezpieczeństwa aplikacji dedykowanych na platformę Android
Testowanie bezpieczeństwa aplikacji dedykowanych na platformę Android
 
[TestWarez 2017] Architektura testów automatycznych dla wielomodułowej aplika...
[TestWarez 2017] Architektura testów automatycznych dla wielomodułowej aplika...[TestWarez 2017] Architektura testów automatycznych dla wielomodułowej aplika...
[TestWarez 2017] Architektura testów automatycznych dla wielomodułowej aplika...
 
Modularny JavaScript - meet.js
Modularny JavaScript - meet.jsModularny JavaScript - meet.js
Modularny JavaScript - meet.js
 
react-pl.pdf
react-pl.pdfreact-pl.pdf
react-pl.pdf
 
Refaktoryzacja
RefaktoryzacjaRefaktoryzacja
Refaktoryzacja
 
Systematyczny architekt na drodze ku planowanemu postarzaniu
Systematyczny architekt na drodze ku planowanemu postarzaniuSystematyczny architekt na drodze ku planowanemu postarzaniu
Systematyczny architekt na drodze ku planowanemu postarzaniu
 
Systematic architect
Systematic architectSystematic architect
Systematic architect
 
JDD2014: Systematyczny architekt na drodze ku postarzaniu produktu - Jarosław...
JDD2014: Systematyczny architekt na drodze ku postarzaniu produktu - Jarosław...JDD2014: Systematyczny architekt na drodze ku postarzaniu produktu - Jarosław...
JDD2014: Systematyczny architekt na drodze ku postarzaniu produktu - Jarosław...
 
SCAP – standaryzacja formatów wymiany danych w zakresie bezpieczeństwa IT
SCAP – standaryzacja formatów wymiany danych w zakresie bezpieczeństwa ITSCAP – standaryzacja formatów wymiany danych w zakresie bezpieczeństwa IT
SCAP – standaryzacja formatów wymiany danych w zakresie bezpieczeństwa IT
 

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

  • 1.
  • 2. Testowalna aplikacja na Androida? Spróbujmy z Clean Architecture. Michał Charmas
  • 3. O mnie • Developer aplikacji mobilnych na Androida • Trener w firmie Bottega IT Solutions • Konsultant / Freelancer
  • 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)
  • 11.
  • 12.
  • 13.
  • 14. Application Networking Persistance Android SDK Models / Domain Android Application Application Android SDK Networking Persistance Models / Domain import  android.* „szczegół techniczny”
  • 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);
 }
  • 32. 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();
        }
    }   //…   
 }  
  • 33. 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);
    }
 }
  • 35. UI
  • 37. Model View Presenter Passive View Presenter Model user events updates model view updates state changes
  • 38.
  • 39.
  • 41. public interface Presenter<T extends UI> {
 void attachUI(T ui);
 }
  • 44. public interface Presenter<T extends UI> {
 void attachUI(T ui);
 
 void detachUI();
 }
  • 46. public interface UICommand<T> {
 void execute(T ui);
 } private Queue<UICommand<T>> commandQueue = new LinkedList<>(); UI1 P1 UI2 P1 AsyncOperation P1
  • 47. @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));
 }
 }
 );
 }
 }
  • 48. @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
 }
 });
 } }
  • 49. 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();
 }
  • 50. Pink Floyd - The Wall Cover
  • 51. 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();
 } }
  • 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?
  • 54. DI
  • 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
  • 56. AppObjectGraph • AndroidModule • DataModule • DomainModule Application Scope PresenterActivityOG • PresenterModule • Preseneter - @Singleton onRetainCustomNonConfigurationInstance ActivityOG • ActivityModule Activity Scope
  • 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.
  • 58. –Robert C. Martin „A good architecture allows major decisions to be deffered.”
  • 61. Na czym polega ,,zasada zależności" w Clean Architecture?
  • 62. Q&A?