Variância no tratamento de tipos

Variância no tratamento de tipos em C#: Variância, Invariância, Covariância e Contravariância: O que é?

Esses conceitos não são muito populares mas são bastante presentes no dia-a-dia do desenvolvimento: Em especial nas principais linguagens baseadas no paradigma orientado a objetos. Assim, quando falamos em variância no tratamento de tipos estamos falando dos conceitos de: variância, invariância, covariância e contravariância. Portanto, esse artigo vai explicar um pouco sobre cada conceitos com cenários práticos e a relação que eles têm com o princípio SOLID, ou mais especificamente sobre o Princípio da substituição de Liskov.

Princípio da substituição de Liskov

Para começar, esse princípio faz parte dos Princípios SOLID, organizados pelo Robert Martin (uncle Bob). Embora o nome pareça complicado o entendimento é simples.

O resumo específico do princípio, tratado como oficial é: um objeto de uma classe derivada deve ser substituível por um objeto de sua classe base sem quebrar a integridade do sistema. Pense que uma classe que herda de outra terá os métodos da superclasse e, por conta disso, eles devem fazer sentido na classe filha.

Por exemplo, imagine que você possui um tipo ContaBancaria e ele possui dois herdeiros: ContaCorrente e ContaPoupanca. Imagine também que a ContaBancaria possui um método chamado Saque(float valor); Pois bem, uma conta corrente pode ter saques que vão além de todo o seu saldo mas a conta poupança não. Veja a seguir um exemplo para esse caso.

using System;

public class ContaBancaria
{
    protected float saldo;

    public virtual void Saque(float valor)
    {
        saldo -= valor;
        Console.WriteLine("Saque de R$ {0} realizado com sucesso. Saldo atual: R$ {1}", valor, saldo);
    }

    public float Saldo()
    {
        return saldo;
    }
}

public class ContaCorrente : ContaBancaria
{
    public override void Saque(float valor)
    {
        saldo -= valor;
        Console.WriteLine("Saque de R$ {0} realizado com sucesso. Saldo atual: R$ {1}", valor, saldo);
    }
}

public class ContaPoupanca : ContaBancaria
{
    public override void Saque(float valor)
    {
        if (saldo >= valor)
        {
            saldo -= valor;
            Console.WriteLine("Saque de R$ {0} realizado com sucesso. Saldo atual: R$ {1}", valor, saldo);
        }
        else
        {
            Console.WriteLine("Saldo insuficiente.");
        }
    }
}

public class Program
{
    public static void Main()
    {
        ContaBancaria conta1 = new ContaCorrente();
        ContaBancaria conta2 = new ContaPoupanca();

        conta1.Saque(1000); // Saque de R$ 1000 realizado com sucesso. Saldo atual: R$ -1000
        conta2.Saque(1000); // Saldo insuficiente.
    }
}

Para corrigir esse problema, podemos ajustar a classe ContaCorrente para verificar o saldo antes de permitir o saque, ou podemos criar uma nova classe derivada que represente contas com saques ilimitados, por exemplo. Dessa forma, garantimos que todas as classes derivadas respeitem as mesmas restrições e comportamentos esperados pela classe base.

Variância no Domain Driven Design

Para Eric Evans (autor do livro Domain Driven Design, atacando a complexidade no coração do software, 2003) invariantes são todas as diferentes formas de representar estavelmente uma mesma entidade. Entenda que uma entidade sempre deve ter valores válidos. Não deve ser possível colocar saldo negativo numa conta poupança, por exemplo. Entenda mais sobre a validação em DDD.

Por outro lado, é fácil notar que uma mesma entidade pode ter estados diferentes e, por conta disso, alguns campos são obrigatórios e outros não, por exemplo. Então, imagine uma classe Contato, que possui um atributo ‘Tipo‘ que pode ter o valores ‘Prospect‘ (cliente potencial), ‘Lead‘ (cliente potencial qualificado) ou ‘Client‘ (cliente efetivo). Considere que quando esse contato é prospect não é obrigatório preencher quase nenhum de seus atributos, mas quando Lead sim.

