O que o autor quer dizer ao converter a referência de interface para qualquer implementação?


17

Atualmente, estou tentando dominar o C #, então estou lendo o Adaptive Code via C # de Gary McLean Hall .

Ele escreve sobre padrões e antipadrões. Na parte implementações versus interfaces, ele escreve o seguinte:

Os desenvolvedores que são novos no conceito de programação para interfaces geralmente têm dificuldade em deixar de lado o que está por trás da interface.

Em tempo de compilação, qualquer cliente de uma interface não deve ter idéia de qual implementação da interface está usando. Esse conhecimento pode levar a suposições incorretas que associam o cliente a uma implementação específica da interface.

Imagine o exemplo comum em que uma classe precisa salvar um registro no armazenamento persistente. Para fazer isso, ele delega corretamente a uma interface, que oculta os detalhes do mecanismo de armazenamento persistente usado. No entanto, não seria correto fazer suposições sobre qual implementação da interface está sendo usada em tempo de execução. Por exemplo, converter a referência de interface para qualquer implementação é sempre uma má ideia.

Pode ser a barreira do idioma ou a minha falta de experiência, mas não entendo direito o que isso significa. Aqui está o que eu entendo:

Eu tenho um projeto divertido de tempo livre para praticar C #. Lá eu tenho uma aula:

public class SomeClass...

Esta classe é usada em muitos lugares. Ao aprender C #, li que é melhor abstrair com uma interface, então fiz o seguinte

public interface ISomeClass <- Here I made a "contract" of all the public methods and properties SomeClass needs to have.

public class SomeClass : ISomeClass <- Same as before. All implementation here.

Então, eu entrei em todas as referências de classe e as substitui pelo ISomeClass.

Exceto na construção, onde escrevi:

ISomeClass myClass = new SomeClass();

Estou entendendo corretamente que isso está errado? Se sim, por que sim e o que devo fazer?


25
Em nenhum lugar do seu exemplo você está convertendo um objeto do tipo de interface para o tipo de implementação. Você está atribuindo algo do tipo de implementação a uma variável de interface, que é perfeitamente correta e correta.
Caleth

1
O que você quer dizer com "no construtor em que escrevi ISomeClass myClass = new SomeClass();? Se você realmente quer dizer isso, isso é recursão no construtor, provavelmente não é o que você deseja. Espero que você queira dizer em" construção ", ou seja, alocação, talvez, mas não no próprio construtor, certo ?
Erik Eidt

@Erik: Sim. Em construção. Você está certo. Irá corrigir a pergunta. Graças
Marshall

Curiosidade: o F # tem uma história melhor que o C # a esse respeito - ele elimina implementações implícitas de interface; portanto, sempre que você deseja chamar um método de interface, precisa fazer upcast para o tipo de interface. Isso deixa muito claro quando e como você está usando interfaces em seu código e torna a programação para interfaces muito mais arraigada na linguagem.
scrwtp

3
Isso é um pouco fora de tópico, mas acho que o autor diagnostica incorretamente o problema que as pessoas novas no conceito têm. Na minha opinião, o problema é que as pessoas novas no conceito não sabem como fazer boas interfaces. É muito fácil criar interfaces muito específicas que não fornecem nenhuma generalidade (o que pode estar acontecendo com isso ISomeClass), mas também é fácil criar interfaces muito genéricas para as quais é impossível escrever código útil e nesse momento as únicas opções são repensar a interface e reescrever o código ou fazer downcast.
Derek Elkins saiu de SE

Respostas:


37

Abstrair sua classe em uma interface é algo que você deve considerar se, e somente se, pretende escrever outras implementações dessa interface ou se existe a forte possibilidade de fazê-lo no futuro.

Então, talvez SomeClasse ISomeClassseja um mau exemplo, porque seria como ter uma OracleObjectSerializerclasse e uma IOracleObjectSerializerinterface.

Um exemplo mais preciso seria algo como OracleObjectSerializere a IObjectSerializer. O único local em seu programa em que você se importa com a implementação a ser usada quando a instância é criada. Às vezes, isso é ainda mais desacoplado usando um padrão de fábrica.

