Delegado x Interfaces - Há mais esclarecimentos disponíveis?


23

Depois de ler o artigo - Quando usar delegados em vez de interfaces (Guia de Programação em C #) , preciso de ajuda para entender os pontos abaixo, que achei que não eram tão claros (para mim). Existem exemplos ou explicações detalhadas disponíveis para estes?

Use um delegado quando:

  • Um padrão de design de eventos é usado.
  • É desejável encapsular um método estático.
  • Fácil composição é desejada.
  • Uma classe pode precisar de mais de uma implementação do método.

Use uma interface quando:

  • Há um grupo de métodos relacionados que podem ser chamados.
  • Uma classe precisa apenas de uma implementação do método.

Minhas perguntas são,

  1. O que eles querem dizer com um padrão de design de eventos?
  2. Como a composição se torna fácil se um delegado é usado?
  3. se houver um grupo de métodos relacionados que possa ser chamado, use a interface - que benefício ele tem?
  4. se uma classe precisa apenas de uma implementação do método, use a interface - como isso se justifica em termos de benefícios?

Respostas:


11

O que eles querem dizer com um padrão de design de eventos?

Eles provavelmente se referem a uma implementação do padrão observador, que é uma construção de linguagem principal em C #, exposta como ' eventos '. É possível ouvir eventos conectando um delegado a eles. Como Yam Marcovic apontou, EventHandleré o tipo de delegado base convencional para eventos, mas qualquer tipo de delegado pode ser usado.

Como a composição se torna fácil se um delegado é usado?

Provavelmente isso se refere apenas à flexibilidade que os delegados oferecem. Você pode facilmente 'compor' determinado comportamento. Com a ajuda de lambdas , a sintaxe para fazer isso também é muito concisa. Considere o seguinte exemplo.

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 );

Fazer algo semelhante com interfaces requer que você use o padrão de estratégia ou use uma Bunnyclasse base (abstrata) a partir da qual você estende coelhos mais específicos.

se houver um grupo de métodos relacionados que possa ser chamado, use a interface - que benefício ele tem?

Mais uma vez, usarei coelhos para demonstrar como seria mais 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();
...

se uma classe precisa apenas de uma implementação do método, use a interface - como isso se justifica em termos de benefícios?

Para isso, considere o primeiro exemplo com o coelho novamente. Se apenas uma implementação for necessária - nenhuma composição personalizada será necessária -, você poderá expor esse comportamento como uma interface. Você nunca precisará construir as lambdas, basta usar a interface.

Conclusão

Os delegados oferecem muito mais flexibilidade, enquanto as interfaces ajudam a estabelecer contratos firmes. Portanto, acho o último ponto mencionado: "Uma classe pode precisar de mais de uma implementação do método". , de longe o mais relevante.

Um motivo adicional para usar delegados é quando você deseja expor apenas parte de uma classe da qual não é possível ajustar o arquivo de origem.

Como exemplo de um cenário desse tipo (flexibilidade máxima, sem necessidade de modificar fontes), considere esta implementação do algoritmo de pesquisa binária para qualquer coleção possível, passando apenas dois delegados.


12

Um delegado é algo como uma interface para uma única assinatura de método, que não precisa ser explicitamente implementada como uma interface regular. Você pode construí-lo em execução.

Uma interface é apenas uma estrutura de linguagem que representa algum contrato - "Garanto que disponibilizo os seguintes métodos e propriedades".

Além disso, não concordo completamente que os delegados sejam úteis principalmente quando usados ​​como uma solução para o padrão observador / assinante. É, no entanto, uma solução elegante para o "problema da verbosidade em java".

Para suas perguntas:

1 e 2)

Se você deseja criar um sistema de eventos em Java, normalmente usa uma interface para propagar o evento, algo como:

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

Isso significa que sua classe precisará implementar explicitamente todos esses métodos e fornecer stubs para todos eles, mesmo se você simplesmente deseja implementar KeyPress(int key).

Em C #, esses eventos seriam representados como listas de delegados, ocultos pela palavra-chave "event" em c #, uma para cada evento singular. Isso significa que você pode se inscrever facilmente no que deseja, sem sobrecarregar sua classe com métodos públicos "Chave" etc.

+1 para os pontos 3-5.

Além disso:

Os delegados são muito úteis quando você deseja fornecer, por exemplo, uma função "map", que leva uma lista e projeta cada elemento em uma nova lista, com a mesma quantidade de elementos, mas diferente de alguma forma. Essencialmente, IEnumerable.Select (...).

IEnumerable.Select pega um Func<TSource, TDest>, que é um delegado que quebra uma função que pega um elemento TSource e transforma esse elemento em um elemento TDest.

Em Java, isso teria que ser implementado usando uma interface. Geralmente, não há lugar natural para implementar essa interface. Se uma classe contém uma lista que deseja transformar de alguma forma, não é muito natural que implemente a interface "ListTransformer", principalmente porque pode haver duas listas diferentes que devem ser transformadas de maneiras diferentes.

Obviamente, você pode usar classes anônimas, que são um conceito semelhante (em java).


Considere editar sua postagem para realmente responder às perguntas dele.
Yam Marcovic

@YamMarcovic Você está certo, eu divaguei um pouco de forma livre em vez de responder diretamente às perguntas dele. Ainda assim, acho que explica um pouco sobre a mecânica envolvida. Além disso, suas perguntas foram um pouco menos formadas quando eu respondi a pergunta. : p
Max

Naturalmente. Acontece comigo também e tem seu valor em si. Foi por isso que eu apenas sugeri , mas não reclamei. :)
Yam Marcovic

3

1) Padrões de eventos, bem, o clássico é o padrão Observer, aqui está um bom link da Microsoft Microsoft talk Observer

O artigo ao qual você vinculou não está muito bem escrito e torna as coisas mais complicadas do que o necessário. O negócio estático é senso comum, você não pode definir uma interface com membros estáticos; portanto, se desejar um comportamento polimórfico em um método estático, use um representante.

O link acima fala sobre os delegados e por que existem boas composições para ajudar na sua consulta.


3

Primeiro de tudo, boa pergunta. Admiro seu foco na utilidade, em vez de aceitar cegamente as "melhores práticas". +1 para isso.

Eu já li esse guia antes. Você precisa se lembrar de algo sobre isso - é apenas um guia, principalmente para iniciantes em C # que sabem como programar, mas não estão tão familiarizados com a maneira como fazem as coisas em C #. Não é apenas uma página de regras, mas uma página que descreve como as coisas já são feitas. E como eles já foram feitos dessa maneira em todos os lugares, pode ser uma boa ideia permanecer consistente.

Vou direto ao ponto, respondendo às suas perguntas.

Primeiro de tudo, suponho que você já saiba o que é uma interface. Quanto a um delegado, basta dizer que é uma estrutura que contém um ponteiro digitado para um método, juntamente com um ponteiro opcional para o objeto que representa o thisargumento desse método. No caso de métodos estáticos, o último ponteiro é nulo.
Também existem delegados de difusão seletiva, que são iguais aos delegados, mas podem ter várias dessas estruturas atribuídas a eles (ou seja, uma única chamada para Invocar em um delegado de difusão seletiva invoca todos os métodos em sua lista de chamadas atribuídas).

O que eles querem dizer com um padrão de design de eventos?

Eles significam o uso de eventos em C # (que possui palavras-chave especiais para implementar sofisticadamente esse padrão extremamente útil). Eventos em C # são alimentados por delegados multicast.

Quando você define um evento, como neste exemplo:

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
  }
}

O compilador realmente transforma isso no seguinte 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
  }
}

Você então se inscreve em um evento fazendo

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

Que compila até

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

Então, isso está ocorrendo em C # (ou .NET em geral).

Como a composição se torna fácil se um delegado é usado?

Isso pode ser facilmente demonstrado:

Suponha que você tenha uma classe que depende de um conjunto de ações a serem passadas para ela. Você pode encapsular essas ações em uma interface:

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

E qualquer pessoa que desejasse passar ações para sua classe teria que implementar essa interface. Ou você pode facilitar a vida deles dependendo da classe a seguir:

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