Nesse caso é fundamental que a classe tenha factories que suportem todas essas invariantes, garantindo que todas elas se mantenham estáveis. Aqui temos esse artigo explicando esse Pattern com mais detalhes.

Variância na Matemática

Já, na matemática e na estatística esse conceito é importante para lidar com sequencias numéricas. Considere, por exemplo, que existem duas sequencias numéricas: Elas podem estar correlacionadas ou não. Se as sequências seguem de modo idêntico elas são invariantes, ou ao contrário são covariantes. Caso queira saber mais detalhes, separei um vídeo de um professor que dará explicações bem melhores do que as minhas do ponto de vista da matemática e estatística: https://www.youtube.com/watch?v=mndKaambbPA

Invariância e testes automatizados

Já a construção de testes unitários é algo que pode ser incessante. Ainda que haja cobertura de 100% de um determinado código não é possível determinar se há outras condições específicas ainda não tratadas. Assim, é fundamental relacionar as invariantes estáveis do código que precisam ser cobertas pelos testes. Além disso os testes não devem precisar ser refatorados todo o momento. Desse modo, o uso de padrões como specification pattern dá boa flexibilidade sem impactar nos testes já existentes. Por fim, vale ressaltar também que a preservação do princípio Open/Close do SOLID reduz o impacto em testes exatamente por não perturbar as invariantes das entidades.

Variância na computação

Já na computação a variância se apresenta essencialmente na relação entre os tipos genéricos – presentes em diversas linguagens de programação – e o seu uso concreto. Para os exemplos aqui em questão utilizaremos a linguagem C#, que suporta manipulação dessas características.

Mais especificamente, consideramos na computação não apenas a variância mas a covariância e a contra-variância como conceitos fundamentais. Veja o exemplo a seguir como referência para entendermos o tema.

public abstract class Animal
{
    public abstract void BalanceRabo();
}

public class Cachorro: Animal
{
    public override void BalanceRabo()
    {
        Console.WriteLine("abanando o rabo...");
    }
}

public static void Chame(Animal animal)
{
    animal.BalanceRabo();
}

public static void Main(string[] args)
{
    var dog = new Cachorro();
    Chame(dog); 
}

O exemplo é bastante simples que utiliza polimorfismo como base através do método Chame(Animal animal); Vamos ver agora uma alteração desse exemplo considerando diferentes containers de tratamento para essas coleções.

public abstract class Animal
{
	public abstract void BalanceRabo();
}
public class Cachorro: Animal
{
	public override void BalanceRabo()
	{
		Console.WriteLine("abanando o rabo...");
	}
}

public static void Main(string[] args)
{
	Cachorro[] dogs = new Cachorro[] { new Cachorro(), new Cachorro(), new Cachorro()};
	Chame(dogs);
}

public static void Chame(Animal[] animais)
{
	foreach(var animal in animais)
		animal.BalanceRabo();
}

Já nesse exemplo há uma conversão implícita do Cachorro[] para Animal[] (ou Array<Cachorro> para Array<Animal>) e tudo funciona corretamente nesse caso. O array com tipo mais específico converte-se para o tipo mais abstrato. Mas há casos que isso ocorre ao contrário. Veja outro exemplo:

public static void Log(object data)
{
	Console.WriteLine($"Dados: {data} do tipo {data.GetType().Name}");
}

public class Writter
{
	private string _data;
	public Writter(string data)
	{
		_data = data;
	}
	public  override string ToString()
	{
		return $"Gravação: '{_data}'";
	}
}

