Utilizar operadores y evitar anidar suscripciones in RxJS, RxSwift or RxJava
Las subscripciones anidadas es un anti-patrón que comúnmente se encuentra en implementaciones con código Rx.Fácilmente se puede caer en este anti-patrón cuando la cantidad de código incrementa, cuando se requiere hacer un ajuste o una modificación.
Veamos el común ejemplo de subscripciones anidadas:
network.getToken(apiKey)
.subscribe(token => {
if (token.isValid) {
cache.storeToken(token)
.subscribe(saved =>
console.log('Token stored: ', saved));
}
});
¿Por qué es una práctica no recomendada?
- Es un mal uso de los componentes.
- Induce memory leaks.
- Induce comportamientos no esperados.
La subscripción en Rx ha sido diseñada para vincular un Observer a un Observable (una fuente emisora de ítems).
Anidar una subscripción produce la vinculación de un Observable a otro Observable, es decir, se intentan usar un Observable como un Observer. Esto es un uso inadecuado de dichos componentes en Rx.
Únicamente Subjects son diseñados con esa capacidad y sin embargo se deben usar solo en casos especiales.
La subscripción se debe usar para vincular un Observer a un Observable, no para intentar orquestar operaciones, la tarea de orquestar operaciones se hace a través de los operadores.
Una subscripción es creada cada vez que se vincula observer code a una fuente (source). Debido a que la subscripción consume recursos de memoria, debe ser liberada una vez ya no sea necesaria. Anidar subscripciones hace que dichas subscripciones se hagan difíciles de detectar y controlar, lo cual puede derivar en fugas de memoria (memory leaks).
Cuando dos tareas se vinculan a través de subscripciones anidadas, no hay garantías en el orden de ejecución de una tarea con respecto a otra, es decir, no se garantiza una correcta orquestación de las tareas. Para dicho propósito existen los operadores tales como flatMap, concatMap, switchMap, etc.
¿Cómo corregir las subscripciones anidadas?
A través de la aplicación de los operadores de orquestación. En la tabla siguiente se muestran algunos de los más usados.
El código anterior podría ser corregido así:
En RxJS
network.getToken(apikey)
.pipe(
filter(token => token.isValid),
concatMap(token => this.cache.storeToken(token))
)
.subscribe(saved =>
console.log('Token stored: ', saved));
En RxSwift
network.getToken(apikey)
.filter { token in token.isValid }
.concatMap { token in self.cache.storeToken(token) }
.subscribe(onNext: { saved in
print("Token stored: \(saved)")
})
.disposed(by: disposeBag)
En RxJava
network.getToken(apiKey)
.filter(Token::isValid)
.concatMap(token -> this.cache.storeToken(token))
.subscribe(saved ->
Log.d(TAG, "Token stored: " + saved));
Subscripciones anidadas ocultas
Tenga cuidado con aquellos escenarios en donde la subscripción anidada no es tan evidente. Algunos casos se muestran a continuación.
Caso 1: A pesar que el código parece organizado, se está anidando la subscripción a través del método storeToken.
getToken(apikey: string) {
this.network.getToken(apikey)
.subscribe(token => {
this.storeToken(token);
});
}
storeToken(token: Token) {
if (token.isValid) {
this.cache.storeToken(token)
.subscribe(saved =>
console.log('Token stored: ', saved));
}
}
Caso 2: En este caso se utiliza Subject para transmitir el mensaje, pero se está sobreutilizando. El mismo resultado se podría obtener solo con un Observable.
const oneSubject = new Subject();
const twoSubject = new Subject();
oneSubject.subscribe(message => console.log(message));
twoSubject.subscribe(message => {
oneSubject.next(message);
});
twoSubject.next("A");
twoSubject.next("B");
twoSubject.next("C");
Aplicar un minucioso code review o linters en las herramientas, podría servir como filtro para evitar dicho anti-patrón.
Espero que este artículo sea de utilidad y haya permitido aclarar dudas sobre subscripciones anidadas.
Si desea mayor informarción, les recomiendo la lectura del libro guía:
ConversionConversion EmoticonEmoticon