Angular — RxJS — Cold e Hot Observable

Saber a diferença de um Cold e um Hot Observable é fundamental para construir aplicações mais eficientes. Conhecendo as diferenças é possível por exemplo evitar requisições desnecessárias, evitar memory leak e aproveitar melhor o compartilhamento de informações entre componentes.

Angular — RxJS — Cold e Hot Observable

Cold Observable

Quando começamos a trabalhar com Angular logo nos deparamos com o RxJS e sua programação reativa. Inicialmente pode parecer um pouco complicado mas garanto que vale apena gastar um pouco de tempo aprendendo a usá-lo.

RxJS is a library for composing asynchronous and event-based programs by using observable sequences. It provides one core type, the Observable, satellite types (Observer, Schedulers, Subjects) and operators inspired by Array#extras(map, filter, reduce, every, etc) to allow handling asynchronous events as collections.

Falando rapidamente do padrão Observable

Observable é um design pattern com o objetivo de notificar seus objetos dependentes cada vez que houver uma mudança de estado no objeto observável. Em RxJS poderíamos citar o seguinte exemplo para efetuar a leitura do clique do mouse:

const clicks = Rx.Observable.fromEvent(document, 'click');

clicks.subscribe(click => console.log(click))
// click around the web page...
// MouseEvent<data>
// MouseEvent<data>

Uma das principais classes do RxJS é a classe Observable. Quando a instanciamos passamos como referência uma função do tipo Subscriber:

// https://rxjs-dev.firebaseapp.com/api/index/class/Observable
// constructor(subscribe?: (this: Observable<T>,
//             subscriber: Subscriber<T>) => TeardownLogic)
const myObservable = new Observable(function(subscriber){ 
    console.log('');
    subscriber.next('valor'); // Aqui emitimos um valor
});

// Aqui efetuamos uma subscrição para pegar 
// os valores emitidos:
myObservable.subscriber(val => console.log(val));

Sempre que alguém efetuar uma subscrição a função que passamos como parâmetro no construtor, o Subscriber, é executada. Se não houver nenhuma subscrição esta função nunca será executada.

Quando criamos um Observable a ideia é emitir alguma informação para que outras partes do seu sistema possam pegar este dado e processá-lo de alguma forma. A origem da informação de um Observable pode ser eventos DOM (cliques, eventos de mouse, etc.), requisições AJAX, etc. Quando a origem da informação que o Observable emite é criada e ativada dentro do Subscriber temos um Cold Observable:

const myObservable = new Observable<string>(subscriber => {
    // Origem dos dados é criada dentro do subscriber:
    const camadaAcesso = new CamadaAcesso();
    console.log('Sou um Cold Observable');
    subscriber.next(camadaAcesso.getValue()); // Emitirá "Testando"
});

Como já comentado, repare que o trecho de código acima, por si só não produz nenhuma informação porque não é executado até que alguém faça uma subscrição (lembre-se de que se não houver nenhum subscrição este o subscriber nunca será executado). Se fizermos uma única subscrição teremos o seguinte resultado:

myObservable.subscribe(msg => console.log(msg));
// Sou um Cold Observable
// Testando

Se houver duas subscrições (abaixo):

myObservable.subscribe(msg => console.log('A', msg));
myObservable.subscribe(msg => console.log('B', msg));

// Sou um Cold Observable
// A Testando
// Sou um Cold Observable
// B Testando

Cada vez que uma subscrição é efetuada também é criada uma instância da nossa fonte de dados, que neste exemplo é camadaAcesso, então significa que cada subscrição tem sua própria instância da camadaAcesso e por esta razão há duas mensagem Sou um Cold Observable.

Como cada subscrição (subscription) tem seu próprio subscriber, dizemos que o Observable é unicast pois os valores emitidos pelo subscriber não são compartilhados para várias subscrições. Com isto podemos afirmar que todo Cold Observable é unicast.

…While plain Observables are unicast (each subscribed Observer owns an independent execution of the Observable)

Veja este outro exemplo:

const myObservable = new Observable<number>(subscriber => {
    const rnd = Math.floor(Math.random() * 200) + 1;
    console.log('Efetuando uma requisição', rnd);
    subscriber.next(rnd);
});

myObservable
    .subscribe(a => console.log('Subscription A', a));
myObservable
    .subscribe(a => console.log('Subscription B', a));
    
// Resultado no console:
//
// Efetuando uma requisição 137
// Subscription A 137
// Efetuando uma requisição 8
// Subscription B 8

Novamente a fonte de dados é criada dentro do subscriber. Apesar de haver duas subscrições para o mesmo Observable repare que cada uma delas recebe um valor diferente.

Como isto pode ajudar/atrapalhar nossa aplicação?

Já repararam que o HttpClient, que utilizamos para efetuar as requisições Ajax, retorna um Observable em suas operações? Então o que aconteceria se dois componentes renderizados na mesma página se inscrevessem em um Observable que efetua uma requisição Ajax ao recurso XYZ? A resposta seria: teríamos duas requisições! Não vou entrar em detalhes porque isto é assunto para outro post mas vejam como é importante conhecer o que é um Cold Observable.

Cold Observables start emitting or creating values only when the subscription starts, like a typical YouTube video. Each subscriber will see the same sequence (or pattern) of events from start to finish.

