Delegado vs Interfaces: ¿hay más aclaraciones disponibles?

23

Después de leer el artículo: Cuándo usar delegados en lugar de interfaces (Guía de programación de C #) , necesito ayuda para comprender los puntos que se detallan a continuación, que no me parecen tan claros (para mí). ¿Algún ejemplo o explicación detallada disponible para estos?

Use un delegado cuando:

  • Se utiliza un patrón de diseño de eventos.
  • Es deseable encapsular un método estático.
  • Se desea una composición fácil.
  • Una clase puede necesitar más de una implementación del método.

Use una interfaz cuando:

  • Hay un grupo de métodos relacionados que se pueden llamar.
  • Una clase solo necesita una implementación del método.

Mis preguntas son

  1. ¿Qué quieren decir con un patrón de diseño de eventos?
  2. ¿Cómo resulta fácil la composición si se usa un delegado?
  3. Si hay un grupo de métodos relacionados a los que se puede llamar, utilice la interfaz. ¿Qué beneficio tiene?
  4. Si una clase solo necesita una implementación del método, utilice la interfaz: ¿cómo se justifica en términos de beneficios?
WinW
fuente

Respuestas:

11

¿Qué quieren decir con un patrón de diseño de eventos?

Lo más probable es que se refieran a una implementación del patrón de observador, que es un constructo de lenguaje central en C #, expuesto como ' eventos '. Es posible escuchar eventos conectando un delegado a ellos. Como señaló Yam Marcovic, EventHandleres el tipo de delegado base convencional para eventos, pero se puede usar cualquier tipo de delegado.

¿Cómo resulta fácil la composición si se usa un delegado?

Esto probablemente solo se refiere a la flexibilidad que ofrecen los delegados. Puede 'componer' fácilmente cierto comportamiento. Con la ayuda de lambdas , la sintaxis para hacer esto también es muy concisa. Considere el siguiente ejemplo.

class Bunny
{
    Func<bool> _canHop;

    public Bunny( Func<bool> canHop )
    {
        _canHop = canHop;
    }

    public void Hop()
    {
        if ( _canHop() )  Console.WriteLine( "Hop!" );
    }
}

Bunny captiveBunny = new Bunny( () => IsBunnyReleased );
Bunny lazyBunny = new Bunny( () => !IsLazyDay );
Bunny captiveLazyBunny = new Bunny( () => IsBunnyReleased && !IsLazyDay );

Hacer algo similar con las interfaces requeriría que use el patrón de estrategia o una Bunnyclase base (abstracta) desde la que extienda conejitos más específicos.

Si hay un grupo de métodos relacionados a los que se puede llamar, utilice la interfaz. ¿Qué beneficio tiene?

Nuevamente, usaré conejitos para demostrar cómo sería más fácil.

interface IAnimal
{
    void Jump();
    void Eat();
    void Poo();
}

class Bunny : IAnimal { ... }
class Chick : IAnimal { ... }

// Using the interface.
IAnimal bunny = new Bunny();
bunny.Jump();  bunny.Eat();  bunny.Poo();
IAnimal chick = new Chick();
chick.Jump();  chick.Eat();  chick.Poo();

// Without the interface.
Action bunnyJump = () => bunny.Jump();
Action bunnyEat = () => bunny.Eat();
Action bunnyPoo = () => bunny.Poo();
bunnyJump(); bunnyEat(); bunnyPoo();
Action chickJump = () => chick.Jump();
Action chickEat = () => chick.Eat();
...

Si una clase solo necesita una implementación del método, utilice la interfaz: ¿cómo se justifica en términos de beneficios?

Para esto, considere el primer ejemplo con el conejito nuevamente. Si solo se necesita una implementación (nunca se requiere una composición personalizada), puede exponer este comportamiento como una interfaz. Nunca tendrá que construir las lambdas, solo puede usar la interfaz.

Conclusión

Los delegados ofrecen mucha más flexibilidad, mientras que las interfaces lo ayudan a establecer contratos sólidos. Por lo tanto, encuentro el último punto mencionado, "Una clase puede necesitar más de una implementación del método". , con mucho, el más relevante.

Una razón adicional para usar delegados es cuando desea exponer solo una parte de una clase desde la que no puede ajustar el archivo fuente.

Como ejemplo de tal escenario (máxima flexibilidad, no es necesario modificar las fuentes), considere esta implementación del algoritmo de búsqueda binaria para cualquier posible colección simplemente pasando dos delegados.

Steven Jeuris
fuente
12

Un delegado es algo así como una interfaz para una firma de método único, que no tiene que implementarse explícitamente como una interfaz normal. Puedes construirlo en la carrera.

Una interfaz es solo una estructura de lenguaje que representa algún contrato: "Por la presente garantizo que tengo disponibles los siguientes métodos y propiedades".

Además, no estoy completamente de acuerdo en que los delegados sean principalmente útiles cuando se usan como una solución para el patrón de observador / suscriptor. Sin embargo, es una solución elegante al "problema de verbosidad de Java".

Para sus preguntas:

1 y 2)