Dessa forma, os chamadores precisam apenas criar uma instância de RequiredMethods e ligar os métodos aos delegados em tempo de execução. Isso geralmente é mais fácil.

Essa maneira de fazer as coisas é extremamente benéfica nas circunstâncias certas. Pense nisso - por que depender de uma interface quando tudo o que realmente importa é ter uma implementação passada para você?

Benefícios do uso de interfaces quando há um grupo de métodos relacionados

É benéfico usar interfaces, porque normalmente requerem implementações explícitas em tempo de compilação. Isso significa que você cria uma nova classe.
E se você tiver um grupo de métodos relacionados em um único pacote, é benéfico que esse pacote seja reutilizável por outras partes do código. Portanto, se eles podem simplesmente instanciar uma classe em vez de criar um conjunto de delegados, é mais fácil.

Benefícios do uso de interfaces se uma classe precisar apenas de uma implementação

Como observado anteriormente, as interfaces são implementadas em tempo de compilação - o que significa que são mais eficientes do que chamar um delegado (que é um nível de indireção per se).

"Uma implementação" pode significar uma implementação que existe em um único local bem definido.
Caso contrário, uma implementação pode vir de qualquer lugar do programa, o que acontece apenas com a assinatura do método. Isso permite mais flexibilidade, porque os métodos precisam apenas estar em conformidade com a assinatura esperada, em vez de pertencer a uma classe que implementa explicitamente uma interface específica. Mas essa flexibilidade pode ter um custo e, na verdade, quebra o princípio da Substituição de Liskov , porque na maioria das vezes você deseja explicitação, porque minimiza a chance de acidentes. Assim como a digitação estática.

O termo também pode se referir a delegados multicast aqui. Os métodos declarados por interfaces podem ser implementados apenas uma vez em uma classe de implementação. Mas os delegados podem acumular vários métodos, que serão chamados seqüencialmente.

Portanto, no geral, parece que o guia não é informativo o suficiente e simplesmente funciona como é - um guia, não um livro de regras. Alguns conselhos podem realmente parecer um pouco contraditórios. Cabe a você decidir quando é certo aplicar o quê. O guia parece nos dar apenas um caminho geral.

Espero que suas perguntas tenham sido respondidas para sua satisfação. E, novamente, parabéns pela pergunta.


2

Se minha memória do .NET ainda se mantiver, um delegado é basicamente uma função ptr ou functor. Ele adiciona uma camada de indireção a uma chamada de função para que as funções possam ser substituídas sem que o código de chamada precise ser alterado. É a mesma coisa que uma interface faz, exceto que uma interface reúne várias funções e um implementador precisa implementá-las.

Em geral, um padrão de evento é aquele em que algo responde a eventos de outros países (como mensagens do Windows). O conjunto de eventos geralmente é aberto, eles podem ocorrer em qualquer ordem e não estão necessariamente relacionados um ao outro. Os delegados trabalham bem para isso, pois cada evento pode chamar uma única função sem precisar de uma referência a uma série de objetos de implementação que também podem conter inúmeras funções irrelevantes. Além disso (é aqui que minha memória do .NET é vaga), acho que vários delegados podem ser anexados a um evento.

A composição, embora eu não esteja muito familiarizado com o termo, é basicamente projetar um objeto para ter várias sub-partes ou filhos agregados aos quais o trabalho é passado. Os delegados permitem que os filhos sejam misturados e combinados de uma maneira mais ad-hoc, onde uma interface pode ser um exagero ou causar muito acoplamento e a rigidez e fragilidade que o acompanham.

O benefício para uma interface para métodos relacionados é que os métodos podem compartilhar o estado do objeto de implementação. As funções de delegado não podem compartilhar de maneira tão limpa, nem mesmo conter, estado.

Se uma classe precisa de uma única implementação, uma interface é mais adequada, pois em qualquer classe que implemente toda a coleção, apenas uma implementação pode ser feita e você obtém os benefícios de uma classe de implementação (estado, encapsulamento etc.). Se a implementação pode mudar devido ao estado de tempo de execução, os delegados funcionam melhor porque podem ser trocados por outras implementações sem afetar os outros métodos. Por exemplo, se houver três delegados, cada um com duas implementações possíveis, você precisará de oito classes diferentes para implementar a interface de três métodos para contabilizar todas as combinações possíveis de estados.


