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?
fonte
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 ?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.Respostas:
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
SomeClass
eISomeClass
seja um mau exemplo, porque seria como ter umaOracleObjectSerializer
classe e umaIOracleObjectSerializer
interface.Um exemplo mais preciso seria algo como
OracleObjectSerializer
e aIObjectSerializer
. 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,
IObjectSerializer
não se preocupe em como ele funciona. Vamos supor por um segundo agora que você também tem umaSQLServerObjectSerializer
implementação além deOracleObjectSerializer
. 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
IObjectSerializer
e convertê-laOracleObjectSerializer
e, em seguida, chamar o métodosetProperty
disponível apenas emOracleObjectSerializer
. Isso é ruim porque, mesmo sabendo que uma instância é umaOracleObjectSerializer
, 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 umaIObjectSerializer
instância para aeOracleObjectSerializer
recebe 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
setProperty
na classe de implementação, como no caso do meu,OracleObjectSerializer
se feito corretamente. Se você classe um resumoOracleObjectSerializer
paraIObjectSerializer
, 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 umDog
trabalho de classe como umaIPerson
aplicação, por exemplo).A abordagem correta seria fornecer um
setProperty
método paraIObjectSerializer
.SQLServerObjectSerializer
Idealmente, métodos semelhantes funcionariam através dessesetProperty
método. Melhor ainda, você padroniza os nomes de propriedades por meio de um emEnum
que 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.fonte
IObjectSerializer
aOracleObjectSerializer
porque 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 useOracleObjectSerializer
todo 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.A resposta aceita é correta e muito útil, mas gostaria de abordar brevemente a linha de código que você perguntou sobre:
De um modo geral, isso não é terrível. O que deveria ser evitado sempre que possível seria o seguinte:
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
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ê
myClass
tem o tipoISomeClass
, 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.fonte
Eu acho que é mais fácil mostrar seu código com um exemplo ruim:
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.
fonte
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 .
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.
fonte
Animal
/Giraffe
acima, se você fizesseAnimal a = (Animal)g;
os bits, seria reinterpretado (quaisquer dados específicos do Giraffe seriam interpretados como "não fazem parte deste objeto").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
:Agora, suponha que haja muitas implementações:
Pode-se pensar em muitas implementações diferentes.
Agora, suponha que tenhamos o seguinte código:
Isso mostra claramente a nossa intenção que queremos usar
iList
. Claro que não podemos mais executarDynamicArrayList
operações específicas, mas precisamos de umiList
.Considere o seguinte código:
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).
fonte
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.
fonte