Si desea crear un sistema de eventos en Java, generalmente usa una interfaz para propagar el evento, algo como:

interface KeyboardListener
{
    void KeyDown(int key);
    void KeyUp(int key)
    void KeyPress(int key);
    .... and so on
}

Esto significa que su clase tendrá que implementar explícitamente todos estos métodos y proporcionar apéndices para todos ellos, incluso si simplemente desea implementarlos KeyPress(int key).

En C #, estos eventos se representarían como listas de delegados, ocultos por la palabra clave "evento" en c #, uno para cada evento singular. Esto significa que puede suscribirse fácilmente a lo que desee sin sobrecargar a su clase con métodos públicos "clave", etc.

+1 para los puntos 3-5.

Adicionalmente:

Los delegados son muy útiles cuando desea proporcionar, por ejemplo, una función de "mapa", que toma una lista y proyecta cada elemento en una nueva lista, con la misma cantidad de elementos, pero diferente de alguna manera. Esencialmente, IEnumerable. Seleccione (...).

IEnumerable.Select toma a Func<TSource, TDest>, que es un delegado que ajusta una función que toma un elemento TSource y transforma ese elemento en un elemento TDest.

En Java, esto debería implementarse utilizando una interfaz. A menudo no existe un lugar natural para implementar dicha interfaz. Si una clase contiene una lista que quiere transformar de alguna manera, no es muy natural que implemente la interfaz "ListTransformer", especialmente porque podría haber dos listas diferentes que deberían transformarse de diferentes maneras.

Por supuesto, podría usar clases anónimas que son un concepto similar (en Java).

Max
fuente
Considere la posibilidad de editar su publicación para responder realmente a sus preguntas
Yam Marcovic
@YamMarcovic Tienes razón, divagué un poco en forma libre en lugar de responder directamente a sus preguntas. Aún así, creo que explica un poco sobre la mecánica involucrada. Además, sus preguntas estaban un poco menos formadas cuando respondí la pregunta. : p
Max
Naturalmente. A mí también me sucede, y tiene su valor per se. Por eso solo lo sugerí , pero no me quejé. :)
Yam Marcovic
3

1) Patrones de eventos, bueno, el clásico es el patrón Observer, aquí hay un buen enlace de Microsoft Microsoft talk Observer

El artículo al que se vinculó no está muy bien escrito y hace las cosas más complicadas de lo necesario. El negocio estático es de sentido común, no puede definir una interfaz con miembros estáticos, por lo que si desea un comportamiento polimórfico en un método estático, usaría un delegado.

El enlace de arriba habla sobre los delegados y por qué son buenos para la composición, por lo que podría ayudar con su consulta.

Ian
fuente
3

En primer lugar, buena pregunta. Admiro su enfoque en la utilidad en lugar de aceptar ciegamente las "mejores prácticas". +1 por eso.

He leído esa guía antes. Debe recordar algo al respecto: es solo una guía, principalmente para los recién llegados a C # que saben programar pero no están tan familiarizados con la forma en que C # hace las cosas. No es tanto como una página de reglas, sino que es una página que describe cómo se suelen hacer las cosas. Y dado que ya se han hecho de esta manera en todas partes, puede ser una buena idea mantenerse constante.

Iré al grano y responderé tus preguntas.

En primer lugar, supongo que ya sabes qué es una interfaz. En cuanto a un delegado, es suficiente decir que es una estructura que contiene un puntero escrito a un método, junto con un puntero opcional al objeto que representa el thisargumento para ese método. En el caso de métodos estáticos, el último puntero es nulo.
También hay delegados de multidifusión, que son como los delegados, pero pueden tener varias de estas estructuras asignadas a ellos (lo que significa que una sola llamada a Invocar en un delegado de multidifusión invoca todos los métodos en su lista de invocación asignada).

¿Qué quieren decir con un patrón de diseño de eventos?

Significan usar eventos en C # (que tiene palabras clave especiales para implementar sofisticadamente este patrón extremadamente útil). Los eventos en C # son alimentados por delegados de multidifusión.

Cuando define un evento, como en este ejemplo:

class MyClass {
  // Note: EventHandler is just a multicast delegate,
  // that returns void and accepts (object sender, EventArgs e)!
  public event EventHandler MyEvent;

  public void DoSomethingThatTriggersMyEvent() {
    // ... some code
    var handler = MyEvent;
    if (handler != null)
      handler(this, EventArgs.Empty);
    // ... some other code
  }
}

El compilador realmente transforma esto en el siguiente código:

class MyClass {
  private EventHandler MyEvent = null;

  public void add_MyEvent(EventHandler value) {
    MyEvent += value;
  }

  public void remove_MyEvent(EventHandler value) {
    MyEvent -= value;
  }

  public void DoSomethingThatTriggersMyEvent() {
    // ... some code
    var handler = MyEvent;
    if (handler != null)
      handler(this, EventArgs.Empty);
    // ... some other code
  }
}

Luego se suscribe a un evento haciendo

MyClass instance = new MyClass();
instance.MyEvent += SomeMethodInMyClass;

Que se compila a

MyClass instance = new MyClass();
instance.add_MyEvent(new EventHandler(SomeMethodInMyClass));

Entonces eso está sucediendo en C # (o .NET en general).

¿Cómo resulta fácil la composición si se usa un delegado?

Esto se puede demostrar fácilmente:

Supongamos que tiene una clase que depende de un conjunto de acciones que se le pasarán. Puede encapsular esas acciones en una interfaz:

interface RequiredMethods {
  void DoX();
  int DoY();
};

Y cualquiera que quisiera pasar acciones a su clase primero tendría que implementar esa interfaz. O podría facilitarles la vida dependiendo de la siguiente clase:

sealed class RequiredMethods {
  public Action DoX;
  public Func<int> DoY();
}

De esta forma, las personas que llaman solo tienen que crear una instancia de RequiredMethods y vincular métodos a los delegados en tiempo de ejecución. Esto suele ser más fácil.

Esta forma de hacer las cosas es extremadamente beneficiosa en las circunstancias correctas. Piénselo: ¿por qué depender de una interfaz cuando lo único que realmente le importa es que le pasen una implementación?

Beneficios de usar interfaces cuando hay un grupo de métodos relacionados

Es beneficioso usar interfaces porque las interfaces normalmente requieren implementaciones explícitas en tiempo de compilación. Esto significa que crea una nueva clase.
Y si tiene un grupo de métodos relacionados en un solo paquete, es beneficioso que ese paquete sea reutilizable por otras partes del código. Entonces, si pueden simplemente crear una instancia de una clase en lugar de crear un conjunto de delegados, es más fácil.

Beneficios de usar interfaces si una clase solo necesita una implementación

Como se señaló anteriormente, las interfaces se implementan en tiempo de compilación, lo que significa que son más eficientes que invocar a un delegado (que es un nivel de indirección per se).

