CSE 210: Programação com Classes

S05 - Atividade de Aprendizagem: Herança

Visão Geral

Nesta atividade, você aprenderá e praticará o princípio da Herança.

Preparação

O que é Herança?

Herança é a capacidade de uma classe obter os atributos e métodos de outra classe diretamente, sem precisar digitá-los. Segue a mesma ideia de pessoas herdando certas características de seus pais.

Considere duas classes, Pessoa e Estudante. Uma pessoa pode ter um determinado conjunto de atributos e métodos que todas as pessoas compartilham, como ObterNome(). Um estudante é uma pessoa, então o estudante deve ter todas as propriedades e comportamentos que uma pessoa tem, mas um estudante pode ter outros itens mais específicos, como um número de identificação de estudante, que pode ser acessado por meio de um método ObterNumero(). Nesse caso, poderíamos fazer com que a classe Estudante herdasse todas as funcionalidades da classe Pessoa e, então, adicioná-las.

Considere o seguinte código.


// uma classe normal chamada Pessoa
public class Pessoa
{
    public string ObterNome()
    {
        return "José";
    }
}

// uma classe que herda de Pessoa
public class Estudante : Pessoa
{
    public string ObterNumero()
    {
        return "0123456789";
    }
}

// a instância de estudante tem automaticamente o método ObterNome()!
Estudante estudante = new Estudante();
string nome = estudante.ObterNome();
Console.WriteLine(nome);

Saída:


José

Nesse caso, a classe Pessoa é conhecida como classe pai. A classe Estudante é conhecida como classe filho. Elas também são chamadas de classes base e derivadas ou superclasse e subclasse. Não importa qual par de termos você usa, desde que você entenda o princípio.

A sintaxe para especificar um relacionamento de herança é diferente de linguagem para linguagem, mas sempre é encontrada na declaração da classe derivada. Em C#, ao definir o nome da classe, você usa dois-pontos seguidos do nome da classe base. Nenhuma outra sintaxe especial é necessária.

Um diagrama de classes que mostra esse relacionamento exibe a classe base no topo e a classe derivada abaixo dela. Uma seta com ponta aberta vai da classe derivada para a classe base.

Diagrama de classes mostrando herança
Diagrama de classes mostrando herança

O benefício real da herança é demonstrado na última parte do exemplo acima. Você é capaz de chamar o método ObterNome em uma instância de Estudante mesmo que ele não esteja definido naquela classe. A classe Estudante obteve-o automaticamente em virtude do relacionamento de herança com Pessoa.

Super e Base

Em algumas circunstâncias, é útil poder chamar métodos em uma classe pai a partir de uma classe filho. Em C#, você usa a palavra-chave base. Considere o seguinte código:


// uma classe pai chamada Pessoa
public class Pessoa
{
    private string _nome;

    public Pessoa(string nome)
    {
        _nome = nome;
    }

    public string ObterNome()
    {
        return _nome;
    }
}

// uma classe filho chamada Estudante
public class Estudante : Pessoa
{
    private string _numero;

    // chamando o construtor primário usando "base"!
    public Estudante(string nome, string numero) : base(nome)
    {
      _numero = numero;
    }

    public string ObterNumero()
    {
        return _numero;
    }
}

Estudante estudante = new Estudante("Brigham", "234");
string nome = estudante.ObterNome();
string numero = estudante.ObterNumero();
Console.WriteLine(nome);
Console.WriteLine(numero);

Saída:


Brigham
234

Neste exemplo, a classe Estudante herda da classe Pessoa. O construtor Estudante chama o construtor Pessoa usando a palavra-chave base e passa o parâmetro nome.

Observe que base não se limita a construtores. Podemos usá-lo em qualquer lugar nos métodos da classe derivada, com notação de ponto, para invocar um comportamento na classe base, como mostra o exemplo a seguir.


string numero = base.ObterNome();
Console.WriteLine($"Número do Estudante: {numero}")

Acessando Dados Privados

No exemplo acima, Estudante herda a variável membro _nome da classe base, mas como ela é privada, você não pode acessar _nome diretamente em métodos definidos na classe Estudante. Considere um método para estudantes chamado ObterDadosEstudante() que retorna o nome e o número de identificação do estudante. Talvez você queira escrever algo como o seguinte:


public class Estudante : Pessoa
{
    private string _numero;

    ...

    public string ObterDadosEstudante()
    {
        // ERRO! Esta linha não funciona porque _nome é privado na classe base
        return _nome + " " + _numero;
    }
}

