quarta-feira, 8 de agosto de 2012

Aula 10 - Herança: o que é e para que serve?


Antes do papo de hoje, vamos dar continuidade na saga das minhocas torcedoras...
A minhoca tricolor ficou P da vida e resolveu chamar seu parente bandido...
Voltando ao que interessa...
Falaremos hoje de um importante conceito da Orientação a Objetos: a Herança! Relembrando que a Orientação a Objetos é uma disciplina regida por muitos conceitos, mas dentre eles podemos eleger quatro importantes pilares: Abstração, Encapsulamento, Herança e Polimorfismo. Já estudamos Abstração e Encapsulamento. Vamos entender nesse texto um pouco do que a Herança pode nos oferecer.
Desde já peço cumplicidade de vocês para com alguns detalhes sobre a Herança. Da mesma forma que a visibilidade protegida só faria sentido quando trabalhássemos com Herança, algumas vantagens da propria Herança frequentemente esbarram com o Polimorfismo (como objetos polimórficos, por exemplo). Vou tentar não falar nada sobre Polimorfismo agora, mas no futuro, eventualmente precisaremos resgatar alguns detalhes dessa aula, ok!?
Aristóteles, como vai? Quanto tempo passou desde nossa última aula... heim?!
Conceitos Gerais sobre Herança
Senhores, a herança na orientação a objetos é tão simples quanto a herança da vida real! Quando um casal tem um filho, as características físicas de ambos são repassadas para a criança. A ideia na programação é a mesma! Leve esse entendimento para a Orientação a Objetos: quando criamos uma classe A, definimos também suas características (atributos) e comportamentos (métodos). Quando criamos uma segunda classe (classe B, por exemplo) que herda da primeira, estamos automaticamente fazendo com que a classe B herde todos os atributos e métodos da classe A.
Claro que há! Pense por exemplo numa modelagem cuja uma classe Pessoa possui os atributos nome, endereço, cidade, cep, estado e telefone, além dos comportamentos de Getters e Setters para todos eles.
Veja que apesar de conceitualmente simples, uma classe desse tipo vai exigir um certo esforço do codificador (muitos métodos e atributos). Imagine agora que o sistema ainda precise representar outras duas classes com semântica similar à classe Pessoa, porém com particularidades distintas: uma seria a classe PessoaFisica e a outra PessoaJuridica. Ambas as classes devem ter todos esses atributos, sendo que a classe PessoaFisica seria acrescida do atributo cpf e a classe PessoaJuridica do atributo cnpj. Como seria a solução?
Putz...
Enfim, nesse caso, na Orientação a Objetos podemos criar essas duas classes como herdeiras (ou, dentro da terminologia, sub-classes) de Pessoa. Ambas automaticamente já teriam todos os atributos de Pessoa (que é chamada de super-classe).
Veja que foram acrescidos os novos atributos e métodos pertinentes às classes PessoaFisica e PessoaJuridica. Na UML, a seta que liga as classes em um relacionamento de herança é chamada de generalização. Como pode ser observado na figura, trata-se de uma seta não-tracejada com origem na classe herdeira e destino na classe pai. Veja que a seta é representada por um retângulo completo sem preenchimento. Essa seta indica justamente que a classe PessoaFisica herda todos os atributos e métodos da classe Pessoa.
Detalhes Importantes sobre Herança
Dentro das formalidades da UML, da Orientação a Objetos e do Java, é dito que a generalização é um relacionamento entre as classes e não entre os objetos. Em outras palavras podemos dizer que a estrutura da classe herdeira é acrescida dos elementos existentes na estrutura da classe pai. Obviamente isso será transportado para as instâncias dessa classe (os objetos), mas como notação, devemos considerar essa definição.
Ótima pergunta! Podemos e devemos fazer algumas observações sobre isso. Primeiro vamos conversar sobre sua consideração a respeito da classe “de cima”. Veja o caso abaixo.
Veja que sintaticamente a representação da classe Pai não precisa ser colocada sempre acima das classes filhas. Todavia, pessoalmente acredito que a representação das classes Filhas abaixo da classe Pai ajuda na compreensão do diagrama.
Agora vamos discutir sobre a nomenclatura das classes envolvidas na generalização. Existem correntes na literatura que empregam os termos Classe Mãe ou Classe Pai para designar a classe de qual os atributos foram herdados. Outra possibilidade que é muito empregada e, aliás, é a que mais me agrada, seria chamar essa classe de Super-classe. De forma semelhante podemos chamar as classes herdeiras de Classe Filha. Prefiro também utilizar o termo Sub-classe.
Existe alguma opção que é mais correta que a outra? Não! Confesso que nunca li a referência original da UML para levantar qual o termo mais correto (se é que existe), mas quando se pensa na prática, devemos ser capazes de entender e interpretar corretamente quaisquer um deles. Veja abaixo um resumo dessa ideia:
Veja que também podemos entender que a Sub-classe é uma especialização da Super-classe, como mostrado na terceira representação da figura acima.
Boa...
É muito importante que vocês entendam que a generalização (herança) é uma associação assimétrica. Isso significa dizer que todos os elementos (atributos e métodos) da Super-classe são repassados para a Sub-classe. Logo, a Sub-classe conhece (e não poderia ser de outra forma) a Super-classe. Todavia a Super-classe não tem nenhuma relação com a Sub-classe. Na classe Mãe não podemos acessar, por exemplo, um atributo ou método da classe filha.
Esse tipo de relacionamento na Orientação a Objetos é parecido com o relacionamento entre jogadores de futebol e seus filhos: o filho conhece o pai, mas o contrário não é verdadeiro!
Sim, claro! Lembre-se que na última aula (http://linubr.blogspot.com.br/2012/08/aula-9-resolvendo-o-exercicio-da-media.html) nós conversamos em como as associações entre objetos são representadas no código Java: como atributos. Logo, quando uma Super-classe possui associações com outras classes, suas Sub-classes também terão. Faremos um exemplo no final desse capítulo e você poderá enxergar isso na prática.
Aristóteles?!! Estou impressionado com você hoje! Boas perguntas...
Voltemos ao exemplo apresentado no início.
Veja que agora a classe PessoaFisica além de ser Sub-classe de Pessoa, também é Super-classe de Funcionario. O relacionamento de generalização permite a construção de qualquer tipo de hierarquia de classes, devendo apenas respeitar a assimetria. No caso acima, a classe Funcionario herda todos os atributos e métodos da classe Pessoa e PessoaFisica. Isso significa dizer que Funcionario possui os atributos nome, endereço, cidade, cpf, entre outros. É correto também entender que Funcionario é uma PessoaFisica, ou ainda que Funcionario é uma Pessoa. Nos conceitos da Orientação a Objetos isso pode ser expresso como Funcionario IS-A PessoaFisica, que traduzido para o português temos: Funcionario É-UMA PessoaFisica.
Ah sim... usar a terminologia Classe-Avó se considerarmos a classe Pessoa em relação à classe Funcionario, não é praticada e defendida no glossário padrão de Orientação a Objetos (pelo que sei), mas as vezes eu mesmo me permito usar essa caracterização para explicar determinada situação na modelagem.
Quando não usar a Herança
Apenas como uma adendo ao material, achei interessante comentar sobre isso. Deve ficar claro para vocês que usar o recurso de herança apenas para evitar a definição de atributos genéricos a vários tipos de classes não é tolerável. O que eu quero dizer com isso é que, mesmo que uma classe Produto tenha o atributo nome e uma classe Cliente também tenha um atributo Nome, não se justifica criar uma classe Coisa (com um atributo nome) como classe pai de Produto e Pessoa apenas para evitar a programação do mesmo atributo e seus Getters e Setters em duas classes diferentes.
Isso significa que a semântica de relação hierárquica entre as classes envolvidas numa herança deve ser respeitada a qualquer custo. Não saia por ai definido classe Carro como super-classe de CachorroQuente só porque ambos possuem o atributo preço.
Encapsulamento + Herança: uma combinação poderosa!
Antes ainda de mostrarmos um exercício em Java, vamos detalhar como se comporta os diferentes tipos de visibilidade quando trabalhamos com Herança. Vejamos o comportamento dos atributos e métodos públicos.
Talvez seja fácil de entender que a SubClasse poderá acessar e modificar o valor do atributoPublico e acessar o método metodoPublico(). Aliás, não só a SubClasse como também quaisquer outras classes do sistema. Caso tenha se esquecido de como a visibilidade pública se comporta, leia por favor o material disponível na aula 8 através do link abaixo:
Vamos agora aos atributos privados.
Nenhuma classe, com exceção da própria classe SuperClasse, poderá acessar o atributo qualquer e o método metodoPrivado(), inclusive sua própria classe filha SubClasse.
A classe filha herda sim todos os atributos e métodos da classe pai! O fato dela não ter acesso não significa que ela não tem os atributos e métodos. Daí você deve estar se perguntando: como então ela poderá utilizá-los? Da mesma forma que as outras classes: através dos métodos públicos! Veja nesse exemplo que existem dois métodos públicos getQualquer() e setQualquer() que permitem o acesso e a definição do valor do atributo privado qualquer, respectivamente. Já o método metodoPrivado() foi construído com a visibilidade privada justamente para esconder a complexidade inerente ao código da SuperClasse. O acesso a ele só deverá acontecer através da chamada de outros métodos públicos que estiverem disponíveis nessa classe - isso também é válido para a SubClasse.
Muito bem!
Como vimos na aula 8, na UML, os atributos e métodos protegidos só estão disponíveis para a própria classe que os implementa e para a classe filha. Isso significa dizer que a SubClasse poderá acessar a característica atributoProtegido e o comportamento metodoProtegido(). Outras classes externas, todavia, não terão acesso.
É muito importante observar que o comportamento da visibilidade protected na UML é distinta da oferecida pela linguagem Java. No Java, além da própria classe e das classes herdeiras, as classes que residem no mesmo pacote também poderão acessar os atributos e métodos protegidos. Como ainda não conversamos sobre pacotes, tanto na UML, quanto no Java, voltaremos a falar sobre esse assunto no futuro.
De qualquer forma essa visibilidade (protected) só tem aplicação plena quando utilizada em conjunto com a herança. Uma correta aplicação para essa visibilidade vai variar de acordo com cada modelagem e sistema específicos. Durante a resolução de um exercício ainda nesse material faremos o uso de um atributo protegido.
Ah, sim... Já que ninguém perguntou, a regra da visibilidade package (de pacote) permanece inalterada: somente a própria classe e as classes do mesmo pacote terão acesso aos atributos e métodos com visibilidade de pacote. É interessante observar que caso uma SubClasse esteja localizada em outro pacote, ela não terá acesso aos atributos e/ou métodos package da SuperClasse. Se não ficou claro... não se preocupe! Idem à visibilidade protected, falaremos sobre package ao abordarmos a ideia dos pacotes.
É exatamente sobre isso que vamos falar agora!
Trabalhando com Herança no Java
No Java podemos aplicar a associação da generalização através da sintaxe extends.
public class SuperClasse {
  // Código da SuperClasse
}
public class SubClasse extends SuperClasse {
  // Codigo da SubClasse
}
Pronto.
Foi só pra te sacanear mesmo... rsrsrsrs!
Estou brincando... vamos fazer agora um exemplo pra fechar esse entendimento!
Aplicando a Herança no Java
Veja abaixo um simples diagrama com uma modelagem qualquer.
Nesse sistema existem duas calculadoras: uma CalculadoraSimples e uma CalculadoraComplexa. Na classe CalculadoraSimples existem os atributos a e b, respectivamente, com as visibilidades privada e protegida. Veja também que CalculadoraSimples possui no mínimo 1 e no máximo N objetos da classe Pilha (omiti os atributos e métodos de Pilha por não agregarem importância ao exercício). Além dos atributos, a classe CalculadoraSimples possui alguns comportamentos: getA() e setA() são métodos Getter e Setter para o atributo a; somar() é um método que recebe dois parâmetros, guarda os valores nos atributos e retorna a soma dos valores.
A classe CalculadoraComplexa possui um relacionamento de generalização com a classe CalculadoraSimples. Além de ter todas as características de CalculadoraSimples, a sub-classe CalculadoraComplexa definiu novos atributos e métodos. No atributo resultado será guardado o resultado do produto entre os valores passados para o método multiplicar(). Existe ainda o método getResultado() que poderá retornar o resultado da última multiplicação que a calculadora resolveu.
Vamos aos códigos. Primeiramente vou mostrar os códigos das classes Pilha e CalculadoraSimples que já são mais corriqueiros no nosso dia-a-dia.
// Arquivo Pilha.java
public class Pilha {
}
// Arquivo CalculadoraSimples.java
public class CalculadoraSimples {
  private int a;
  protected int b;
  private Pilha[] pilhas;
  public int getA() {
    return a;
  }
  public void setA(int vA) {
    a = vA;
  }
  public int somar(int vA, int vB) {
    setA( vA );
    b = vB;
    return getA() + b;
  }
}
Veja que para representar corretamente o modelo, existe um array de Pilha na classe CalculadoraSimples. Note também que o atributo b foi definido como protegido. Um adendo interessante é que no método somar() eu optei em chamar os métodos getA() e setA() para acessar e definir o valor do atributo a. Por que eu não acessei diretamente o atributo? Quando existem métodos Getters e Setters, pode ocorrer dentro desses métodos algum comportamento de negócio (aqui não houve nenhum). Então é uma boa prática utilizar os métodos de acesso aos atributos, mesmo que eles estejam disponíveis na própria classe.
Vamos à classe CalculadoraComplexa.
public class CalculadoraComplexa extends CalculadoraSimples {
  private int resultado;
  public int multiplicar(int vA, int vB) {
    setA( vA );
    b = vB;
    resultado = getA() * b;
    return resultado;
  }
  public int getResultado() {
    return resultado;
  }
}
O primeiro detalhe que devo destacar é o uso da sintaxe extends. Esse simples comando faz com que tudo que exista na classe CalculadoraSimples seja herdado pela CalculadoraComplexa. Veja, por exemplo, que dentro do código do método multiplicar() o atributo b foi acessado diretamente (isso só foi possível pelo fato dele ser protegido) e o atributo a foi acessado através do Getter e Setter.
Vamos agora mostrar uma classe auxiliar em que o pequeno projeto foi executado.
public class Executora {
  public static void main(String args[]) {
   
    CalculadoraComplexa c = new CalculadoraComplexa();
    System.out.println( "A soma de 2+3 e: " + c.somar( 2 , 3 ) );
    System.out.println( "A multiplicacao de 2*3 e: " + c.multiplicar( 2 , 3 ) );
  }
}
Através da execução dessa classe, chegamos ao seguinte resultado.
Bom... é isso! Vamos agora a um exercício.
Exercício de Fixação (difícil)
Esse é um exercício que podemos dizer que apresenta uma dificuldade média/alta. Optei em passá-lo para tentarmos aplicar todos os nossos conhecimentos sobre Abstração, Encapsulamento e Herança. Conseguindo resolvê-lo, avançaremos para outras ponderações sobre a Herança. Como de costume, vou postar para vocês uma descrição dos requisitos. Vocês devem ser capazes de modelar e implementar essa solução através das linguagens UML e Java - é claro, com um maior foco no Java.
Requisitos do Sistema 
1. Modelar um sistema que permita o usuário escolher entre duas alternativas de estrutura de dados: uma Pilha ou uma Fila. 
2. Ambas estruturas devem ser capazes de armazenar um conjunto de até 10 números inteiros. 
3. O sistema deve oferecer a possibilidade do usuário colocar ou retirar elementos da estrutura, de forma iterativa. Mensagens devem ser mostradas na tela quando os limites superior e inferior da estrutura forem alcançados.
Observações técnicas (dicas): 
  • Uma Pilha é uma estrutura em que o último elemento que entra é o primeiro que sai. Lembre-se de uma pilha de pratos. O termo mais utilizado para representar uma Pilha é LIFO (Last-in First-out). 
  • Uma Fila é uma estrutura de dados em que o primeiro elemento que entra é o primeiro que sai. Lembre-se de uma fila do banco. O termo mais conhecido é FIFO (First-in First-out). 
  • Você deve utilizar os conceitos de Abstração, Encapsulamento e Herança, como também elementos da sintaxe do Java como arrays e estruturas de repetição.
Boa sorte!
Agora é com você!
Caro leitor, postei de propósito essa figura abaixo apenas para chamar sua atenção! Não apresentarei a resposta desse exercício aqui! Todavia, postei mais adiante uma foto da modelagem que fizemos no quadro, em sala de aula. Se você deseja treinar a modelagem de sistemas Orientados a Objetos, não olhe essa sugestão de modelagem, por enquanto. Tente criar uma solução você mesmo! Não fique frustrado caso não consiga! Esse exercício realmente é difícil.
Modelagem realizada em sala
Essa caneta verde não está ajudando muito a enxergar né?! Mas tudo bem... no próximo artigo faremos juntos a modelagem.
Abraços, Guilherme Pontes

Nenhum comentário:

Postar um comentário