"Una implementación" podría significar una implementación que existe en un solo lugar bien definido.
De lo contrario, una implementación podría provenir de cualquier parte del programa que simplemente se ajuste a la firma del método. Eso permite una mayor flexibilidad, porque los métodos solo necesitan ajustarse a la firma esperada, en lugar de pertenecer a una clase que implemente explícitamente una interfaz específica. Pero esa flexibilidad puede tener un costo y, de hecho, rompe el principio de sustitución de Liskov , porque la mayoría de las veces desea ser explícito, ya que minimiza la posibilidad de accidentes. Al igual que la escritura estática.

El término también podría referirse a los delegados de multidifusión aquí. Los métodos declarados por las interfaces solo se pueden implementar una vez en una clase de implementación. Pero los delegados pueden acumular múltiples métodos, que se llamarán secuencialmente.

Así que, en general, parece que la guía no es lo suficientemente informativa y simplemente funciona como lo que es: una guía, no un libro de reglas. Algunos consejos pueden sonar un poco contradictorios. Depende de usted decidir cuándo es correcto aplicar qué. La guía parece solo darnos un camino general.

Espero que sus preguntas hayan sido respondidas a su satisfacción. Y de nuevo, felicitaciones por la pregunta.

Ñame Marcovic
fuente
2

Si mi memoria de .NET aún se mantiene, un delegado es básicamente una función ptr o functor. Agrega una capa de indirección a una llamada de función para que las funciones puedan sustituirse sin que el código de llamada tenga que cambiar. Esto es lo mismo que hace una interfaz, excepto que una interfaz empaqueta varias funciones juntas, y un implementador tiene que implementarlas juntas.

Un patrón de eventos, en términos generales, es aquel en el que algo responde a eventos de otras partes (como mensajes de Windows). El conjunto de eventos suele ser abierto, pueden presentarse en cualquier orden y no están necesariamente relacionados entre sí. Los delegados trabajan bien para esto, ya que cada evento puede llamar a una sola función sin necesidad de una referencia a una serie de objetos de implementación que también pueden contener numerosas funciones irrelevantes. Además (aquí es donde mi memoria .NET es vaga), creo que se pueden adjuntar múltiples delegados a un evento.

La composición, aunque no estoy muy familiarizado con el término, consiste básicamente en diseñar un objeto para que tenga múltiples subpartes o hijos agregados a los que se pasa el trabajo. Los delegados permiten que los niños se mezclen y se combinen de una manera más ad-hoc donde una interfaz puede ser excesiva o causar demasiado acoplamiento y la rigidez y fragilidad que conlleva.

El beneficio de una interfaz para métodos relacionados es que los métodos pueden compartir el estado del objeto de implementación. Las funciones de delegado no pueden compartir de manera tan limpia, o incluso contener, el estado.

Si una clase necesita una única implementación, una interfaz es más adecuada, ya que dentro de cualquier clase que implemente la colección completa, solo se puede hacer una implementación y obtendrá los beneficios de una clase de implementación (estado, encapsulación, etc.). Si la implementación puede cambiar debido al estado de tiempo de ejecución, los delegados funcionan mejor porque pueden cambiarse por otras implementaciones sin afectar los otros métodos. Por ejemplo, si hay tres delegados que tienen dos implementaciones posibles, necesitaría ocho clases diferentes que implementen la interfaz de tres métodos para dar cuenta de todas las combinaciones posibles de estados.

kylben
fuente
1

El patrón de diseño de "evento" (más conocido como el Patrón de Observador) le permite adjuntar múltiples métodos de la misma firma al delegado. Realmente no puedes hacer eso con una interfaz.

Sin embargo, no estoy convencido de que la composición sea más fácil para un delegado que una interfaz. Esa es una declaración muy extraña. Me pregunto si quiere decir porque puedes adjuntar métodos anónimos a un delegado.

pdr
fuente
1

La mayor aclaración que puedo ofrecer:

  1. Un delegado define una firma de función: qué parámetros aceptará una función coincidente y qué devolverá.
  2. Una interfaz trata con un conjunto completo de funciones, eventos, propiedades y campos.

Asi que:

  1. Cuando desee generalizar algunas funciones con la misma firma, use un delegado.
  2. Cuando desee generalizar algún comportamiento o calidad de una clase, use una interfaz.