Há duas maneiras de corrigir esse problema. A primeira é criar um getter para a variável _nome na classe base e então, neste método, você pode chamar o getter para acessar o valor.

A outra abordagem é tornar a variável acessível à classe derivada. Já aprendemos sobre public e private, mas há outro nível intermediário chamado protected (protegido). Variáveis membro e métodos rotulados como protected podem ser acessados por métodos na classe, bem como por métodos em classes derivadas, mas não podem ser acessados por código fora dessas classes.

Então, qual é melhor?

De modo geral, devemos tentar limitar o acesso às variáveis o máximo possível, pois tornar uma variável membro do tipo protected em vez de private aumenta o acesso a ela, o que pode abrir caminho para mais problemas no futuro. Portanto, geralmente é melhor usar private para a variável membro e então usar o getter para retornar seu valor a outras classes. Há casos, no entanto, em que isso causa mais problemas do que ajuda e faz sentido tornar a variável protected e acessá-la diretamente na classe derivada.

Substituição e Relacionamentos "É Uma" (Is-A)

Um ponto importante a ser observado com a herança é que, como uma classe derivada "é uma" (is-a) versão mais específica de uma classe base (por exemplo, um estudante "é uma" pessoa), não apenas a classe derivada herda todas as características e comportamentos da classe base, mas você também deve ser capaz de usar a classe derivada em qualquer lugar onde poderia usar a classe base.

Por exemplo, como um Estudante é uma Pessoa, qualquer código que funcione com um objeto Pessoa deve ser capaz de funcionar com um objeto Estudante sem dar erro. Isso inclui passar o objeto Estudante para funções que esperam um objeto Pessoa, bem como colocar um objeto Estudante em uma lista de objetos Pessoa.

Esse conceito de substituição se tornará ainda mais importante com o princípio do polimorfismo, que é o tópico da próxima aula.

Princípio da Substituição de Liskov

A ideia de ser capaz de substituir um objeto derivado no lugar de um tipo herdado é formalmente chamada de Princípio da Substituição de Liskov, em homenagem a Barbara Liskov, que o apresentou em uma conferência em 1987.

Você também pode observar que o Princípio da Substituição de Liskov é o "L" dos populares princípios de design SOLID da programação orientada a objetos.

Demonstrações em Vídeo

Por favor, assista aos vídeos a seguir que discutem esses conceitos em mais detalhes:

Link Direto: ▶️Herança (9 minutos)
Link Direto: ▶️Herança no C# (6 minutos)
Link Direto: ▶️Detalhes sobre Herança no C# (8 minutos)

Uma Palavra de Cautela

Herança é um princípio poderoso que pode economizar muitas horas de codificação. No entanto, o uso excessivo pode causar problemas. Considere uma longa cadeia de herança de 10, 15, 20 ou até mais classes! Pode ser extremamente difícil e demorado inspecionar uma longa hierarquia de herança apenas para entender uma única classe na parte inferior.

Patrick Wyatt, um desenvolvedor de jogos de longa data, escreveu sobre esse problema em um post de blog chamado Tough times on the road to Starcraft (Tempos difíceis no caminho para Starcraft) (conteúdo em inglês). A herança pertence a programas com classes. No entanto, a experiência do Sr. Wyatt é muito instrutiva.

As opiniões variam, mas uma boa regra prática é limitar os níveis de herança ao número médio de itens que uma pessoa consegue lembrar de uma só vez. Para a maioria das pessoas, isso significa três ou quatro. Se você perceber que está criando mais, pare e pergunte-se: "Eu realmente preciso de uma abstração diferente?"

Resumidamente

Herança é o terceiro princípio da programação com classes. A chave para entender é lembrar que herança é um mecanismo de reutilização de código. Em vez de escrever a mesma coisa várias vezes, podemos simplesmente herdar de uma classe para outra.

Mas tenha cuidado. Como um certo tio disse uma vez ao seu sobrinho super-herói em ascensão: "com grandes poderes vêm grandes responsabilidades!" Discipline-se na forma como você aplica a herança. Mantenha suas hierarquias simples e gerenciáveis. Você conseguirá adicionar mais funcionalidades em menos tempo, garantindo que seu programa continue fácil de manter.

Instruções da Atividade

Pratique o princípio da herança criando uma classe base e classes derivadas.

Para esta atividade, você escreverá classes para representar diferentes tipos de tarefas de casa. Considere o seguinte exemplo de tarefas de matemática e redação.

Tarefas de Matemática

