evoluindo arquiteturas reativas - qconsp · rxjava permite operações assíncronas em um estilo...
TRANSCRIPT
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