padrão de ouvinte de evento na API - o que deve adicionar o mesmo ouvinte duas vezes?

8

Ao projetar uma API que fornece uma interface de escuta de eventos, parece haver duas maneiras conflitantes de tratar chamadas para adicionar / remover ouvintes:

  1. Várias chamadas para addListener adicionarão apenas um único ouvinte (como adicioná-lo a um conjunto); pode ser removido com uma única chamada para removeListener.

  2. Várias chamadas para addListener adicionarão um ouvinte a cada vez (como adicioná-lo a uma lista); deve ser equilibrado por várias chamadas para removeListener.

Encontrei um exemplo de cada um: (1) - A chamada DOM addEventListener nos navegadores apenas adiciona ouvintes uma vez, ignorando silenciosamente as solicitações para adicionar o mesmo ouvinte pela segunda vez e (2) - o .oncomportamento do jQuery adiciona ouvintes várias vezes .

A maioria das outras APIs de ouvintes parece usar (2), como ouvintes de eventos SWT e Swing. Se (1) for escolhido, há também a questão de saber se deve falhar silenciosamente ou com um erro quando houver uma solicitação para adicionar o mesmo ouvinte duas vezes.

Nas minhas implementações, costumo seguir com (2), uma vez que fornece uma interface mais limpa do tipo configuração / desmontagem e revela bugs em que a 'instalação' é feita acidentalmente duas vezes e é consistente com a maioria das implementações que já vi.

Isso me leva à minha pergunta - Existe uma arquitetura específica ou outro design subjacente que se presta melhor à outra implementação? (ou seja: por que o outro padrão existe?)

Krease
fonte
1
Para esclarecer: considere addListener(foo); addListener(foo); addListener(bar);. O seu caso nº 1 adiciona um fooe um bar, ou apenas bar(ou seja, barsobrescreve foocomo ouvinte)? No caso 2, foodispararia duas ou uma vez?
Apsillers
As implementações do caso # 1 com as quais eu estou familiarizado geralmente são feitas por referência - se foo== bar, ele seria substituído, caso contrário, ele teria um fooe um barcomo ouvintes. Se sempre sobrescrevesse, não seria um conjunto, mas um único objeto que representava um observador.
Krease
2
(2) não revelará nenhum erro, desde que você não proíba adicionar o mesmo ouvinte duas vezes - e, portanto, não haverá diferença real em (1).
Doc Brown
A escolha deve depender dos requisitos dos usuários da API; portanto, seria melhor perguntar a um deles. Quando você agora toma uma decisão de design, e um dos usuários usa sua API e informa que o design não funciona bem para ele, você tem a chance de mudar essa decisão mais tarde?
Doc Brown
@DocBrown - no caso específico, foi o motivo pelo qual fiz a pergunta, não temos muitas opções para mudar. Sei que não é um grande negócio usar uma opção ou outra, por isso a pergunta é mais conceitual - existem razões baseadas na arquitetura / design / confiabilidade (ou seja: além das preferências do usuário) para escolher um padrão sobre o outro ?
Krease

Respostas:

2

Se você tiver alguns eventos com problemas no gerenciamento de adicionar / remover, eu começaria a adicionar IDs.

Adicionar um ouvinte retorna um ID, a classe que o adicionou controla os IDs dos ouvintes adicionados e, quando precisar removê-los, chama remover ouvinte com esses IDs únicos.

Isso coloca os consumidores no controle, para que possam obter conformidade com o Princípio de menor espanto no comportamento.

Isso significa que adicionar o mesmo duas vezes o adiciona duas vezes, fornece um ID diferente para cada um e a remoção por ID remove apenas o associado a esse ID. Qualquer pessoa que consome a API esperaria esse comportamento ao ver os sigs.

Uma adição adicional em violação ao YAGNI seria um GetIds, onde você entregará um ouvinte e retornará uma lista de IDs associados a esse ouvinte, se ele for capaz de obter verificações de igualdade apropriadas, embora isso dependa do seu idioma: é igualdade de referência? , digite igualdade, igualdade de valor, etc? você precisa ter cuidado aqui, pois pode devolver IDs com os quais esse consumidor não deve remover ou interferir, portanto, essa exposição é perigosa e é desaconselhável e desnecessária, mas GetIDs é um possível complemento se você estiver com sorte.