Hot Observable

Ao contrário do Cold, um Hot Observable cria ou ativa sua fonte de informações fora do subscriber garantindo que todos que estejam o ouvindo receberão a mesma informação da mesma fonte naquele dado instante.

Hot Observables are always being updated with new values, like a live stream on YouTube. When you subscribe you start with the most recent value and only see future changes.

Então tomando novamente o último exemplo de código, mais acima, se modificarmos a posição da geração do número randômico para fora do Observable teremos um Hot Observable.

// Movido para fora do Subscriber
const rnd = Math.floor(Math.random() * 200) + 1;
const myObservable = new Observable<number>(subscriber => {
    // Trecho comentado:
    // const rnd = Math.floor(Math.random() * 200) + 1;
    console.log('Efetuando uma requisição', rnd);
    subscriber.next(rnd);
});

myObservable
    .subscribe(a => console.log('Subscription A', a));
myObservable
    .subscribe(a => console.log('Subscription B', a));
    
// Resultado no console:
//
// Efetuando uma requisição 120
// Subscription A 120
// Efetuando uma requisição 120
// Subscription B 120

Apesar de termos um Hot Observable e compartilharmos exatamente a mesma informação a todos os que efetuaram a subscrição ainda temos a execução independente de um Subscriber. Veja que a mensagem "Efetuando uma requisição 120" aparece duas vezes. Isto significa que a função do subscriber foi executada duas vezes.

Para resolver este tipo de problema podemos contar com a classe Subject do RxJS. Ela é um Observable e permite que os mesmos valores sejam emitidos para múltiplas subscrições. Veja o exemplo:

const mySubject = new Subject<number>();
mySubject
    .subscribe(a => console.log('Subscription A', a));
mySubject
    .subscribe(a => console.log('Subscription B', a));
    
// Criação da fonte de dados
const rdn = Math.floor(Math.random() * 200) + 1;
console.log('Efetuando uma requisição', rdn);

// Emissão de uma notificação:
mySubject.next(rdn);

// Resultado no console:
//
// Efetuando uma requisição 119
// Subscription A 119
// Subscription B 119

Veja que a criação da fonte de dados está fora do Observable e as subscrições recebem exatamente a mesma informação da mesma fonte.

Como este Observable (lembre-se de que um Subject é um Observable) tem a capacidade de enviar notificações para diversas subscrições através de uma única fonte, ele é chamado de multicast.

Como tornar um Observable Cold em Hot?

Agora que definimos o que é um Cold e um Hot Observable vamos ver como podemos fazer para tornar um Cold Observable em Hot. Veja o exemplo:

const cold = new Observable<string>(subscriber => {
    console.log('cold', 'passei aqui');
    subscriber.next('valor do cold');
    
    setTimeout(() => {
        console.log('cold', '2 segundos depois');
        console.log('cold', 'passei aqui');
        subscriber.next('valor do cold');
    }, 2000);
});

const hot = new Subject<string>();
hot.subscribe(val => console.log('Subscrição A', val));
hot.subscribe(val => console.log('Subscrição B', val));
cold.subscribe(hot);

// Resultado no console:
//
// cold passei aqui
// Subscrição A valor do cold
// Subscrição B valor do cold
// cold 2 segundos depois
// cold passei aqui
// Subscrição A valor do cold
// Subscrição B valor do cold

Fizemos uma subscrição no coldpassando como parâmetro um Subject (hot). Então no momento que efetuamos a subscrição no coldo subscriber foi executado instanciando nossa fonte de dados. Quando o coldemitiu uma notificação o hotfoi notificado e emitiu uma notificação para todos os seus subinscritos. Ficou confuso? Para que saber isto? onde vou usar isto? Talvez estas sejam as perguntas de quem está começando neste mundo, mas vamos com calma.

Lembram que mais acima eu citei o exemplo do HttpClient? Imagine que você criou uma camada que faz uma requisição AJAX e esta camada é utilizada por diversos componentes que, eventualmente podem estar na mesma página. O que vai acontecer? Cada vez que uma subscrição for efetuada será feita uma requisição, como já vimos na descrição do Cold Observable. O exemplo acima ilustra um meio de evitar que façamos estas requisições desnecessárias mas ainda não é o ideal!

Every Subject is an Observable. Given a Subject, you can subscribe to it, providing an Observer, which will start receiving values normally. From the perspective of the Observer, it cannot tell whether the Observable execution is coming from a plain unicast Observable or a Subject.

O RxJS fornece um operador para este tipo de tarefa, o multicast, que será abordado no próximo post.

A “multicasted Observable” passes notifications through a Subject which may have many subscribers, whereas a plain “unicast Observable” only sends notifications to a single Observer.

Considerações

O objetivo deste post foi deixar claro quais são as principais diferenças entre o Cold e Hot Observable. Particularmente, uma das melhores analogias para entender a diferença entre o Cold e Hot Observable que li foi:

Pense que o Hot Observable é uma estação de rádio, ela sempre está emitindo sinal independente da quantidade de ouvintes. Os ouvintes que sintonizarem na rádio ouvirão exatamente a mesma música naquele instante. Cold Observable é como um CD, você pode ouvir o CD do início independente de outras pessoas.

Links Interessantes: