patrón de escucha de eventos en la API: ¿qué debería hacer dos veces al mismo escucha?

8

Al diseñar una API que proporciona una interfaz de escucha de eventos, parece que hay dos formas conflictivas de tratar las llamadas para agregar / quitar escuchas:

  1. Múltiples llamadas a addListener solo agregarán un único oyente (como agregarlo a un conjunto); se puede eliminar con una sola llamada a removeListener.

  2. Múltiples llamadas a addListener agregarán un oyente cada vez (como agregarlo a una lista); debe estar equilibrado por varias llamadas para eliminarListener.

He encontrado un ejemplo de cada uno: (1) - La llamada DOM addEventListener en los navegadores solo agrega oyentes una vez, ignorando silenciosamente las solicitudes para agregar el mismo oyente por segunda vez y (2) - El .oncomportamiento de jQuery agrega oyentes varias veces .

La mayoría de las otras API de escucha parecen usar (2), como SWT y oyentes de eventos Swing. Si se elige (1), también está la cuestión de si debería fallar silenciosamente o con un error cuando hay una solicitud para agregar el mismo oyente dos veces.

En mis implementaciones, tiendo a seguir con (2) ya que proporciona una interfaz de tipo de configuración / desmontaje más limpia y revela errores en los que la 'configuración' se realiza involuntariamente dos veces, y es consistente con la mayoría de las implementaciones que he visto.

Esto me lleva a mi pregunta: ¿hay una arquitectura particular u otro diseño subyacente que se preste mejor a la otra implementación? (es decir, ¿por qué existe el otro patrón?)

Krease
fuente
1
Para aclarar: considerar addListener(foo); addListener(foo); addListener(bar);. ¿Su caso # 1 agrega uno fooy uno bar, o solo bar(es decir, se barsobrescribe foocomo el oyente)? En el caso # 2, ¿se foodispararía dos veces o una vez?
apsillers
Las implementaciones del caso # 1 con las que estoy familiarizado generalmente van por referencia: si foo== bar, entonces se sobrescribiría, de lo contrario, tendría uno fooy uno barcomo oyentes. Si siempre sobrescribe, no sería un conjunto, sino un solo objeto que representara un observador.
Krease
2
(2) no revelará ningún error siempre que no prohíba agregar el mismo oyente dos veces, y entonces no hay una diferencia real con (1).
Doc Brown
La elección debe depender de los requisitos de los usuarios de su API, por lo que normalmente sería mejor preguntar a uno de ellos. Cuando ahora toma una decisión de diseño, y uno de los usuarios usa su API y le dice que el diseño no funciona bien para él, ¿tiene la oportunidad de cambiar esa decisión más adelante?
Doc Brown
@DocBrown: en el caso específico por el que hice la pregunta, no tenemos muchas opciones para cambiar. Sé que no es un gran problema usar una opción u otra, por lo que la pregunta es más conceptual: ¿hay alguna razón basada en la arquitectura / diseño / confiabilidad (es decir, además de las preferencias del usuario) para elegir un patrón sobre el otro ?
Krease

Respuestas:

2

Si tiene algunos eventos con los que tiene problemas para administrar las adiciones / eliminaciones, comenzaría a agregar ID.

Agregar un escucha devuelve un ID, la clase que lo agregó realiza un seguimiento de los ID de los oyentes que se agrega y cuando necesita eliminarlos, llama a eliminar escucha con ese / esos ID (s) únicos.

Esto pone a los consumidores en control para que puedan cumplir con el Principio de Menos Asombro en el comportamiento.

Esto significa que agregar el mismo dos veces lo agrega dos veces, proporciona una ID diferente para cada una, y la eliminación por ID elimina solo la asociada con esa ID. Cualquiera que consuma la API esperaría este comportamiento cuando viera las señales.

Una adición adicional en violación de YAGNI sería un GetIds donde le entregue un oyente y devuelva una lista de ID asociados con ese oyente si es capaz de obtener las verificaciones de igualdad adecuadas, aunque eso depende de su idioma: ¿es igualdad de referencia? , tipo igualdad, valor igualdad, etc. debe tener cuidado aquí porque puede devolver identificaciones que ese consumidor no debería eliminar o entrometerse, por lo tanto, esta exposición es peligrosa y no es aconsejable y debe ser innecesaria, pero GetID es una posible adición si se siente afortunado.