John Fisher
fuente
1

Su pregunta sobre eventos ya ha sido bien cubierta. Y también es cierto que una interfaz puede definir varios métodos (pero en realidad no es necesario), mientras que un tipo de función solo impone restricciones a una función individual.

Sin embargo, la verdadera diferencia es:

  • los valores de función se corresponden con los tipos de función por subtipo estructural (es decir, compatibilidad implícita con la estructura demandada). Eso significa que si el valor de una función tiene una firma compatible con el tipo de función, es un valor válido para el tipo.
  • las instancias se corresponden con las interfaces por subtipo nominal (es decir, uso explícito de un nombre). Eso significa que, si una instancia es miembro de una clase, que implementa explícitamente la interfaz dada, la instancia es un valor válido para la interfaz. Sin embargo, si el objeto solo tiene todos los miembros demandados por las interfaces, y todos los miembros tienen firmas compatibles, pero no implementa explícitamente la interfaz, no es un valor válido para la interfaz.

Claro, pero ¿qué significa esto?

Tomemos este ejemplo (el código está en haXe ya que mi C # no es muy bueno):

class Collection<T> {
    /* a lot of code we don't care about now */
    public function filter(predicate:T->Bool):Collection<T> { 
         //build a new collection with all elements e, such that predicate(e) == true
    }
    public function remove(e:T):Bool {
         //removes an element from this collection, if contained and returns true, false otherwise
    }
}

Ahora, el método de filtro hace que sea fácil pasar convenientemente solo una pequeña pieza de lógica, que desconoce la organización interna de la colección, mientras que la colección no depende de la lógica que se le da. Excelente. Excepto por un problema:
La colección no dependen de la lógica. La colección inherentemente asume que la función pasada está diseñada para probar un valor contra una condición y devolver el éxito de la prueba. Tenga en cuenta que no todas las funciones, que toman un valor como argumento y devuelven un valor booleano, son en realidad meros predicados. Por ejemplo, el método remove de nuestra colección es tal función.
Supongamos que llamamos c.filter(c.remove). El resultado sería una colección con todos los elementos de cwhilecse queda vacio. Esto es desafortunado, porque naturalmente esperaríamos cque sea invariante.

El ejemplo está muy construido. Pero el problema clave es que el código que llama c.filtercon algún valor de función como argumento no tiene forma de saber si ese argumento es adecuado (es decir, en última instancia conservará la invariante). El código que creó el valor de la función puede o no saber que se interpretará como un predicado.

Ahora cambiemos las cosas:

interface Predicate<T> {
    function test(value:T):Bool;
}
class Collection<T> {
    /* a lot of code we don't care about now */
    public function filter(predicate:Predicate<T>):Collection<T> { 
         //build a new collection with all elements e, such that predicate.test(e) == true
    }
    public function remove(e:T):Bool {
         //removes an element from this collection, if contained and returns true, false otherwise
    }
}

¿Que ha cambiado? Lo que ha cambiado es que cualquier valor que se le filterhaya dado ahora ha firmado explícitamente el contrato de ser un predicado. Por supuesto, los programadores maliciosos o extremadamente estúpidos crean implementaciones de la interfaz que no están libres de efectos secundarios y, por lo tanto, no son predicados.
Pero lo que ya no puede suceder es que alguien vincula una unidad lógica de datos / código, que se interpreta erróneamente como un predicado debido a su estructura externa.

Entonces, para reformular lo que se dijo anteriormente en pocas palabras:

  • las interfaces significan el uso de la tipificación nominal y, por lo tanto, las relaciones explícitas
  • delegados significan usar tipificación estructural y por lo tanto relaciones implícitas

La ventaja de las relaciones explícitas es que puede estar seguro de ellas. La desventaja es que requieren la sobrecarga de lo explícito. Por el contrario, la desventaja de las relaciones implícitas (en nuestro caso, la firma de una función que coincide con la firma deseada), es que realmente no puede estar 100% seguro de que puede usar las cosas de esta manera. La ventaja es que puede establecer relaciones sin todos los gastos generales. Puedes simplemente juntar cosas rápidamente, porque su estructura lo permite. Eso es lo que significa composición fácil.
Es un poco como LEGO: simplemente puedes enchufar una figura LEGO de Star Wars en un barco pirata LEGO, simplemente porque la estructura exterior lo permite. Ahora puede sentir que eso está terriblemente mal, o podría ser exactamente lo que desea. Nadie te va a detener.

back2dos
fuente
1
Vale la pena mencionar que "implementa explícitamente la interfaz" (bajo "subtipo nominal") no es lo mismo que la implementación explícita o implícita de los miembros de la interfaz. En C #, las clases siempre deben declarar explícitamente las interfaces que implementan, como usted dice. Pero los miembros de la interfaz pueden implementarse explícitamente (por ejemplo, int IList.Count { get { ... } }) o implícitamente ( public int Count { get { ... } }). Esta distinción no es relevante para esta discusión, pero merece mención para evitar confundir a los lectores.
phoog
@phoog: Sí, gracias por la enmienda. Ni siquiera sabía que el primero era posible. Pero sí, la forma en que un idioma realmente aplica la implementación de la interfaz varía mucho. En Objective-C, por ejemplo, solo te dará una advertencia si no está implementado.
back2dos
1
  1. Un patrón de diseño de eventos implica una estructura que implica un mecanismo de publicación y suscripción. Los eventos son publicados por una fuente, cada suscriptor obtiene su propia copia del elemento publicado y es responsable de sus propias acciones. Ese es un mecanismo débilmente acoplado porque el editor ni siquiera tiene que saber que los suscriptores están allí. Y mucho menos tener suscriptores no es obligatorio (no puede ser ninguno)
  2. la composición es "más fácil" si se usa un delegado en comparación con una interfaz. Las interfaces definen una instancia de clase como un objeto de "tipo de controlador" donde, como una instancia de construcción de composición parece "tener un controlador", por lo tanto, la apariencia de la instancia está menos restringida mediante el uso de un delegado y se vuelve más flexible.
  3. En el comentario a continuación, descubrí que necesitaba mejorar mi publicación, consulte esto como referencia: http://bytes.com/topic/c-sharp/answers/252309-interface-vs-delegate
Carlo Kuip
fuente
1
3. ¿Por qué los delegados requerirían más casting y por qué un acoplamiento más estrecho es un beneficio? 4. No tiene que usar eventos para usar delegados, y con los delegados es como mantener un método: sabe que es el tipo de retorno y los tipos de argumentos. Además, ¿por qué un método declarado en una interfaz facilitaría el manejo de errores que un método adjunto a un delegado?
Yam Marcovic
En el caso de la especificación de un delegado, tiene razón. Sin embargo, en el caso del manejo de eventos (al menos eso es a lo que se refiere mi referencia), siempre debe heredar algún tipo de eventoArgs para actuar como contenedor de tipos personalizados, por lo que en lugar de poder probar de forma segura un valor que siempre tiene que desenvolver su tipo antes de manejar los valores.
Carlo Kuip
En cuanto a la razón por la que creo que un método definido en una interfaz facilitaría el manejo de errores es que el compilador pueda verificar los tipos de excepción lanzados desde ese método. Sé que no es una evidencia convincente, pero ¿tal vez debería indicarse como preferencia?
Carlo Kuip
1
En primer lugar, estábamos hablando de delegados frente a interfaces, no de eventos frente a interfaces. En segundo lugar, hacer que un evento se base en un delegado de EventHandler es solo una convención, y de ninguna manera es obligatorio. Pero, de nuevo, hablar de eventos aquí es como perder el punto. En cuanto a su segundo comentario, las excepciones no están marcadas en C #. Te estás confundiendo con Java (que por cierto ni siquiera tiene delegados).
Yam Marcovic
Encontré un hilo sobre este tema que explica diferencias importantes: bytes.com/topic/c-sharp/answers/252309-interface-vs-delegate
Carlo Kuip