EVOLUINDO ARQUITETURAS REATIVAS - QConSP · RxJava permite operações assíncronas em um estilo...

Preview:

Citation preview

EVOLUINDO ARQUITETURAS

REATIVASUbiratan Soares QCONSP / 2017

O QUE É UMA ARQUITETURA EM

MOBILE ?

MVP

MVVMVIPER

FLUX REDUX

CLEAN

MVC…

MVI

PRINCÍPIOS DE ARQUITETURAOrganização

Facilidade em se encontrar o que se precisa

Menor impedância para se resolver bugs

Menos dor ao escalar em tamanho (codebase e devs)

Estilo de projeto unificado, definido e defendido pelo time

MOBILE CHALLENGESInterações com usuário e eventos de sistema são assíncronos

I/O deve ser concorrente

Processamento pesado deve ser concorrente

Fragmentação de plataformas

ETC….

UMA QUEIXA COMUM NA COMUNIDADE

MOBILE ?

TEM PELO MENOS UM UNIT TEST NO APP?

EM MOBILE, ARQUITETURA É CRÍTICA

PARA TESTABILIDADE

QUAL ARQUITETURA ESCOLHER ENTÃO ???

MIXED FEELINGS

NÃO HÁ SILVER BULLETS!

MODEL VIEW

PRESENTER

PRESENTATION LAYER

DATA LAYER

DB

REST

ETC

UI

. . .