1

O padrão de design "evento" (mais conhecido como Padrão do Observador) permite anexar vários métodos da mesma assinatura ao delegado. Você realmente não pode fazer isso com uma interface.

Não estou convencido de que a composição seja mais fácil para um delegado do que uma interface. Essa é uma afirmação muito estranha. Gostaria de saber se ele quis dizer porque você pode anexar métodos anônimos a um delegado.


1

O maior esclarecimento que posso oferecer:

  1. Um delegado define uma assinatura de função - quais parâmetros uma função de correspondência aceitará e o que ela retornará.
  2. Uma interface lida com todo um conjunto de funções, eventos, propriedades e campos.

Tão:

  1. Quando você deseja generalizar algumas funções com a mesma assinatura - use um representante.
  2. Quando você deseja generalizar algum comportamento ou qualidade de uma classe - use uma interface.

1

Sua pergunta sobre eventos já foi bem abordada. E também é verdade que uma interface pode definir vários métodos (mas na verdade não precisa), enquanto um tipo de função apenas impõe restrições a uma função individual.

A diferença real, porém, é:

  • Os valores de função são correspondidos aos tipos de função por subtipagem estrutural (isto é, compatibilidade implícita à estrutura demandada). Isso significa que, se um valor de função tiver uma assinatura compatível com o tipo de função, será um valor válido para o tipo.
  • as instâncias são correspondidas às interfaces por subtipagem nominal (ou seja, uso explícito de um nome). Isso significa que, se uma instância é membro de uma classe, que implementa explicitamente a interface fornecida, a instância é um valor válido para a interface. No entanto, se o objeto tiver apenas todos os membros exigidos pelas interfaces e todos os membros tiverem assinaturas compatíveis, mas não implementar explicitamente a interface, não será um valor válido para a interface.

Claro, mas o que isso significa?

Vamos pegar este exemplo (o código está no haXe, pois meu c # não é muito bom):

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
    }
}

Agora, o método de filtragem facilita a transmissão conveniente de apenas uma pequena parte da lógica, que não tem conhecimento da organização interna da coleção, enquanto a coleção não depende da lógica que lhe é dada. Ótimo. Exceto por um problema:
A coleção não dependem da lógica. A coleção supõe inerentemente que a função transmitida foi projetada para testar um valor em relação a uma condição e retornar o sucesso do teste. Observe que nem todas as funções, que recebem um valor como argumento e retornam um booleano, na verdade são meros predicados. Por exemplo, o método remove da nossa coleção é uma função.
Suponha que ligamos c.filter(c.remove). O resultado seria uma coleção com todos os elementos de cwhilecse torna vazio. Isso é lamentável, porque naturalmente esperaríamos cser invariáveis.

O exemplo é muito construído. Mas o principal problema é que o código que chama c.filtercom algum valor de função como argumento não tem como saber se esse argumento é adequado (ou seja, acabará por conservar a invariante). O código que criou o valor da função pode ou não saber, que será interpretado como um predicado.

Agora vamos mudar as coisas:

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
    }
}

O que mudou? O que mudou é que, seja qual for o valor agora atribuído filter, assinou explicitamente o contrato de ser um predicado. É claro que programadores mal-intencionados ou extremamente estúpidos criam implementações da interface que não são livres de efeitos colaterais e, portanto, não são predicados.
Mas o que não pode mais acontecer é que alguém vincula uma unidade lógica de dados / código, que por engano é interpretada como predicado por causa de sua estrutura externa.

Então, para reformular o que foi dito acima em apenas algumas palavras:

  • interfaces significam o uso de tipagem nominal e, assim, relacionamentos explícitos
  • delegados significam usar tipagem estrutural e, assim, relacionamentos implícitos