Em qualquer outro lugar do seu programa, IObjectSerializernão se preocupe em como ele funciona. Vamos supor por um segundo agora que você também tem uma SQLServerObjectSerializerimplementação além de OracleObjectSerializer. Agora, suponha que você precise definir alguma propriedade especial para definir e esse método esteja presente apenas no OracleObjectSerializer e não no SQLServerObjectSerializer.

Há duas maneiras de fazer isso: a maneira incorreta e a abordagem do princípio de substituição de Liskov .

A maneira incorreta

A maneira incorreta, e a própria instância mencionada em seu livro, seria pegar uma instância IObjectSerializere convertê-la OracleObjectSerializere, em seguida, chamar o método setPropertydisponível apenas em OracleObjectSerializer. Isso é ruim porque, mesmo sabendo que uma instância é uma OracleObjectSerializer, você está introduzindo outro ponto em seu programa em que deseja saber qual é a implementação. Quando essa implementação é alterada e, presumivelmente, mais cedo ou mais tarde, se você tiver várias implementações, o melhor cenário, será necessário encontrar todos esses locais e fazer os ajustes corretos. No pior cenário, você lança uma IObjectSerializerinstância para ae OracleObjectSerializerrecebe uma falha de tempo de execução na produção.

Abordagem do princípio da substituição de Liskov

Liskov disse que você nunca deve precisar de métodos como setPropertyna classe de implementação, como no caso do meu, OracleObjectSerializerse feito corretamente. Se você classe um resumo OracleObjectSerializerpara IObjectSerializer, você deve abranger todos os métodos necessários para usar essa classe, e se você não pode, então algo está errado com sua abstração (tentando fazer um Dogtrabalho de classe como uma IPersonaplicação, por exemplo).

A abordagem correta seria fornecer um setPropertymétodo para IObjectSerializer. SQLServerObjectSerializerIdealmente, métodos semelhantes funcionariam através desse setPropertymétodo. Melhor ainda, você padroniza os nomes de propriedades por meio de um em Enumque cada implementação traduz essa enumeração para o equivalente para sua própria terminologia de banco de dados.

Simplificando, usar um ISomeClassé apenas metade disso. Você nunca precisará lançá-lo para fora do método responsável por sua criação. Fazer isso é quase certamente um sério erro de design.


1
Parece-me que, se você estiver indo para lançar IObjectSerializera OracleObjectSerializerporque você “sabe” que este é o que é, então você deve ser honesto consigo mesmo (e mais importante ainda, com outras pessoas que podem manter este código, que pode incluir o seu futuro self) e use OracleObjectSerializertodo o caminho de onde é criado até onde é usado. Isso torna muito público e claro que você está introduzindo uma dependência em uma implementação específica - e o trabalho e a feiúra envolvidos nesse processo tornam-se, por si só, uma forte dica de que algo está errado.
precisa saber é o seguinte

(E, se por algum motivo você realmente não tem que confiar em uma implementação específica, torna-se muito mais claro que é isso que você está fazendo e que você está fazendo isso com a intenção e propósito. Este “” nunca deve acontecer naturalmente, e 99% das vezes pode parecer que está acontecendo na verdade não é e você deve consertar as coisas, mas nada é 100% certo ou como as coisas deveriam ser).
KRyan

@KRyan Absolutely. A abstração só deve ser usada se você precisar. O uso da abstração quando não é necessário serve apenas para tornar o código um pouco mais difícil de entender.
Neil

29

A resposta aceita é correta e muito útil, mas gostaria de abordar brevemente a linha de código que você perguntou sobre:

ISomeClass myClass = new SomeClass();

De um modo geral, isso não é terrível. O que deveria ser evitado sempre que possível seria o seguinte:

void someMethod(ISomeClass interface){
    SomeClass cast = (SomeClass)interface;
}

