S06 - Atividade de Aprendizagem: Polimorfismo
Visão Geral
Nesta atividade, você aprenderá e praticará o princípio do Polimorfismo.
Preparação
O que é Polimorfismo?
Polimorfismo é a capacidade de assumir muitas formas. Em programação, esse princípio é mostrado quando uma linha de código pode ter comportamento diferente dependendo do contexto.
Sobrescrita de Método (Overriding)
Para ver o polimorfismo em ação com objetos e herança, primeiro você precisa aprender sobre sobrescrita de métodos. Sobrescrita de método é a capacidade de uma classe derivada sobrescrever ou alterar o comportamento de um método que ela herdou de uma classe base. O nome do método permanece o mesmo, mas o comportamento, ou o código a ser executado, é diferente.
Exemplo
Ao usar herança, uma classe derivada pode herdar variáveis membro e métodos de uma classe base. Por exemplo, um sistema de folha de pagamento pode definir um colaborador que tenha um nome, CPF, endereço e muitos outros atributos. Também pode haver um método para calcular o pagamento desse colaborador.
Para um colaborador mensalista, talvez o salário seja simplesmente retornado conforme mostrado neste exemplo:
// uma classe base
public class Colaborador
{
private float salarioMensal = 5000f;
public float Pagamento()
{
return salarioMensal;
}
}
Mas o sistema de folha de pagamento também pode ter que contabilizar colaboradores que recebem uma certa quantia
de dinheiro com base no número de horas trabalhadas. Esses colaboradores são muito semelhantes à classe de
colaboradores padrão, mas precisam de uma lógica diferente para o método Pagamento. Isso pode
ser definido em uma classe derivada que sobrescreve o método da classe Colaborador. Para fazer isso, primeiro
marcamos o método na classe base com a palavra-chave virtual, que informa ao C# que esse método pode
ser sobrescrito por outra classe. Então, na classe derivada, usamos a palavra-chave override,
conforme mostrado neste exemplo:
// a classe base mostrando a palavra-chave "virtual" incluída
public class Colaborador
{
private float salarioMensal = 5000f;
public virtual float Pagamento()
{
return salarioMensal;
}
}
// uma classe derivada
public class Horista : Colaborador
{
private float valorHora = 10f;
private float horasTrabalhadas = 35f;
public override float Pagamento()
{
return valorHora * horasTrabalhadas; // o pagamento é calculado de forma diferente
}
}
Alterar um comportamento dessa maneira é chamado de sobrescrita de método (override). Linguagens
diferentes têm sintaxe ligeiramente diferente para sobrescrever métodos. Em C#, você sobrescreve o método usando
as palavras-chave virtual e override nos métodos das classes base e derivada,
conforme mostrado.
Observe que tanto Colaborador quanto Horista têm um método para
Pagamento, então, nesse aspecto, são iguais. Mas o comportamento real ou código do método é
diferente.
Uma Combinação Poderosa
A herança e a sobrescrita de métodos são uma combinação poderosa. Quando usadas em conjunto, fornecem uma maneira de variar o comportamento do tempo de execução de acordo com o contexto.
Lembra-se da aula sobre Herança? Você deve ser capaz de usar um objeto derivado em qualquer lugar onde você
possa usar a classe base (o Princípio da Substituição de Liskov). Com isso em mente, se você criar uma lista de
objetos Colaborador, você também poderá inserir objetos Horista na lista.
Revise o código a seguir com cuidado.
// Crie uma lista de funcionários
List<Colaborador> colaboradores = new List<Colaborador>();
// Crie diferentes tipos de colaboradores e adicione-os à mesma lista
colaboradores.add(new Colaborador());
colaboradores.add(new Horista());
// Obtenha um cálculo personalizado para cada um
foreach(Colaborador colaborador in colaboradores)
{
float pagamento = colaborador.Pagamento();
Console.WriteLine(pagamento);
}
Saída:
5000
350
Neste exemplo, uma nova instância de Colaborador e Horista são adicionadas à
lista colaboradores. No loop a seguir, o método Pagamento é chamado para cada
uma. O método real que é chamado e o valor resultante dependem do contexto, ou do tipo de colaborador, durante
cada iteração. Se o objeto for um Colaborador, o método base será chamado. Entretanto, se o objeto
for um Horista, a versão definida para colaboradores horistas será usada.
A importância da última afirmação não pode ser subestimada. Tudo o que é preciso para variar o comportamento do
loop é criar novas derivações de Colaborador, sobrescrever o método Pagamento e
adicionar uma instância à lista. Nenhum código escrito anteriormente precisa ser modificado de forma alguma.
Alterar o programa é fácil!
Polimorfismo em Ação
O exemplo de código anterior mostra o polimorfismo em ação. Lembre-se da seguinte linha de código desse exemplo:
float pagamento = colaborador.Pagamento();
Conforme declarado, essa mesma linha de código pode assumir "muitas formas" ou, mais especificamente, chamará métodos diferentes dependendo do tipo de objeto de funcionário encontrado em tempo de execução.
Outro Exemplo: Passagem de Parâmetros
Além de ver o polimorfismo usado no contexto de percorrer uma lista de objetos de classe base, você pode ver isso em ação passando um objeto para um método. Considere o seguinte código:
public class Program
{
// ...
static void MostrarPagamento (Colaborador e)
{
float pagamento = e.Pagamento();
// ...
}
}
Observe que, neste exemplo, a função MostrarPagamento tem um parâmetro Colaborador. Novamente,
deveríamos ser capazes de substituir qualquer classe derivada e fazê-la funcionar, então você pode chamar essa
função
com um objeto Horista, e ela funcionará perfeitamente. O código
e.Pagamento() chamará o método correto com base no objeto real em tempo de execução.
Outro Exemplo: Retornar Valores
Outro uso comum do Polimorfismo é que, quando o tipo de retorno de um método é uma classe base, você também pode retornar objetos de classes derivadas. Considere o seguinte código:
public class Program
{
// ...
static Funcionario ObterGerente()
{
// ... código aqui para encontrar o gerente ...
return Gerente;
}
static void ExibirPagamentoGerente()
{
Colaborador gerente = ObterGerente();
float pagamento = gerente.Pagamento();
// ...
}
}
O código que retorna o gerente pode retornar um objeto Colaborador da
classe base ou pode retornar um objeto Horista. Independentemente do tipo de colaborador
retornado, o código gerente.Pagamento() chamará o método apropriado.
Métodos Abstratos
No exemplo acima, a classe base continha uma implementação padrão para o método Pagamento
que funcionava para colaboradores base. Mas às vezes não é possível criar um bom método padrão. Por exemplo, em
vez
de ter a classe base representando um Mensalista e a classe derivada representando um Horista, você pode definir a
classe base para um Colaborador genérico com duas classes derivadas. Nesse caso, você
não poderia fornecer uma boa implementação padrão do método Pagamento na classe base, então você deve deixá-lo em
branco.
Um método virtual em branco tem um nome especial: é chamado de Método Abstrato. Qualquer classe que
tenha pelo menos um método abstrato é uma classe Abstrata. Isso significa que a classe abstrata base
não pode ser instanciada diretamente; você só pode criar objetos a partir dos tipos derivados.
Você especifica métodos abstratos com a palavra-chave abstract em vez de "virtual". Então, a
definição da classe também deve conter a palavra-chave abstract. Por exemplo:
// a classe base mostrando a palavra-chave "abstract"
public abstract class Colaborador
{
private string _nome;
// Observe que o método abstrato não tem corpo (nem mesmo um vazio)
// e é seguido por um ponto e vírgula.
public abstract float Pagamento();
}
// uma classe derivada
public class Mensalista : Colaborador
{
private float salarioMensal = 5000f;
public override float Pagamento()
{
return salarioMensal;
}
}
// uma classe derivada
public class Horista : Colaborador
{
private float valorHora = 10f;
private float horasTrabalhadas = 35f;
public override float Pagamento()
{
return valorHora * horasTrabalhadas; // o pagamento é calculado de forma diferente
}
}
Tudo se Resume à Interface
O aspecto mais importante do exemplo anterior é o método compartilhado chamado Pagamento. É
um contrato formal de que todas as classes derivadas de Colaborador, não importa qual seja seu tipo
específico, fornecerão a mesma capacidade, ou seja, um método com o nome Pagamento e, nesse
caso, sem parâmetros e com um valor de retorno do tipo float. Essa garantia é invocada por quaisquer outras partes
do
programa que utilizem um Colaborador de qualquer tipo.
Reserve um momento e lembre-se do segundo princípio da programação com classes. Um dos aspectos mais importantes da aplicação do encapsulamento era focar no que uma classe deve fazer e não em como ela fará isso. O mesmo conselho se aplica aqui.
Interfaces (opcional)
Métodos abstratos definidos em uma classe base nos fornecem uma maneira de especificar que um método deve estar presente em classes derivadas sem fornecer uma implementação padrão na classe base. Essa ideia é tão poderosa que muitas vezes tudo o que queremos fazer é definir os métodos públicos que uma classe derivada deve ter — não queremos nem mesmo fornecer variáveis membro ou corpos de método na classe base.
Uma classe base que contém apenas esses métodos abstratos e nada mais tem um nome especial. Ela é chamada de Interface porque define a interface ou os métodos públicos que qualquer classe que a implementa deve ter. Nesse caso, você define a "classe" como uma interface e, então, não precisa especificar que os métodos são abstratos, virtuais ou mesmo públicos, porque todas essas coisas estão implícitas. Considere o seguinte código:
// a interface de Colaborador
// C# tem como convenção que os nomes de interface comecem com I
public interface IColaborador
{
float Pagamento(); // o método de interface não tem um corpo
}
// uma implementação específica da interface Colaborador
public class Mensalista : IColaborador
{
private float salarioMensal = 5000f;
public float Pagamento()
{
return salarioMensal;
}
}
// outra implementação da interface Colaborador
public class Horista : IColaborador
{
private float valorHora = 10f;
private float horasTrabalhadas = 30f;
public float Pagamento()
{
return valorHora * horasTrabalhadas;
}
}
Então, o que você deveria usar, uma classe abstrata ou uma interface? A resposta depende se sua classe base terá variáveis membro ou corpos de método. Se você quiser fornecer isso, deverá criar uma classe abstrata. Se sua classe base estiver lá apenas para definir os métodos que devem ser sobrescritos, então você deve usar uma Interface.
Exemplo em Vídeo
Por favor, assista ao exemplo a seguir de como usar Polimorfismo em C#.
Link Direto: ▶️Polimorfismo em C# (14 minutos)
Resumidamente
Polimorfismo é o quarto e principal princípio da programação com classes. O uso habilidoso de abstração, encapsulamento e herança é necessário para aplicar o polimorfismo de forma eficaz. O resultado é um mecanismo simples, mas poderoso, de modo a garantir que os programas sejam flexíveis e prontos para mudanças.
Um dos temas recorrentes em tudo isso tem sido a importância de focar nos contratos de classe, ou seja, na interface. Identificá-los, defini-los e desenvolvê-los é uma preocupação primordial para quem pratica programação com classes regularmente.
Instruções da Atividade
Pratique o princípio do polimorfismo criando um programa que calcula as áreas de diferentes figuras recortadas de pedaços de papel.
Para todas as figuras, você precisa monitorar a cor do papel e então ter um método para calcular a área. A área não deve ser armazenada como uma variável membro, mas, em vez disso, você deve armazenar o comprimento dos lados das figuras e então calcular a área conforme necessário.
Seu programa deve incluir quadrados (que armazenam uma cor e um único lado), retângulos (que armazenam uma cor e dois lados) e um círculo (que armazena uma cor e um raio). Você deve criar vários tipos de figuras e colocá-las em uma única lista. Em seguida, percorra a lista e exiba as áreas delas.
Projetar as Classes
Com base no que você aprendeu sobre herança, parece razoável criar uma classe base de figura em que você pode incluir quaisquer responsabilidades que todas as figuras tenham em comum. Então você pode criar classes derivadas para as figuras individuais de quadrado, retângulo e círculo.
Neste exemplo, todas as figuras têm uma cor e um método para obter a área, mas a implementação desse método será
diferente para cada tipo de figura. Portanto, o método ObterArea deve ser declarado na classe base,
mas você deve sobrescrevê-lo nas classes derivadas.
Essas relações podem ser vistas no seguinte diagrama de classes:
Inicie o Projeto
- Abra o projeto da classe no VS Code.
- Navegue até o projeto
Figurasna pastasemana06. Encontre o arquivoProgram.cs, que será seu ponto de entrada para o programa. - Verifique se você consegue executar o projeto.
Crie a classe Figura
- Em um novo arquivo, crie a classe
Figura. - Adicione a variável membro cor e um getter e setter para ela.
- Crie um construtor que aceite a cor e defina-a.
- Crie um método virtual para
ObterArea().
Crie a classe Quadrado
- Em um novo arquivo, crie a classe
Quadrado. - Certifique-se de que esta classe herda da classe base.
- Crie um construtor que aceite a cor e o lado e, em seguida, chame o construtor base com a cor.
- Crie o atributo
_ladocomo uma variável membro privada. - Sobrescreva o método
ObterArea()da classe base e preencha o corpo desta função para retornar a área.
Teste a classe Quadrado
- Retorne ao método
MainemProgram.cspara testar seu código. - Crie uma instância de
Quadrado, chame os métodosObterCor()eObterArea()e certifique-se de que eles retornem os valores esperados.
Crie as classes Retangulo e Circulo
- Repita os passos acima para as classes
RetanguloeCirculo, colocando cada uma em seus próprios arquivos, armazenando as variáveis necessárias e sobrescrevendoObterArea()para cada uma. - Teste essas classes novamente no
Maine certifique-se de que funcionam conforme o esperado.
Crie uma lista
- No seu método
Main, crie uma lista para armazenar figuras (Dica: O tipo de dados deve serList<Figura>). - Adicione um quadrado, um retângulo e um círculo a esta lista.
- Percorra a lista de figuras. Para cada um, chame e exiba os métodos
ObterCor()eObterArea().
Exemplo de Solução
Quando terminar, por favor, compare sua abordagem com o seguinte exemplo de solução (você também pode usar esse exemplo de solução como um guia se precisar de ajuda para terminar).
Envio
- Verifique se cada uma de suas classes funciona conforme descrito acima.
- Confirme (commit) e envie (push) seu código para seu repositório no GitHub.
- Verifique se você consegue ver seu código atualizado no GitHub.
- Reaponda ao questionário do Canvas para relatar seu trabalho.