Jimmy Hoffa
fonte
Não há necessariamente um problema em que há problemas com adições / remoções (como a pergunta deve se aplicar a qualquer programa, não apenas àquele em que estou trabalhando), embora eu definitivamente veja como esse padrão deixaria especificamente claro que a API é usando a opção # 2 (ou uma ligeira variação do mesmo, onde a remoção é por id em vez de pelo ouvinte)
Krease
Ter uma assinatura retornando um objeto que pode ser usado para cancelar a inscrição é, IMHO, a abordagem correta. Em estruturas orientadas a objetos com IDisposable, Autocloseableou outra interface desse tipo, o objeto de cancelamento de assinatura deve implementar essa interface de maneira segura para threads (sempre possível - se nada mais, colocando o assinante dentro do próprio objeto de cancelamento de inscrição e tendo seu método de cancelamento de inscrição invalidar esse referência e ter solicitações de assinatura ocasionalmente varre a lista de assinaturas em busca de assinaturas inativas).
Supercat
3

Primeiro, eu escolheria uma abordagem em que a ordem na qual os ouvintes são adicionados é exatamente a ordem em que serão chamados quando os eventos relacionados forem acionados. Se você decidir ir com (1), isso significa que você usa um conjunto ordenado, não apenas um conjunto.

Segundo, você deve esclarecer um objetivo geral de design: sua API deve seguir mais uma estratégia de "falha antecipada" ou uma estratégia de "perdão de erros"? Isso depende do ambiente de uso e dos cenários de uso da sua API. Geralmente, (desenvolvendo principalmente aplicativos de desktop), prefiro "travar mais cedo", mas às vezes é melhor tolerar algum tipo de erro para tornar o uso de uma API mais suave. Os requisitos, por exemplo, em aplicativos incorporados ou aplicativos de servidor podem ser diferentes. Talvez você discuta isso com um de seus possíveis usuários da API?

Para uma estratégia de "falha antecipada", use (2), mas proíba adicionar o mesmo ouvinte duas vezes, lance uma exceção se um ouvinte for adicionado novamente. Também lança uma exceção se alguém tentar remover um ouvinte que não está na lista.

Se você acha que uma estratégia de "perdoar erros" é mais apropriada no seu caso, você pode

  • ignore a adição dupla do mesmo ouvinte à lista - que é a opção (1) - ou

  • anexá-lo à lista como em (2), para que ele seja chamado duas vezes quando os eventos forem disparados

  • ou você o anexa, mas não chame o mesmo ouvinte duas vezes em caso de acionamento de evento

Observe que a remoção do ouvinte deve corresponder a isso - se você ignorar a adição dupla, também deve ignorar a remoção dupla. Se você permitir que o mesmo ouvinte seja adicionado duas vezes, deve ficar claro qual das duas entradas do ouvinte será removida quando uma chamada removeListener(foo). A última das três marcações é provavelmente a abordagem menos suscetível a erros entre essas sugestões; portanto, no caso de uma estratégia de "perdão de erros", eu aceitaria isso.

Doc Brown
fonte
Até agora, minha principal estratégia foi 'usar um padrão semelhante ao ambiente ao redor' - ie: o padrão mais comum usado no restante do código / idioma no qual estou integrando. A estratégia 'travar cedo' definitivamente capturaria muitos problemas em potencial, mas eu nunca vi isso usado na prática, por isso pode ser surpreendente para um usuário da API (embora, quanto mais eu penso sobre isso, eu considero isso um 'bom' surpresa uma vez que ajudaria a erros de captura)
Krease
@ Chris: Tenho certeza que você já viu "travar cedo" muito. Por exemplo, na maioria das linguagens mainstream modernas, você recebe exceções ao tentar escrever fora dos limites da matriz, tenta converter seqüências de caracteres em números que não são conversíveis e assim por diante.
Doc Brown
Eu estava me referindo especificamente no contexto do padrão ouvinte
Krease