Jimmy Hoffa
fuente
No hay necesariamente un problema en el que haya problemas con las adiciones / eliminaciones (ya que la pregunta debería aplicarse a cualquier programa, no solo en el que estoy trabajando), aunque definitivamente veo cómo este patrón aclararía específicamente que la API es usando la opción # 2 (o una ligera variación de la misma, donde la eliminación es por id en lugar de por el oyente)
Krease
Tener una suscripción que devuelva un objeto que pueda usarse para cancelar la suscripción es, en mi humilde opinión, el enfoque correcto. En los marcos orientados a objetos con IDisposable, Autocloseableo en otra interfaz, el objeto de anulación de suscripción debe implementar la interfaz en la moda thread-safe (siempre es posible - si nada más colocando el abonado dentro de la propia objeto de darse de baja, y que tiene su método de cancelación de suscripción invalidate que referencia, y tener solicitudes de suscripción ocasionalmente escanea la lista de suscripciones en busca de suscripciones muertas).
supercat
3

Primero, elegiría un enfoque donde el orden en que se agregan los oyentes es exactamente el orden en que se los llamará cuando se activen los eventos relacionados. Si decide ir con (1), eso significará que usa un conjunto ordenado, no solo un conjunto.

En segundo lugar, debe aclarar un objetivo de diseño general: ¿su API seguirá más una estrategia de "bloqueo anticipado" o una estrategia de "perdón de errores"? Esto depende del entorno de uso y los escenarios de uso de su API. En general, (desarrollando principalmente aplicaciones de escritorio) prefiero "bloquear antes", pero a veces es mejor tolerar algún tipo de error para hacer que el uso de una API sea más fluido. Los requisitos, por ejemplo, en aplicaciones integradas o aplicaciones de servidor pueden ser diferentes. ¿Quizás hable de esto con uno de sus usuarios potenciales de API?

Para una estrategia de "bloqueo anticipado", use (2), pero prohíba agregar el mismo oyente dos veces, arroje una excepción si se agrega un oyente nuevamente. También arroje una excepción si intenta eliminar un oyente que no está en la lista.

Si cree que una estrategia de "perdón de errores" es más apropiada en su caso, podría

  • ignore la doble adición del mismo oyente a la lista, que es la opción (1), o

  • añádalo a la lista como en (2), por lo que se llamará dos veces cuando se activen los eventos

  • o lo agrega, pero no llame al mismo oyente dos veces en caso de activación de eventos

Tenga en cuenta que la eliminación del oyente debe corresponder a eso: si ignora la doble adición, también debe ignorar la doble eliminación. Si permite que se agregue el mismo oyente dos veces, debe quedar claro cuál de las dos entradas del oyente se eliminará cuando una llame removeListener(foo). La última de las tres viñetas es probablemente el enfoque menos propenso a errores entre esas sugerencias, por lo que en caso de una estrategia de "perdón de errores", iría con eso.

Doc Brown
fuente
Mi estrategia principal hasta ahora ha sido 'usar un patrón similar al entorno circundante', es decir: el patrón más común utilizado en el resto del código / lenguaje con el que me estoy integrando. La estrategia de "bloqueo anticipado" definitivamente detectaría muchos problemas potenciales, pero en realidad nunca he visto esto usado en la práctica, por lo que puede ser sorprendente para un usuario de API (aunque, cuanto más lo pienso, lo consideraría una "buena" sorpresa ya que ayudaría a atrapar errores)
Krease
@ Chris: Estoy bastante seguro de que has visto mucho "chocar temprano". Por ejemplo, en la mayoría de los idiomas principales modernos, obtienes excepciones cuando intentas escribir fuera de los límites de la matriz, intentas convertir cadenas en números que no son convertibles, etc.
Doc Brown
Me refería específicamente a él en el contexto del patrón de escucha
Krease