Onde seu código recebe uma interface externamente, mas a lança internamente para uma implementação específica, "Porque eu sei que será apenas essa implementação". Mesmo que isso acabasse sendo verdade, usando uma interface e convertendo-a em uma implementação, você voluntariamente renuncia à segurança do tipo real apenas para poder fingir usar a abstração. Se alguém mais trabalhar no código mais tarde e vir um método que aceite um parâmetro de interface, eles assumirão que qualquer implementação dessa interface é uma opção válida para passar. Pode até ser você mesmo um pouco mais depois que você esqueceu que um método específico está sobre quais parâmetros ele precisa. Se você sentir a necessidade de transmitir de uma interface para uma implementação específica, a interface, a implementação, ou o código que os referencia é projetado incorretamente e deve mudar. Por exemplo, se o método funcionar apenas quando o argumento transmitido for uma classe específica, o parâmetro deverá aceitar apenas essa classe.

Agora, olhando para sua chamada de construtor

ISomeClass myClass = new SomeClass();

os problemas do casting realmente não se aplicam. Nada disso parece estar exposto externamente; portanto, não há nenhum risco específico associado a ele. Essencialmente, essa linha de código em si é um detalhe de implementação que as interfaces são projetadas para abstrair, para que um observador externo a veja funcionando da mesma maneira, independentemente do que elas façam. No entanto, isso também não ganha nada com a existência de uma interface. Você myClasstem o tipo ISomeClass, mas não tem nenhum motivo, pois sempre recebe uma implementação específica,SomeClass. Existem algumas vantagens potenciais menores, como poder trocar a implementação no código alterando apenas a chamada do construtor ou reatribuir essa variável posteriormente para uma implementação diferente, mas, a menos que haja outro lugar que exija que a variável seja digitada na interface em vez de a implementação desse padrão faz com que seu código pareça que as interfaces foram usadas apenas por rotina, e não pela compreensão real dos benefícios das interfaces.


1
O Apache Math faz isso com o Cartesian3D Source, linha 286 , o que pode ser realmente irritante.
J_F_B_M

1
Esta é a resposta realmente correta que aborda a pergunta original.
Benjamin Gruenbaum

2

Eu acho que é mais fácil mostrar seu código com um exemplo ruim:

public interface ISomeClass
{
    void DoThing();
}

public class SomeClass : ISomeClass
{
    public void DoThing()
    {
       // Mine for BitCoin
    }

}

public class AnotherClass : ISomeClass
{
    public void DoThing()
    {
        // Mine for oil
    }
    public Decimal Depth;
 }

 void main()
 {
     ISomeClass task = new SomeClass();

     task.DoThing(); //  This is good

     Console.WriteLine("Depth = {0}", ((AnotherClass)task).Depth); <-- The task object will not have this field
 }

O problema é que, quando você escreve o código inicialmente, provavelmente existe apenas uma implementação dessa interface, para que a conversão ainda funcione, é que, no futuro, você poderá implementar outra classe e, em seguida (como mostra meu exemplo), você tente acessar dados que não existem no objeto que você está usando.


Olá, senhor. Alguém já lhe disse como você é bonito?
Neil

2

Apenas para maior clareza, vamos definir o casting.

A transmissão é converter à força algo de um tipo para outro. Um exemplo comum é converter um número de ponto flutuante em um tipo inteiro. Uma conversão específica pode ser especificada ao converter, mas o padrão é simplesmente reinterpretar os bits.

Aqui está um exemplo de transmissão de desta página de documentos da Microsoft .

// Create a new derived type.  
Giraffe g = new Giraffe();  

// Implicit conversion to base type is safe.  
Animal a = g;  

// Explicit conversion is required to cast back  
// to derived type. Note: This will compile but will  
// throw an exception at run time if the right-side  
// object is not in fact a Giraffe.  
Giraffe g2 = (Giraffe) a;  

Você poderia fazer a mesma coisa e converter algo que implementa uma interface para uma implementação específica dessa interface, mas não deve porque isso resultará em erro ou comportamento inesperado se uma implementação diferente da que você espera for usada.


"Casting está convertendo algo de um tipo para outro." - Não. A transmissão está explicitamente convertendo algo de um tipo para outro. (Especificamente, "conversão" é o nome da sintaxe usada para especificar essa conversão.) Conversões implícitas não são convertidas. "Uma conversão específica pode ser especificada ao converter, mas o padrão é simplesmente reinterpretar os bits." -- Certamente não. Existem muitas conversões, implícitas e explícitas, que envolvem alterações significativas nos padrões de bits.
hvd

