Boas práticas para o padrão de desenho do observador

No .NET, o padrão de design do observador é implementado como um conjunto de interfaces. A System.IObservable<T> interface representa o provedor de dados, que também é responsável por fornecer uma IDisposable implementação que permite que os observadores cancelem a assinatura de notificações. A System.IObserver<T> interface representa o observador.

Este artigo descreve as melhores práticas a seguir quando implementa o padrão de design do observador com estas interfaces.

Considere alternativas antes de implementar

As interfaces IObservable<T> e IObserver<T> são adequadas para cenários de notificação baseados em push, mas outros padrões .NET podem ser mais adequados. Para uma notificação simples dentro de uma única aplicação, use eventos. Para sequências baseadas em pull assíncronas onde o consumidor controla o ritmo, use IAsyncEnumerable<T>. Para padrões produtor-consumidor com contrapressão, use System.Threading.Channels. Para composição, filtragem e transformação de eventos complexos, use o pacote System.Reactive (Rx.NET) em vez de implementar diretamente IObservable<T>. Para mais informações, consulte Padrão de design do observador.

Use um tipo separado para os dados de notificação

O objeto que contém os dados que o provedor envia aos seus observadores corresponde ao parâmetro de tipo genérico de IObservable<T> e IObserver<T>. Embora este objeto possa ser igual à IObservable<T> implementação, defina-o como um tipo separado. Um tipo de dado dedicado mantém as responsabilidades do fornecedor separadas da carga útil de notificação e facilita a evolução da API.

Não confie na ordem de notificação

A ordem em que os observadores recebem notificações não está definida. O fornecedor é livre de usar qualquer método para determinar a ordem, por isso não escreva observadores que dependam de serem notificados antes ou depois de outro observador.

Torne a subscrição e a eliminação seguras em ambiente multithread

Normalmente, um fornecedor implementa o IObservable<T>.Subscribe método adicionando um observador a uma lista de assinantes representado por um objeto de coleção, e implementa o IDisposable.Dispose método removendo o observador dessa lista. Um observador pode chamar esses métodos a qualquer momento. O contrato de fornecedor/observador não especifica quem é responsável por cancelar a subscrição após o método de IObserver<T>.OnCompleted callback, por isso o fornecedor e o observador podem tentar remover o mesmo membro da lista.

Para evitar condições de corrida, torne os métodos Subscribe e Dispose seguros em ambientes multithread. Normalmente, isso envolve o uso de uma coleção simultânea ou um bloqueio. Implementações que não são thread-safe devem documentar explicitamente que não o são.

Documente quaisquer garantias de contrato extra

Especifique quaisquer garantias extra numa camada por cima do contrato de prestador/observador. Quando impões outros requisitos, aponta-os claramente para que os utilizadores não fiquem confusos com o contrato de observador.

Tratar exceções como informação

Devido ao acoplamento flexível entre um provedor de dados e um observador, as exceções no padrão de design do observador destinam-se a ser informativas. Esta característica afeta a forma como os fornecedores e observadores lidam com exceções.

Chame o OnError apenas quando as atualizações não podem continuar

O OnError método pretende ser uma mensagem informativa para os observadores, tal como o IObserver<T>.OnNext método. No entanto, o OnNext método fornece ao observador dados atuais ou atualizados, enquanto indica OnError que o fornecedor não pode fornecer dados válidos.

Siga estas melhores práticas ao tratar de exceções e chame o OnError método:

  • O provedor deve lidar com suas próprias exceções se tiver quaisquer requisitos específicos.
  • O fornecedor não deve esperar nem exigir que os observadores tratem das exceções de qualquer forma específica.
  • O provedor deve chamar o OnError método quando ele lida com uma exceção que comprometa sua capacidade de fornecer atualizações. Transmitir informações sobre tais exceções ao observador. Noutros casos, não é necessário notificar os observadores de uma exceção.

Depois de o fornecedor invocar o método OnError ou IObserver<T>.OnCompleted, não deverá haver mais notificações e o fornecedor pode anular a subscrição dos seus observadores. No entanto, os observadores também podem anular a subscrição a qualquer momento, incluindo antes e depois de receberem uma notificação OnError ou IObserver<T>.OnCompleted. O padrão de desenho do observador não dita se o fornecedor ou o observador é responsável por cancelar a subscrição, pelo que ambos podem tentar cancelar. Normalmente, quando os observadores cancelam a subscrição, são removidos da coleção dos assinantes. Numa aplicação de execução única, a implementação IDisposable.Dispose deve garantir que uma referência ao objeto é válida e que o objeto pertence à coleção de subscritores antes de o tentar remover. Numa aplicação com múltiplas threads, use um bloqueio para proteger a coleção de observadores.

Trate as notificações OnError como informativas para os observadores

Quando um observador recebe uma notificação de erro de um fornecedor, este deve tratar a exceção como informativa e não deve ser obrigado a tomar qualquer ação específica.

Siga estas boas práticas ao responder a uma invocação de método de um OnError fornecedor:

  • Não lance exceções de implementações de interface como OnNext ou OnError. Se o observador lançar exceções, espere que essas exceções não sejam tratadas.
  • Para preservar a pilha de chamadas, um observador que pretenda lançar um objeto Exception que lhe foi passado no método OnError deve encapsular a exceção antes de a lançar. Use um objeto de exceção padrão para este propósito.

Não desfaça o registo no método Subscrever

Não tente cancelar o registo no IObservable<T>.Subscribe método, pois isso pode resultar numa referência nula.

Anexar um observador a um único fornecedor

Embora possa anexar um observador a múltiplos fornecedores, o padrão recomendado é anexar uma IObserver<T> instância apenas a uma IObservable<T> instância.