A vantagem de relacionamentos explícitos é que você pode ter certeza deles. A desvantagem é que eles exigem a sobrecarga da explicitação. Por outro lado, a desvantagem dos relacionamentos implícitos (no nosso caso, a assinatura de uma função que corresponde à assinatura desejada) é que você não pode realmente ter 100% de certeza de que pode usar as coisas dessa maneira. A vantagem é que você pode estabelecer relacionamentos sem toda a sobrecarga. Você pode juntar as coisas rapidamente, porque a estrutura delas permite. É isso que significa composição fácil.
É um pouco como LEGO: você pode simplesmente conectar uma figura LEGO de Star Wars em um navio pirata LEGO, simplesmente porque a estrutura externa permite. Agora você pode achar que isso é terrivelmente errado ou pode ser exatamente o que você deseja. Ninguém vai te parar.


1
Vale ressaltar que "implementa explicitamente a interface" (em "subtipagem nominal") não é o mesmo que implementação explícita ou implícita dos membros da interface. Em C #, as classes sempre devem declarar explicitamente as interfaces que implementam, como você diz. Mas os membros da interface podem ser implementados explicitamente (por exemplo, int IList.Count { get { ... } }) ou implicitamente ( public int Count { get { ... } }). Essa distinção não é relevante para esta discussão, mas merece menção para evitar confundir os leitores.
Phoog 21/10

@ Phohoog: Sim, obrigado pela alteração. Eu nem sabia que o primeiro era possível. Mas sim, a maneira como uma linguagem realmente impõe a implementação da interface varia muito. No Objective-C, por exemplo, ele só emitirá um aviso se não for implementado.
back2dos

1
  1. Um padrão de design de eventos implica uma estrutura que envolve um mecanismo de publicação e assinatura. Os eventos são publicados por uma fonte, cada assinante recebe sua própria cópia do item publicado e é responsável por suas próprias ações. Esse é um mecanismo fracamente acoplado porque o editor nem precisa saber que os assinantes estão lá. Muito menos ter assinantes não é obrigatório (não pode haver nenhum)
  2. composição é "mais fácil" se um representante for usado comparado a uma interface. As interfaces definem uma instância de classe como sendo um objeto "tipo de manipulador", onde, como uma instância de construção de composição, parece ter "um manipulador", portanto, a aparência da instância é menos restrita usando um delegado e se torna mais flexível.
  3. No comentário abaixo, descobri que precisava melhorar minha postagem, consulte isto para referência: http://bytes.com/topic/c-sharp/answers/252309-interface-vs-delegate

1
3. Por que os delegados precisariam de mais elenco e por que um acoplamento mais apertado é um benefício? 4. Você não precisa usar eventos para usar delegados, e com delegados é como segurar um método - você sabe que é o tipo de retorno e o tipo de argumentos. Além disso, por que um método declarado em uma interface facilitaria o tratamento de erros do que um método anexado a um delegado?
Yam Marcovic

No caso de apenas as especificações de um delegado, você está certo. No entanto, no caso de manipulação de eventos (pelo menos é a que minha referência se refere), você sempre precisa herdar algum tipo de eventArgs para atuar como contêiner para tipos personalizados, portanto, em vez de poder testar com segurança um valor, você sempre precisa desembrulhar seu tipo antes de manipular os valores.
Carlo Kuip

Quanto à razão pela qual acho que um método definido em uma interface facilitaria o tratamento de erros, o fato de o compilador poder verificar os tipos de exceção lançados nesse método. Eu sei que não é uma evidência convincente, mas talvez deva ser declarada como preferência?
Carlo Kuip

1
Em primeiro lugar, estávamos falando sobre delegados versus interfaces, não eventos versus interfaces. Em segundo lugar, criar um evento com base em um delegado EventHandler é apenas uma convenção e de forma alguma obrigatória. Mas, novamente, falar sobre eventos aqui meio que erra o ponto. Quanto ao seu segundo comentário - as exceções não são verificadas em C #. Você está ficando confuso com Java (que aliás nem tem delegados).
Yam Marcovic

Encontrado uma discussão sobre este assunto explicando diferenças importantes: bytes.com/topic/c-sharp/answers/252309-interface-vs-delegate
Carlo Kuip
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.