C# - Usando interfaces (revisitado)
Retornamos ao assunto do uso de interfaces na linguagem C#. |
Uma
interface pode incluir apenas a declaração de métodos, propriedades e eventos,
mas não pode fornecer implementações para eles. As classes que implementam uma
interface devem fornecer suas próprias implementações para cada membro da
interface.
Interfaces são frequentemente usadas para definir contratos comuns que as classes podem seguir para garantir a interoperabilidade entre diferentes partes de um programa. Por exemplo, uma interface IComparable pode ser usada para definir um contrato que as classes devem seguir para permitir a comparação de instâncias dessas classes.
Para implementar uma interface em uma classe, usamos dois pontos ( : ) após o nome da classe seguido do nome da interface e a seguir fornecemos as implementações necessárias para cada membro da interface na classe.
// Definição da
interface public interface IAnimal { void EmitirSom(); } //
Implementação da classe que segue a interface |
O uso de
interfaces é uma boa prática na linguagem C# por várias razões, como:
Permite o desenvolvimento de código mais modular e fácil
de manter: ao utilizar interfaces, é possível definir um contrato ou um
conjunto de comportamentos que um objeto deve implementar, independentemente de
como ele é implementado. Isso significa que é possível alterar a implementação
interna de um objeto sem afetar outros objetos que dependem dele, desde que a
nova implementação atenda ao contrato definido pela interface.
Facilita a comunicação e a colaboração entre objetos:
quando um objeto implementa uma interface, ele se torna compatível com
outros objetos que também implementam essa interface. Isso significa que esses
objetos podem interagir de forma mais fácil e consistente, mesmo que sejam de
tipos diferentes.
Permite a criação de classes genéricas: interfaces
podem ser usadas como restrições em parâmetros genéricos, o que permite criar
classes genéricas que funcionem com vários tipos diferentes, desde que esses
tipos implementem a interface exigida.
Ajuda a prevenir erros de compilação: quando um
objeto não implementa todos os membros exigidos por uma interface, o compilador
emite um erro, o que ajuda a identificar erros antes mesmo de executar o código.
Exemplo prático
Para ilustrar como o uso de interfaces otimiza o código vejamos um exemplo
prático usando um projeto Console criado no VS 2022 usando o C# 11 e o .NET 7.0.
Suponha que temos um sistema que precisa gerar relatórios de diferentes tipos, como relatórios em PDF, relatórios em Excel, relatórios em HTML etc.
Cada tipo de relatório pode ter uma implementação diferente, mas todos eles devem ser gerados a partir de uma fonte de dados comum, como um banco de dados ou um arquivo CSV.
Podemos começar definindo uma interface IGeradorRelatorio que define o método GerarRelatorio para cada tipo de relatório:
public
interface
IGeradorRelatorio { void GerarRelatorio(string fonteDados); } |
Em seguida, podemos criar classes para cada tipo de relatório, implementando a interface IGeradorRelatorio de maneira diferente para cada um:
public
class
GeradorPDF
: IGeradorRelatorio { public void GerarRelatorio(string fonteDados) { Console.WriteLine($"Gerando relatório PDF a partir da fonte de dados: {fonteDados}"); } } public class GeradorExcel : IGeradorRelatorio{ public void GerarRelatorio(string fonteDados) { Console.WriteLine($"Gerando relatório Excel a partir da fonte de dados: {fonteDados}"); } } public class GeradorHTML : IGeradorRelatorio{ public void GerarRelatorio(string fonteDados) { Console.WriteLine($"Gerando relatório HTML a partir da fonte de dados: {fonteDados}"); } } |
Agora, podemos criar uma classe GerenciadorRelatorios que usa a interface IGeradorRelatorio para gerar relatórios de qualquer tipo, sem se preocupar com a implementação específica de cada um:
public
class
GerenciadorRelatorios { private readonly IGeradorRelatorio _gerador; public GerenciadorRelatorios(IGeradorRelatorio gerador) { _gerador = gerador; } public void GerarRelatorio(string fonteDados) { Console.WriteLine($"Gerando relatório usando {_gerador.GetType().Name}"); _gerador.GerarRelatorio(fonteDados); Console.WriteLine(); } } |
Observe que o
construtor da classe GerenciadorRelatorios recebe
uma instância da interface IGeradorRelatorio. Isso
permite que possamos injetar diferentes implementações da interface, dependendo
do tipo de relatório que queremos gerar.
Por exemplo, podemos criar uma instância de cada tipo de gerador de relatório e
usá-los com o GerenciadorRelatorios:
var
fonteDados =
"dados.csv"; var geradorPDF = new GeradorPDF();var geradorExcel = new GeradorExcel(); var geradorHTML = new GeradorHTML(); var gerenciador = new GerenciadorRelatorios(geradorPDF); gerenciador.GerarRelatorio(fonteDados); gerenciador.GerarRelatorio(fonteDados); gerenciador = new GerenciadorRelatorios(geradorHTML); gerenciador.GerarRelatorio(fonteDados); Console.ReadKey(); |
Executando teremos o seguinte resultado :
Observe que o código é o mesmo para cada tipo de relatório, e que o GerenciadorRelatorios pode lidar com qualquer tipo de gerador de relatório, sem se preocupar com a implementação específica de cada um.
Isso torna o
código mais flexível e extensível, pois podemos adicionar novos tipos de
geradores de relatório simplesmente criando uma nova classe que implementa a
interface IGeradorRelatorio.
Além disso, como o GerenciadorRelatorios depende
apenas da abstração (ou seja, da interface IGeradorRelatorio), podemos
substituir as implementações específicas sem afetar o restante do código. Isso
torna o código mais fácil de manter e de testar, pois podemos testar cada
implementação da interface IGeradorRelatorio
separadamente.
Em resumo, o uso de interfaces nos permite escrever código mais flexível,
extensível e fácil de manter, especialmente em sistemas complexos com várias
implementações diferentes de uma funcionalidade específica. O exemplo acima
ilustra como o uso de interfaces pode otimizar o código ao permitir que
dependamos apenas de abstrações em vez de implementações específicas.
Sem o uso de interfaces, o código para o gerenciamento de relatórios pode ficar mais complexo e menos flexível. Veja um exemplo de como seria sem o uso de interfaces:
public
class
GerenciadorRelatorios { public void GerarRelatorioPDF(string fonteDados) { Console.WriteLine("Gerando relatório PDF a partir da fonte de dados: " + fonteDados); } public void GerarRelatorioExcel(string fonteDados) { Console.WriteLine("Gerando relatório Excel a partir da fonte de dados: " + fonteDados); } public void GerarRelatorioHTML(string fonteDados) { Console.WriteLine("Gerando relatório HTML a partir da fonte de dados: " + fonteDados); } } |
Parece mais simples , não é mesmo !!!
Nesse caso, o
GerenciadorRelatorios possui um método para cada
tipo de gerador de relatório, o que pode tornar o código mais difícil de manter
e estender. Se quisermos adicionar um novo tipo de gerador de relatório,
precisamos criar um novo método no GerenciadorRelatorios alterando o código da
classe e ferindo o princípio SOLID Open-Closed.
Além disso, o código que usa o GerenciadorRelatorios
precisa chamar o método correto de acordo com o tipo de gerador de relatório
desejado. Isso pode resultar em código duplicado e difícil de manter,
especialmente em sistemas com muitos pontos de chamada.
Portanto, o uso de interfaces pode otimizar o código ao permitir que dependamos
apenas de abstrações em vez de implementações específicas, tornando o código
mais flexível e fácil de manter.
E
estamos conversados ...
"A estultícia do homem perverterá o seu caminho, e o seu coração se irará contra
o Senhor."
Provérbios
19:3
Referências: