CSE 210: Programação com Classes

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:

Diagrama de Classes
Diagrama de Classe de Figura

Inicie o Projeto

  1. Abra o projeto da classe no VS Code.
  2. Navegue até o projeto Figuras na pasta semana06. Encontre o arquivo Program.cs, que será seu ponto de entrada para o programa.
  3. Verifique se você consegue executar o projeto.

Crie a classe Figura

  1. Em um novo arquivo, crie a classe Figura.
  2. Adicione a variável membro cor e um getter e setter para ela.
  3. Crie um construtor que aceite a cor e defina-a.
  4. Crie um método virtual para ObterArea().

Crie a classe Quadrado

  1. Em um novo arquivo, crie a classe Quadrado.
  2. Certifique-se de que esta classe herda da classe base.
  3. Crie um construtor que aceite a cor e o lado e, em seguida, chame o construtor base com a cor.
  4. Crie o atributo _lado como uma variável membro privada.
  5. Sobrescreva o método ObterArea() da classe base e preencha o corpo desta função para retornar a área.

Teste a classe Quadrado

  1. Retorne ao método Main em Program.cs para testar seu código.
  2. Crie uma instância de Quadrado, chame os métodos ObterCor() e ObterArea() e certifique-se de que eles retornem os valores esperados.

Crie as classes Retangulo e Circulo

  1. Repita os passos acima para as classes Retangulo e Circulo, colocando cada uma em seus próprios arquivos, armazenando as variáveis necessárias e sobrescrevendo ObterArea() para cada uma.
  2. Teste essas classes novamente no Main e certifique-se de que funcionam conforme o esperado.

Crie uma lista

  1. No seu método Main, crie uma lista para armazenar figuras (Dica: O tipo de dados deve ser List<Figura>).
  2. Adicione um quadrado, um retângulo e um círculo a esta lista.
  3. Percorra a lista de figuras. Para cada um, chame e exiba os métodos ObterCor() e ObterArea().

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

  1. Verifique se cada uma de suas classes funciona conforme descrito acima.
  2. Confirme (commit) e envie (push) seu código para seu repositório no GitHub.
  3. Verifique se você consegue ver seu código atualizado no GitHub.
  4. Reaponda ao questionário do Canvas para relatar seu trabalho.