Uma tarefa de matemática pode precisar armazenar o nome do estudante, o tópico (por exemplo, "Frações"), o capítulo do livro didático (por exemplo, “7.3") e os problemas dessa seção (por exemplo, "3-10, 20-21").

A tarefa de matemática deve ter um construtor que exija um valor para cada um dos itens que ele armazena.

A tarefa de matemática precisa fornecer um método para retornar um resumo da tarefa que contenha o nome do estudante e o tópico, bem como fornecer um método para exibir a lista de tarefas de matemática, incluindo o número do capítulo e os problemas (por exemplo, "Capítulo 7.3 Problemas 8-19").

Tarefas de Redação

Uma tarefa de redação pode precisar armazenar o nome do estudante, o tópico (por exemplo, "História da Europa") e o título da tarefa (por exemplo, "As Causas da Segunda Guerra Mundial").

A tarefa de redação deve ter um construtor que exija um valor para cada um dos itens que ele armazena.

A tarefa de redação precisa fornecer um método para retornar um resumo da tarefa que contenha o nome do estudante e o tópico, bem como fornecer um método para obter as informações da redação que consistem no título e no nome do estudante (por exemplo, "As Causas da Segunda Guerra Mundial, por Mary Waters").

Projetar as Classes

Há uma série de coisas em comum entre essas classes e uma série de diferenças. Usando herança, você pode separar as coisas que mudam das coisas que permanecem as mesmas, colocando os elementos comuns em uma classe base e os elementos diferentes em uma classe derivada.

Considere o seguinte diagrama de classes:

Diagrama de classes mostrando classes separadas
Diagrama de Classes Sem Herança

A partir desses diagramas, você pode ver que os atributos _nomeEstudante e _topico são os mesmos em ambas as classes, assim como o método ObterResumo(). Em vez de duplicar esses itens, você pode criar uma classe base da qual ambos herdam.

O diagrama de classes a seguir mostra uma abordagem que usa herança. Esta é a abordagem que você usará para esta tarefa.

Diagrama de classes mostrando herança
Diagrama de Classes Mostrando Herança

Inicie o projeto

  1. Abra o projeto da classe no VS Code.
  2. Navegue até o projeto Tarefas na pasta semana05. 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 base

  1. Comece criando um novo arquivo e uma classe para sua classe base Tarefa.
  2. Adicione os atributos como variáveis membro privadas.
  3. Crie um construtor para essa classe que receba um nome de estudante e um tópico e defina as variáveis membro.
  4. Adicione o método ObterResumo() para retornar o nome do estudante e o tópico.
  5. Teste sua classe retornando ao método Main no arquivo Program.cs. Crie uma tarefa simples, chame o método para obter o resumo e depois exiba-o na tela.

Exemplo de Resultado


Samuel Silva - Multiplicação

Crie a classe TarefaDeMatematica

  1. Crie um novo arquivo para a classe TarefaDeMatematica.
  2. Crie esta classe e certifique-se de especificar que ela herda da classe base Tarefa.
  3. Adicione os atributos como variáveis membro privadas. Certifique-se de não criar novas variáveis membro para aquelas que você já herdou da classe base.
  4. Crie um construtor para sua classe que aceite todos os quatro parâmetros e faça com que ele chame o construtor da classe base para definir os atributos da classe base dessa maneira.
  5. Adicione o método ObterListaDeTarefas().
  6. Teste sua classe retornando ao método Main e criando um novo objeto TarefaDeMatematica e definindo seus valores. Certifique-se de testar os métodos ObterResumo() e ObterListaDeTarefas().

Exemplo de Resultado


Roberto Rodriguez - Frações
Capítulo 7.3 Problemas 8-19

Crie a classe TarefaDeRedacao

  1. Siga o mesmo padrão anterior criando um novo arquivo para a classe TarefaDeRedacao.
  2. Crie a classe e configure o relacionamento de herança.
  3. Adicione as variáveis membro e configure o construtor como você fez para a classe TarefaDeMatematica.
  4. Adicione o método ObterInformacoesDaRedacao().
  5. Observe que esse método precisa acessar a variável _nomeEstudante definida na classe base. Embora a classe TarefaDeRedacao tenha herdado esse atributo, ele é privado, então você não pode acessá-lo diretamente na classe derivada.

    Para obter os dados necessários para o método, você pode tornar a variável protected na classe base ou criar um método público ObterNomeEstudante para retorná-la.

  6. Retorne a Main e teste sua nova classe.

Exemplo de Resultado


Maria Antunes - História da Europa
As Causas da Segunda Guerra Mundial

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. Responda ao questionário do Canvas para relatar seu trabalho.