public interface ViewDelegate {

void displayResults(DataModel model);

void networkingError();

void displayEmptyState();

void displayErrorState();

// More delegation }

public class MainActivity extends AppCompatActivity implements ViewDelegate {

Presenter presenter; // How to resolve this instance ???

@Override protected void onStart() { super.onStart(); presenter.bindView(this); presenter.fetchData(); }

@Override public void displayResults(DataModel model) { // Put data into view }

@Override public void networkingError() { // Up to you }

@Override public void displayEmptyState() { // And this too! }

@Override public void displayErrorState() { // Please, do not mess with your user } }

public class Presenter {

public void bindView(ViewDelegate delegate) { this.delegate = delegate; }

public void fetchData() { source.fetchData(new DataSource.Callback() {

@Override public void onDataLoaded(DataModel model) { delegate.displayResults(model); }

@Override public void onError(Throwable t) {

if (t instanceof NetworkingError) { delegate.networkingError(); } else if (t instanceof NoDataAvailable) { … } } }); } }

DATASOURCE

REST GATEWAY

PRESENTER

VIEW DELEGATION CALLBACKS

PLATAFORM CONTROLLER

CALLBACKUNIT TESTS

(Mocked Contract)

FUNCTIONAL UI TESTS INTEGRATION TESTS

INTEGRATION TESTS (DOUBLES)

UNIT TESTS (Mocked Source

+ Mocked View)DA

TAM

ODE

L

String description = “Blah” String date = “2010-02-26T19:35:24Z” int step = 2

String description = “Blah” LocalDateTime dateTime = (JSR310) TrackingStep currentStep = (enum)

String description = “Blah” String formattedDate = “26/02/2010” String currentStep = “Concluído”

Response Model

Domain Model

View Model

DATA MODEL

PROBLEMAS EM POTENCIALQual representação de dados utilizar? Unificada ou separada?

Onde aplicar parsing? E formatação para a UI?

Callbacks aninhados

Memory leaks no nível do mecanismo de entrega

Etc

BRACE YOURSELVES

RX IS COMING

THE RXJAVA REVOLUTIONRxJava permite operações assíncronas em um estilo síncrono, turbinadas por operadores funcionais

Threading transparente

Tratamento unificado de erros via adição ao Observer Pattern

Battle-tested

COMO ADICIONAR RX

NESSA ARQUITETURA ??

PRESENTATION LAYER

DATA LAYER

DB

REST

ETC

UI

. . .

Callback(T)Callback(T) Callback(T)

SUBSTITUIR CALLBACKS POR SEQUÊNCIAS OBSERVÁVEIS

PRIMEIRA INTERAÇÃO

CAMADA DE DADOS REATIVA

REST GATEWAY

VIEW DELEGATION

VIEW

DATA SOURCE

Observable<T>

PRESENTER

Callback(T)

OBSERVER<T>

public interface HelpDeskEventsSource {

Observable<HelpDeskEvent> fetchWith(MessagesForOrderParameters params);

Observable<HelpDeskEvent> sendMessage(MessageToSellerParameters params);

Observable<HelpDeskEvent> requireMediation(MediationParameters params);

}

ADEUS CALLBACKS !!!👌

public class HelpDeskStreamInfrastructure implements HelpDeskEventsSource {

@Override public Observable<HelpDeskEvent> fetchWith(MessagesForOrderParameters params) { return restAPI.getHelpDeskTickets(params) .subscribeOn(Schedulers.io()) .map(HelpDeskPayloadMapper::map) .filter(Preconditions::notNullOrEmpty) .flatMap(Observable::from); }

@Override public Observable<HelpDeskEvent> sendMessage(MessageToSellerParameters params) { MessageToSellerBody body = SendMessageToSellerBodyMapper.convert(params);

return restAPI.sendHelpdeskMessageToSeller(body) .subscribeOn(Schedulers.io()) .flatMap(emptyBody -> fetchWith(sameFromSeller(params))); }

} Chained request, easy !!!!

VANTAGENS OBSERVADASFacilidades via frameworks utilitários para REST / DB

Validação de dados de entrada e tradução de modelos como etapas do pipeline

Tratamento de erros, auto retry, exponential backoff no “baixo nível”

PROBLEMAS OBSERVADOSConsumir os dados no nível da apresentação nos força a rodar comportamentos na thread principal do app (orquestração dos callbacks)

Indireção forçada para prover Scheduler via DI, para propósitos de testes

Muitas responsabilidades no Presenter

SEGUNDA INTERAÇÃO

CAMADA DE APRESENTAÇÃO REATIVA

REST GATEWAY

VIEW DELEGATION

VIEW

DATA SOURCE

Observable<T>

PRESENTER

OBSERVER<T>

Observable<T>

SUBSCRIPTION

public interface SomeView<T> {

Func1<Observable<T>, Subscription> results();

Func1<Observable<Unit>, Subscription> showEmptyState();

Func1<Observable<Unit>, Subscription> hideEmptyState();

Func1<Observable<Unit>, Subscription> showLoading();

Func1<Observable<Unit>, Subscription> hideLoading();

// More delegation }

public static <T> Subscription bind(Observable<T> observable, Func1<Observable<T>, Subscription> uiFunc) { return uiFunc.call(observable); }

public static <T> Func1<Observable<T>, Subscription> uiFunction(Action1<T> uiAction) { return uiFunction(uiAction, () -> {}); }

public static <T> Func1<Observable<T>, Subscription> uiFunction(Action1<T> uiAction, Action0 done) {

return observable -> observable .observeOn(AndroidSchedulers.mainThread()) .subscribe( uiAction, throwable -> Logger.e(throwable.getMessage()), done ); }

public static <T> Subscription bind(Observable<T> observable, Func1<Observable<T>, Subscription> uiFunc) { return uiFunc.call(observable); }

public static <T> Func1<Observable<T>, Subscription> uiFunction(Action1<T> uiAction) { return uiFunction(uiAction, () -> {}); }

public static <T> Func1<Observable<T>, Subscription> uiFunction(Action1<T> uiAction, Action0 done) {

return observable -> observable .observeOn(AndroidSchedulers.mainThread()) .subscribe( uiAction, throwable -> Logger.e(throwable.getMessage()), done ); }

public static <T> Subscription bind(Observable<T> observable, Func1<Observable<T>, Subscription> uiFunc) { return uiFunc.call(observable); }

public static <T> Func1<Observable<T>, Subscription> uiFunction(Action1<T> uiAction) { return uiFunction(uiAction, () -> {}); }

public static <T> Func1<Observable<T>, Subscription> uiFunction(Action1<T> uiAction, Action0 done) {

return observable -> observable .observeOn(AndroidSchedulers.mainThread()) .subscribe( uiAction, throwable -> Logger.e(throwable.getMessage()), done ); }

public class HelpDeskMessagingActivity extends BaseActivity implements HelpDeskMessagesStreamView {

@Override public Func1<Observable<String>, Subscription> restoreNotSentMessage() { return uiFunction(message -> { Toast.makeText(this, "Erro ao enviar mensagem", LENGTH_SHORT).show(); messageInput.setText(message); }); }

@Override public Func1<Observable<Unit>, Subscription> enableComplaintOption() { return uiFunction(action -> complaintButton.setVisibility(VISIBLE)); }

@Override public Func1<Observable<Unit>, Subscription> disableComplaintOption() { return uiFunction(action -> complaintButton.setVisibility(GONE)); }

@Override public Func1<Observable<Unit>, Subscription> showEmptyState() { return uiFunction(action -> emptyStateContainer.setVisibility(VISIBLE)); }

// More delegate methods

public class HelpDeskMessagingActivity extends BaseActivity implements HelpDeskMessagesStreamView {

@Override public Func1<Observable<String>, Subscription> restoreNotSentMessage() { return uiFunction(message -> { Toast.makeText(this, "Erro ao enviar mensagem", LENGTH_SHORT).show(); messageInput.setText(message); }); }

@Override public Func1<Observable<Unit>, Subscription> enableComplaintOption() { return uiFunction(action -> complaintButton.setVisibility(VISIBLE)); }

@Override public Func1<Observable<Unit>, Subscription> disableComplaintOption() { return uiFunction(action -> complaintButton.setVisibility(GONE)); }

@Override public Func1<Observable<Unit>, Subscription> showEmptyState() { return uiFunction(action -> emptyStateContainer.setVisibility(VISIBLE)); }

// More delegate methods

public void userRequiredMediation(String productId, String sellerId) {

MediationParameters parameters = new RequiredMediationParameters.Builder() .productId(productId)

// … .sellerId(sellerId) .build();

executionPipeline(parameters); }

private void executionPipeline(MediationParameters parameters) {

Observable<HelpDeskEventViewModel> execution = source.requireMediation(parameters) .doOnSubscribe(this::prepareToLoad) .map(ViewModelMappers::map) .flatMap(Observable::from) .doOnCompleted(this::finishLoadingMessages);

subscription().add(bind(execution, view().onMessagesLoaded()));

}

PRESENTER LEVEL

VANTAGENS OBSERVADASPresenter não precisa da noção de threading

Possibilidade de combinação de múltiplas fontes de forma organizada

Presenter passar a orquestrar a UI através de um pipeline de execução bem definido

Tradução de ViewModels é uma etapa do pipeline

PROBLEMAS OBSERVADOSProtocolo View ainda estava gordo

“Repetição” de código entre Presenters, normalmente relacionada a comportamentos de UI similares

- Mostrar empty state se não houver dados

- Mostrar loading ao iniciar operação; esconder ao terminar

- Controlar interação com Pull-to-refresh

- Estado de erro no caso de problemas, caso não exista conteúdo

- Vários outros

TERCEIRA INTERAÇÃO

REACTIVE VIEW SEGREGATION

public interface SomeView<T> {

Func1<Observable<T>, Subscription> results();

Func1<Observable<Unit>, Subscription> showEmptyState();

Func1<Observable<Unit>, Subscription> hideEmptyState();

Func1<Observable<Unit>, Subscription> showLoading();

Func1<Observable<Unit>, Subscription> hideLoading();

Func1<Observable<Unit>, Subscription> networkError();

Func1<Observable<Unit>, Subscription> networkUnavailable();

Func1<Observable<Unit>, Subscription> networkSlow(); }

UI BEHAVIOR

VIEW PROTOCOL

UI BEHAVIOR UI BEHAVIOR

UI BEHAVIOR . . .

public interface EmptyStateView<T> {

Func1<Observable<Unit>, Subscription> showEmptyState();

Func1<Observable<Unit>, Subscription> hideEmptyState(); }

public interface LoadingView<T> {

Func1<Observable<Unit>, Subscription> showLoading();

Func1<Observable<Unit>, Subscription> hideLoading(); }

public interface SomeView<T> extends LoadingView, EmptyStateView, NetworkingReporterView {

Func1<Observable<T>, Subscription> displayResults(); }

public interface NetworkingReporterView<T> {

Func1<Observable<Unit>, Subscription> networkError();

Func1<Observable<Unit>, Subscription> networkUnavailable();

Func1<Observable<Unit>, Subscription> networkSlow(); }

- Cada comportamento poderia ter o seu “mini-presenter” associado, e o Presenter “grande” faria a orquestração dos colaboradores

- Melhor estratégia : fazer a composição ser uma etapa do pipeline !!!

f(g(x))

public class LoadingWhileProcessing<T> implements Observable.Transformer<T, T> {

private PublishSubject<Unit> show, hide = PublishSubject.create();

public Subscription bindLoadingContent(LoadingView view) { CompositeSubscription composite = new CompositeSubscription(); composite.add(bind(show, view.showLoading())); composite.add(bind(hide, view.hideLoading())); return composite; }

@Override public Observable<T> call(Observable<T> upstream) { return upstream .doOnSubscribe(this::showLoading) .doOnTerminate(this::hideLoading); }

private void hideLoading() { hide.onNext(Unit.instance()); }

private void showLoading() { show.onNext(Unit.instance()); } }

public class OrdersHistoryPresenter extends ReactivePresenter<OrdersHistoryView> {

// Binding all behaviors on view [ ... ]

public void fetchOrders(SearchCriteria criteria) { bind(executionPipeline(criteria), view().displayOrders()); }

private Observable<OrderHistoryType> executionPipeline(SearchCriteria criteria) {

return source.search(criteria) .compose(networkErrorFeedback) .compose(loadingWhenProcessing) .compose(coordinateRefresh) .compose(emptyStateWhenMissingData) .compose(errorWhenProblems) .map(OrdersHistoryViewModelMapper::map); } }

@Test public void shouldTransformView_RegardlessEmptyStream() {

// When stream has no data Observable<String> stream = Observable.empty();

// and we add transformation to pipeline stream.compose(loadingWhileProcessing) .subscribe( s -> {}, throwable -> {}, () -> {} );

// we should interact with view, anyway verify(view.showLoadingAction).call(uiMethod()); verify(view.hideLoadingAction).call(uiMethod()); }

VANTAGENSCada evento delegado para a UI agora é unit-testable de uma forma muito fácil !!!

Presenters apenas orquestram a UI (como prega MVP)

Presenter não liga para qual tipo de View está associado

Transformers são facilmente reutilizáveis

PROBLEMAS ENCONTRADOS (I)1) Boilerplate para o binding de comportamentos

@Override public void bind(OrdersHistoryView view) { super.bind(view); subscription().add(loadingWhileProcessing.bindLoadingContent(view)); subscription().add(networkErrorFeedback.bindNetworkingReporter(view)); subscription().add(coordinateRefresh.bindRefreshableView(view)); subscription().add(emptyStateWhenMissingData.bindEmptyStateView(view)); subscription().add(errorStateWhenProblem.bindErrorStateView(view)); }

✔ Possível solução (WIP) : ViewBinder

PROBLEMAS ENCONTRADOS (II)2) Comportamentos injetados via DI no Presenter; possível confusão ao fazer pull das dependências

✔ Possível solução (WIP) : ViewBinder fornecido via DI + configuração definida na apresentação

3) Cooperação entre comportamentos ✔ Possível solução (WIP) : Transformers agregadores

CONCLUSÕES

LIÇÕES APRENDIDASEscolher um modelo de arquitetura não é uma tarefa trivial

Evoluir um modelo para obter vantagens de um paradigma (FRP) é ainda menos trivial

Não tenha medo de errar; adote iterações na evolução da arquitetura

Esforço se paga no médio prazo

https://speakerdeck.com/ubiratansoares/evoluindo-arquiteturas-reativas

OBRIGADO@ubiratanfsoares

ubiratansoares.github.io

https://br.linkedin.com/in/ubiratanfsoares

Recommended