public static void Main(string[] args)
{
	Action<string> StringLogger = Log;
	Action<Writter> Writter = Log;
	
	StringLogger("Guardando dados 1");
	Writter(new Writter("guardando dados 2"));
	
}
Nesse exemplo utilizamos uma Action. A Action<string> aceita a função Log(object data);. Isso é como Action<Log> se tornando Action<string>, de modo implícito nesse cenário. Trata-se do contrário do exemplo anterior. O mais genérico se torna o tipo mais específico.

Variância e Contravariância em C#

Na linguagem C#, a variação e contravariação são implementadas usando as palavras-chave in e out. A palavra-chave out é usada para indicar covariância e a in para contravariância. A seguir, vamos dar exemplos com o uso desses.

Covariância (Out)

A palavra-chave out é usada em um tipo genérico quando o tipo é retornado do método. Ou seja, o tipo é covariante com relação à classe ou interface genérica. Aqui está um exemplo de uma interface genérica covariante IAnimal<out T>, onde o tipo T é retornado de um método:

public interface IAnimal<out T>
{
    T ObterAnimal();
}

A classe Gato deriva de Animal e pode ser usada como tipo T. Como a interface IAnimal é covariante, podemos usar IAnimal<Gato> como tipo de IAnimal<Animal>.

public class Gato : Animal
{
    public string Nome { get; set; }
}

public class AnimalService
{
    public static void ImprimirAnimal(IAnimal<Animal> animal)
    {
        Animal meuAnimal = animal.ObterAnimal();
        Console.WriteLine(meuAnimal.GetType().Name);
    }
}

public static void Main(string[] args)
{
    IAnimal<Gato> gato = new Gato() { Nome = "Garfield" };
    AnimalService.ImprimirAnimal(gato);
}

Contravariância (In)

Esse é o caso contrário, em que o mais abstrato por se passar pelo mais específico. A palavra-chave in é usada em um tipo genérico quando é parâmetro de entrada. Ou seja, o tipo é contravariante em relação à classe ou interface genérica. Aqui está um exemplo de uma interface genérica contravariante IAnimal<in T>, onde o tipo T é passado como parâmetro de entrada para um método:

public interface IAnimal<in T>
{
    void AdicionarAnimal(T animal);
}

A classe Gato deriva de Animal e pode ser usada como tipo T. Como a interface IAnimal é contravariante, podemos usar IAnimal<Animal> como tipo de IAnimal<Gato>. Observe que passamos IAnimal<Animal> para AdicionarAnimal(), que espera um IAnimal<Gato>: Isso é possível devido à contravariância.

public class Gato : Animal
{
    public string Nome { get; set; }
}

public class AnimalService
{
    public static void AdicionarAnimal(IAnimal<Gato> animal, Gato novoGato)
    {
        animal.AdicionarAnimal(novoGato);
    }
}

public static void Main(string[] args)
{
    IAnimal<Animal> animal = new AnimalService();
    Gato meuGato = new Gato() { Nome = "Tom" };
    AnimalService.AdicionarAnimal(animal, meuGato);
}

Conclusão de Variância no tratamento de tipos

O artigo fala sobre as variâncias e como elas se dispõe. Observamos a variância na matemática, na visão do Domain Driven Design do Eric Evans e na computação pura com tipos genéricos. Entretanto o artigo se especializa no terceiro caso. É possível ver exemplos da variância e contravariância além de uma explicação específica das palavras-reservados in e out em tipos genéricos, por exemplo IAnimal<in T>. Além disso há um cruzamento desses conceitos com o princípio L, Inversão de Liskov, dos princípios SOLID.


Thiago Anselme
Thiago Anselme - Gerente de TI - Arquiteto de Soluções

Ele atua/atuou como Dev Full Stack C# .NET / Angular / Kubernetes e afins. Ele possui certificações Microsoft MCTS (6x), MCPD em Web, ITIL v3 e CKAD (Kubernetes) . Thiago é apaixonado por tecnologia, entusiasta de TI desde a infância bem como amante de aprendizado contínuo.

Deixe um comentário