@hvd Fiz uma correção agora em relação à explicitação do casting. Quando eu disse que o padrão é simplesmente reinterpretar os bits, estava tentando expressar que, se você criar seu próprio tipo, nos casos em que uma conversão for definida automaticamente, quando você a converter para outro tipo, os bits serão reinterpretados. . No exemplo Animal/ Giraffeacima, se você fizesse Animal a = (Animal)g;os bits, seria reinterpretado (quaisquer dados específicos do Giraffe seriam interpretados como "não fazem parte deste objeto").
precisa saber é o seguinte

Apesar do que diz o hvd, as pessoas costumam usar o termo "conversão" em referência a conversões implícitas; veja, por exemplo, https://www.google.com/search?q="implicit+cast"&tbm=bks . Tecnicamente, acho mais correto reservar o termo "conversão" para conversões explícitas, desde que você não se confunda quando outras pessoas o usam de maneira diferente.
ruakh 11/09/17

0

Meus 5 centavos:

Todos esses exemplos são válidos, mas não são exemplos do mundo real e não mostraram as intenções do mundo real.

Como não sei C #, darei um exemplo abstrato (mistura entre Java e C ++). Espero que esteja tudo bem.

Suponha que você tenha uma interface iList:

interface iList<Key,Value>{
   bool add(Key k, Value v);
   bool remove(Element e);
   Value get(Key k);
}

Agora, suponha que haja muitas implementações:

  • DynamicArrayList - usa matriz plana, rápida para inserir e remover no final.
  • LinkedList - usa lista dupla vinculada, rápida para inserir na frente e no final.
  • AVLTreeList - usa a árvore AVL, rápido para fazer tudo, mas usa muita memória
  • SkipList - usa SkipList, rápido para fazer tudo, mais lento que o AVL Tree, mas usa menos memória.
  • HashList - usa HashTable

Pode-se pensar em muitas implementações diferentes.

Agora, suponha que tenhamos o seguinte código:

uint begin_size = 1000;
iList list = new DynamicArrayList(begin_size);

Isso mostra claramente a nossa intenção que queremos usar iList. Claro que não podemos mais executar DynamicArrayListoperações específicas, mas precisamos de um iList.

Considere o seguinte código:

iList list = factory.getList();

Agora nem sabemos qual é a implementação. Este último exemplo é frequentemente usado no processamento de imagens quando você carrega algum arquivo do disco e não precisa do seu tipo de arquivo (gif, jpeg, png, bmp ...), mas tudo o que você deseja é manipular a imagem (inverter, escala, salve como png no final).


0

Você tem uma interface ISomeClass e um objeto myObject do qual você não conhece nada do seu código, exceto que ele é declarado para implementar ISomeClass.

Você tem uma classe SomeClass, que você conhece implementa a interface ISomeClass. Você sabe disso porque é declarado implementar o ISomeClass ou você mesmo o implementou para implementar o ISomeClass.

O que há de errado em transmitir myClass para SomeClass? Duas coisas estão erradas. Primeiro, você realmente não sabe que myClass é algo que pode ser convertido em SomeClass (uma instância de SomeClass ou uma subclasse de SomeClass), para que o elenco possa dar errado. Segundo, você não deveria ter que fazer isso. Você deve trabalhar com myClass declarado como iSomeClass e usar os métodos ISomeClass.

O ponto em que você obtém um objeto SomeClass é quando um método de interface é chamado. Em algum momento você chama myClass.myMethod (), que é declarado na interface, mas tem uma implementação em SomeClass e, é claro, possivelmente em muitas outras classes que implementam ISomeClass. Se uma chamada terminar no seu código SomeClass.myMethod, você saberá que self é uma instância de SomeClass e, nesse ponto, é absolutamente correto e realmente correto usá-lo como um objeto SomeClass. Obviamente, se for realmente uma instância de OtherClass e não SomeClass, você não chegará ao código SomeClass.

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.