[Pesquisar este blog]

sábado, 3 de fevereiro de 2018

POO::Fundamentos-09-Métodos de Acesso e Alteração

POO-F-08-Encapsulamento POO-F-10-Pacotes e Espaços de Nomes
Na OO, o encapsulamento é o agrupamento de ideias correlacionadas em uma classes. Por meio dos especificadores de acesso, a visibilidade de um membro da classe pode ser modificada, possibilitando o encapsulamento da implementação ou ocultação das informações; e também permite construir mecanismos sofisticados para garantia da integridade dos dados mantidos pelos objetos de uma classe.

Consideremos o problema simples e frequente na programação de garantir que uma variável de tipo conhecido tenha valores limitados a um conjunto ou critério determinado. Por exemplo, considere a classe Java/C# a seguir, cujo propósito é representar um horário, no formato de 24 horas, incluindo apenas horas e minutos.

public class Horario {
  int horas;
  int minutos;
}

Embora seja claro o propósito de cada um de seus atributos, a forma na qual foram declarados não oferece qualquer mecanismo que assegure a consistência dos seus valores, isto é, a garantia de que seus campos correspondam efetivamente a um horário. Veja, através do fragmento Java/C# apresentado na sequência, que elementos desta classe podem receber valores inválidos na sua inicialização ou outra situação:

Horario h1 = new Horario();
h1.horas = 33;   // valor inválido para horas
h1.minutos = 14; // valor válido para minutos

Horario h2 = new Horario();
h2.horas = 12;   // valor válido para horas
h2.minutos = -1; // valor inválido para minutos

O problema acontece porque os atributos horas e minutos declarados na classe são do tipo inteiro e públicos, assim seus usuários podem atribuir quaisquer valores inteiros válidos, ou seja, pertencentes ao intervalo [-2^31, +2^31-1) do tipo int; quando o correto seria manter o campo hora dentro do intervalo [0, 23] e minutos dentro do intervalo [0, 59], ambos bem mais restritos.

Este tipo de problema e outros podem ser facilmente resolvidos através do encapsulamento provido pelas classes através de duas medidas:
  1. restringindo o acesso aos campos cujos valores possuem restrições, declarado-os como privados ou protegidos, de modo que não possam ser utilizados diretamente; e 
  2. implementando métodos especialmente destinados a manipulação destes campos, como meio de garantir que assumam apenas valores consistentes.
Os métodos (funções-membro) de qualquer classe são sempre de um dos tipos que segue:
  • Criação
    Operações responsáveis pela criação de novos objetos (construtores no Java e C#).
  • Destruição
    Operações responsáveis pela eliminação de objetos existentes (finalizadores no Java).
  • Observação
    Operações que informam sobre o estado do objeto, sem modificá-lo.
  • Mutação
    Operações que alteram o estado do objeto, mantendo-o consistente.
  • Produção
    Operações que criam novos objetos a partir de objetos existentes.
Na classe Horario, os atributos horas e minutos, que representam os componentes do horário, devem ser números inteiros e positivos, limitados aos intervalos hora: [0, 23] e minutos: [0, 59],

Estas restrições exigem que os atributos horas e minutos sejam declarados privados, evitando que os usuários da classe atribuam valores inválidos. Mas sendo privados, seus valores não poderão ser ajustados ou sequer consultados externamente à classe. Assim lançamos mão de métodos de observação, ou seja, operações públicas que podem informar sobre o valor destes atributos, sem alterá-los; e também métodos de mutação, para que sua alteração possa ser controlada e mantida de acordo com as regras definidas para horários válidos. Analise o código que segue, com a classe Horario (Java/C#) melhorada:

public class Horario {
   private int horas;
   private int minutos;

   public int getHoras() {
      return horas;
   }
   public int getMinutos() {
      return minutos;
   }

   public void setHoras(int h) {
      if (h<0 || h>23) {
         // lança exceção para indicar condição inválida
         throw new RuntimeException("h inválido: " + h);
      }
      horas = h;
   }
   public void setMinutos(int m) {
      if (h<0 || h>59) {
         // lança exceção para indicar condição inválida
         throw new RuntimeException("m inválido: " + m);
      }
      minutos = m;
   }
}

A classe Horario melhorada exibe três características:
  • Os atributos horas e minutos são privados, portanto não podem ser acessados externamente à classe.
  • Cada atributo possui um método de prefixo get associado: horas e getHoras; minutos e getMinutos.
  • Cada atributo também possui um método de prefixo set associado: horas e setHoras; minutos e setMinutos.
Os métodos de prefixo set são denominados de acesso (ou getter methods), pois são operações de observação, usualmente públicas, sem parâmetros, que retornam o conteúdo dos atributos relacionados e permitem, assim, consultar externamente tais atributos.

Já os métodos de prefixo set são chamados de ajuste (ou setter methods), pois são operações de mutação, que podem ser públicas, sem valor de retorno e que recebem um parâmetro do mesmo tipo do atributo associado, ajustando o atributo para o valor do argumento recebido, apenas quando o mesmo é válido.

É típico que, quando as regras de validação não são atendidas, que seja lançada uma exceção, mecanismo tradicional nas linguagens de programação OO para indicar erros que devem ser tratados pelo programador, mas sem uso de códigos de erro ou exibição direta de mensagens.

A estrutura sintática típica dos conjuntos atributo-métodos é sempre a mesma:

private <Tipo> <nomeAtributo>;
public void set<NomeAtributo>(<Tipo>); // setter method
public <Tipo> get<NomeAtributo>(void); // getter method

Esta estratégia é muito interessante, por várias razões:
  • O atributo privado fica armazenado em segurança dentro da classe.
  • A consulta fica condicionada a existência de método get público associado.
  • Se o método get é protegido, a consulta se limita as subclasses; se private a leitura do atributo deixa de ser possível.
  • A alteração fica condicionada a existência de método set público associado e também às regras (de negócio) que devem ser aplicadas.
  • As regras de validação dos atributos podem ser complexas e relacionadas com o estado de outros atributos.
  • Se o método set é protegido, o ajuste do atributo só pode ocorrer em subclasses; se private, deixa de ser possível.
  • Mudanças nas regras de validação são transparentes, pois não requerem que outras partes do código sejam alteradas (desde que as regras sejam atendidas).
Quando o atributo é do tipo lógico (boolean) também pode ser adotada uma variante da estrutura sintática apresentada, que é como segue:

private boolean <nomeAtributo>;
public void set<NomeAtributo>(<Tipo>); // setter method
public boolean is<NomeAtributo>(void); // getter method

Pode ser observado que o método de ajuste/alteração set tem a mesma forma, mas o método de consulta/observação get foi substituído por outro de prefixo is, que, semanticamente, é mais coerente com o atributo lógico. Se o atributo boolean é denominado ligado, teríamos os métodos setLigado(boolean) e isLigado().

Um exemplo completo pode auxiliar na compreensão destes conceitos.

Considere a necessidade de uma classe Java/C#, cujos objetos devem armazenar as medidas dos lados de um triângulo qualquer. Tal classe poderia ser declarada como

public class Triangulo {
  double ladoA; // lados de um triângulo
  double ladoB;
  double ladoC;
}

Embora seja claro o propósito de cada um de seus atributos, na forma com que se apresenta, não oferece qualquer mecanismo que assegure a consistência dos seus valores, isto é, a garantia de que os valores das medidas correspondam efetivamente a um triângulo. Veja, através do fragmento apresentado a seguir, que elementos desta classe podem receber valores inválidos na sua inicialização ou outra situação:

Triangulo triang1 = new Triangulo();
triang1.ladoA = 5.0; // medidas válidas
triang1.ladoB = 2.0; // mas que não formam um triângulo
triang1.ladoC = 1.5;

Triangulo triang2 = new Triangulo();
triang2.ladoA = -2.0; // medida inválida
triang2.ladoB = -2.0; // medida inválida
triang2.ladoC = 1.5;

Na classe Triangulo, os atributos ladoA, ladoB e ladoC, que representam os lados do triângulo, devem ser números reais e positivos. Além disso, para que tais medidas representem um triângulo, a soma de quaisquer dois lados deve ser maior do que o lado restante.

Estas restrições exigem que os atributos ladoA, ladoB e ladoC sejam declarados privados. Sendo privados, seus valores não poderão ser diretamento ajustados ou consultados externamente à classe, assim lançamos mão de métodos de acesso (de observação) que podem informar sobre o valor destes atributos; e também métodos de ajuste (de mutação), para que sua alteração possa ser controlada e mantida de acordo com as regras dos triângulos.

A classe Java/C# Triangulo pode ser aperfeiçoada como segue.

public class Triangulo {
   // lados do triângulo
   private double ladoA, ladoB, ladoC;

   // métodos de acesso
   public double getLadoA() { return ladoA; }
   public double getLadoB() { return ladoB; }
   public double getLadoC() { return ladoC; }

   // método de observação
   public boolean isEquilatero() {
      return ladoA==ladoB && ladoB==ladoC;
   }

   // métodos de ajuste
   public void setLadoA(double a) {
testaLados(a, ladoB, ladoC);
ladoA = a;
   }
   public void setLadoB(double b) {
testaLados(ladoA, b, ladoC);
ladoB = b;
   }
   public void setLadoA(double b) {
testaLados(LadoA, ladoB, c);
ladoC = c;
   }

   // construtor
   public Triangulo(double a, double b, double c) {
testaLados(a, b, c);
      // se lados válidos, ajusta atributos
ladoA = a;
ladoB = b;
ladoC = c;
   }

   // método auxiliar privado
   // verifica se medidas dadas forma um triângulo
   private void testaLados(double a, double b, double c) {
      if ((a<0) || (b<0) || (c<0) ||
            !(a<b+c) || !(b<a+c) || !(c<a+b)) {
         // lança exceção para indicar condição inválida
         throw new RuntimeException("lados inválidos: " +
            a + ", " + b + ", " + c);
      }
   }
}

É possível notar na classe Triangulo que os lados do triângulo são representados por três variáveis-membro privadas de tipo double denominadas ladoA, ladoB e ladoC. Os métodos de acesso getLadoA, getLadoB e getLadoC permitem consultar as medidas dos lados. O método isEquilatero() simula a presença de um atributo lógico que indica se o triângulo é ou não equilátero. Como o atributo, de fato, não existe, este método é mais precisamente de observação.

Como validação de cada lado é semelhante, foi criado o método privado testaLados(double, double, double), que permite verificar se as medidas são positivas e se formam um triângulo válido. Assim, cada método de ajuste (setLadoA(double), setLadoB(double) e setLadoC(double)) primeiro efetua o teste da nova medida junto as existentes, para depois armazenar sua alteração.

O construtor parametrizado requer as medidas dos três lados, testando-os e armazenando-os se válidos. Não existe construtor default porque não faz sentido criar um triângulo sem medidas definidas ou mesmo com ladoA=ladoB=ladoC=0. A instanciação de um objeto do tipo Triangulo é obtida com:

Triangulo triangulo = new Triangulo(1.6, 2.5, 3.4);

O programa Java listado na sequência mostra como a classe Triangulo poderia ser usada.

import java.util.Scanner;

public class TestaTriangulo {
   public static void main(String[] arg) {
      // declara e instancia objeto tipo Scanner
      Scanner teclado = new Scanner(System.in);

      // leitura das medidas dos lados
      System.out.print("Lado A: ");
      double a = teclado.nextDouble();
      System.out.print("Lado B: ");
      double b = teclado.nextDouble();
      System.out.print("Lado C: ");
      double c = teclado.nextDouble();

      // declara e instancia objeto tipo Triangulo
      Triangulo tri = new Triangulo(a, b, c); 

      // exibe lados do triangulo
      System.out.println("Triangulo: " + tri.getLadoA() +
            ", " + tri.getLadoC() + ", " + tri.getLadoC())
   }
}

É importante observar a divisão de responsabilidade proporcionada pelas classes Triangulo e TestaTriangulo. TestaTriangulo é um programa que realiza a interface com o usuário (leitura das medidas, criação de objetos auxiliares e exibição de resultados), não se preocupando em validar as medidas dos lados, pois esta é uma responsabilidade da classe Triangulo. Já a classe Triangulo se ocupa apenas em representar corretamente um triângulo, não se preocupando com a origem dos valores, nem com interação com o usuário. A classe Scanner se ocupa da leitura dos valores.

A divisão de responsabilidades é útil porque cada segmento do programa (cada classe) se ocupa de coisa específica e limitada, facilitando o desenvolvimento, o teste e a manutenção do código. Isto é o que chamamos de coesão.

O programa ainda é limitado, pois se medidas inválida forem fornecidas, ocorre uma exceção que impede a continuação do programa. Com o uso do tratamento de exceções, a ser visto nas próximas lições, é possível construir um programa mais adequado para o usuário.

Concluindo, é uma prática de programação consagrada, recomendada pelos manuais de Engenharia de Software, que os atributos de uma classe sejam privados e dotados de métodos de acesso e ajuste, conforme as necessidades da classe (ou seja, conforme as regras de validação - de negócio).

POO-F-08-Encapsulamento POO-F-10-Pacotes e Espaços de Nomes

Referências Bibliográficas

[1] JAMSA, K.; KLANDER, L.. Programando em C/C++: a bíblia. São Paulo: Makron Books, 1999.
[2] PAGE_JONES, M.. Fundamentos do Desenho Orientado a Objeto com UML. São Paulo: Makron Books, 2001.
[3] SOMMERVILLE, I.. Software Engineering. 6th. Ed. Harlow: Pearson, 2001.
[4] DEITEL, H.M.; DEITEL, P.J.. Java: como programar. 6a. Ed. São Paulo: Pearson Prentice-Hall, 2005.
[5] SAVITCH, W.. C++ Absoluto. São Paulo: Pearson Addison-Wesley, 2004.
[6] JANDL JR., P. Introdução ao C++. São Paulo: Futura, 2003.
[7] JANDL JR., P.. Java - guia do programador. 3a. ed. São Paulo: Novatec, 2015.
[8] RUMBAUGH, J.; BLAHA, M.; PREMERLANI, W.; EDDY, F.; LORENSEN, W.. Object-oriented modeling and design. Englewoods Cliffs: Prentice-Hall, 1991.
[9] STROUSTRUP, B.. The C++ Programming Language. 3rd Ed. Reading: Addison-Wesley, 1997.
[10] LANGSAM, Y.; AUGENSTEIN, M. J.; TENENBAUM, A. M.. Data structures using C and C++. 2nd Ed. Upper Saddle River: Prentice-Hall, 1996.
[11] WATSON, K.; NAGEL, C.; PEDERSEN, J.H.; REID, J.D.; SKINNER, M.; WHITE, E.. Beginning Microsoft Visual C# 2008. Indianapolis: Wiley Publishing, 2008.

quarta-feira, 31 de janeiro de 2018

POO::Fundamentos-08-Encapsulamento

POO-F-07-Construtores POO-F-09-Getters e Setters

O termo encapsulamento se refere ao agrupamento de ideias correlacionadas na forma unidades, módulos ou classes, que passam a ser usadas apenas por meio de seu nome [2]. É um conceito amplo, pois se aplica na construção de métodos, de classes e, portanto, dos objetos, muito importante na OO.

Sabemos que os métodos nada mais são do que funções-membro ou subprogramas que realizam uma tarefa, ou seja, um conjunto organizado de ações relacionadas. Métodos são acionados por meio de seus nomes. Já os objetos novos são criados com o uso do nome de suas classes.

O encapsulamento também determina a representação do objeto, ou seja, possibilita que um objeto seja usado apenas por sua aparência exterior [6], o que pode ser denominado de interface (no sentido de ligação entre as partes -- faces -- externa e interna dos objetos).

Aqui o encapsulamento está muito relacionado a ocultação de informações (data hiding), outra característica da OO, que permite preservar informações importantes dentro do objeto, externando apenas o que se julga conveniente ou necessário. Até mesmo parcelas da própria construção do objeto, ou seja, sua implementação, podem ser escondidas pelo mesmo mecanismo. A ocultação das informações é obtida por meio dos mecanismos de encapsulamento [2][6].

O encapsulamento da implementação ou ocultação das informações, também conhecido como visibilidade ou acessibilidade dos membros de uma classe, é a forma com que seus elementos podem ser vistos e utilizados externamente. Então o encapsulamento define como os membros da classe, isto é, seus atributos, seus métodos e seus construtores da classe poderão ser usados externamente. Aqui é necessário definir mais precisamente os conceitos de interno e externo.

Interno

Interno é aquilo que faz parte da construção/implementação da própria classe. No Java e C# é tudo aquilo que está dentro das chaves que delimitam o corpo da classe. Observe a classe Contador:
class Contador {
   double total;
   Contador() {
      set(100);
   }
   void inc(double d) {
      total = total + d;
   }
   void dec(double d) {
      total = total - d;
   }
   void set(double d) {
      total = d;
   }
}

A variável-membro total, assim como as funções-membro inc(double), dec(double) e set(double), bem como o construtor Contador() são elementos internos da classe Contador. Alguns membros usam outros membros da classe.

Externo
Externo é o uso que as instâncias da classe fazem dos membros desta em outras partes do programa (certamente em outras classes). O uso externo é realizado pelos "clientes" ou "usuários" da classe.
Contador cont = new Contador();
contador.total = 23.5;
contador.dec(0.5);
Neste fragmento, o construtor Contador() é usado externamente à classe Contador, assim como os membros total e dec(double).

A ocultação de dados (data hiding) é muito importante dentro da OO, pois:

  1. permite esconder detalhes da forma de implementação, permitindo resguardar esforços de desenvolvimento;
  2. possibilita criar de mecanismos de consistência, que asseguram que os atributos assumam apenas valores restritos a um conjunto desejado;
  3. garante que operações determinadas sejam realizadas na sequência correta, ou condicionado a estados apropriados do objeto; e
  4. assegura que as instâncias da classe conheçam apenas aquilo que é necessário para o uso dos objetos criados.
O projetista de uma classe tem assim liberdade para determinar quais elementos serão conhecidos e utilizados, separando-os, por meio da ocultação, daqueles que existem apenas para permitir sua implementação. Assim, atributos, métodos e até mesmo construtores que existem numa classe podem ou não ser visíveis externamente.

Em Java, C#, C++ (e outras linguagens OO) a acessibilidade ou visibilidade dos membros da classe é determinada por especificadores de acesso (access specifiers), palavras reservadas destas linguagens que definem seu (grau de) encapsulamento: public, protected e private.

public
A palavra reservada public determina o acesso público. Com ele, membros de uma classe têm utilização irrestrita e podem ser usados livremente pelas suas instâncias. Atributos (variáveis-membro) públicos podem ter seus conteúdos acessados (lidos) e alterados (escritos) sem qualquer restrição; assim como os métodos públicos podem ser acionados arbitrariamente. Qualquer membro declarado público fica, portanto, exposto.
Os membros públicos, vistos em conjunto, compõem a interface da classe, ou seja, são o conjunto de atributos e operações conhecidos e que caracterizam externamente a classe.

private
O acesso privado, indicado pelo especificador de acesso private, define que um método ou atributo de classe nunca deve ser acessível externamente, ocultando completamente tais membros.
Os membros privados usualmente compõem a infraestrutura de uma classe, com atributos e métodos que não precisam ser expostos, mas que contribuem para o funcionamento da classe.

protected
Existe o especificador de acesso protected destinado a indicar membros de uma classe que não devem ser utilizados por meio de suas instâncias, mas apenas na construção de novas classes. Tais membros, ditos protegidos, compõem uma interface de programação da classe, ou seja, um conjunto de atributos e operações destinados exclusivamente aos programadores que usarão tal classe como base para criação de outras, papel que ficará mais claro quando a herança (inheritance) for tratada.

Os especificadores de acesso public, protected e private definem níveis de visibilidade diferentes, ilustrados no quadro seguinte, que considera as situações de implementação (codificação) e instanciação (criação de objetos dentro de programas).


Existe um quarto nível de acessibilidade que é package (ou pacote), que não possui uma palavra reservada para indicá-lo. Por padrão, todos os membros de uma classe têm acesso pacote, exceto quando são aplicados os demais especificadores. Esse foi o acesso definido para todos os membros dos exemplos providos até agora.

Outra observação importante é que todos os membros de uma classe são acessíveis entre si, isto é, os especificadores de acesso só se aplicam externamente à classe.

O encapsulamento, entendido como as restrições aplicadas aos membros de uma classe (ou ocultação de dados -- data hiding), possibilita um controle mais sofisticado para o acesso a tais membros, o que traz várias vantagens [6][7][8]:

  1. o código se torna mais claro, pois ficam evidentes quais membros oferecem funcionalidades reais e quais são auxiliares;
  2. são minimizados os erros de programação, tornando as interfaces mais simples;
  3. classes semelhantes podem exibir uma mesma interface, pois detalhes da implementação permanecem ocultos, facilitando sua utilização;
  4. proporciona facilidades para extensão (criação de novas classes a partir de classes existentes); e
  5. oculta a realização de modificações e alterações, pois quando a interface não é afetada, as mudanças são transparentes para os usuários da classe.
O encapsulamento proporciona a separação do projeto (design) da implementação (coding), facilidades de modificação e padronização do código. Isto implica em maiores chances de extensão, redução do tempo de desenvolvimento e reusabilidade real, ou seja, as maiores vantagens da programação orientada a objetos [8].

Em Java e C# a declaração de classes também emprega os especificadores de acesso public (caso mais comum) e private, mas esse último apenas em situações especiais. Quando uma classe Java é declarada como pública, o arquivo que a contém deve possuir o mesmo nome, para facilitar sua localização.

No exemplo abaixo, a classe denominada Acesso contém quatro campos inteiros com todos os níveis de acesso (público, protegido, privado e pacote); e dois métodos com níveis público e privado. O único construtor é público, situação típica para possibilitar a criação de objetos das classes (embora possam ser declarados com outras visibilidades).

// Arquivo Acesso.java
public class Acesso {
   public int publico;
   protected int protegido;
   private int privado;
   int pacote;

   public Acesso() {
      prepare();
   }

   public String toString() {
      return String.format(
         "publico=%d\nprotegido=%d\nprivado=%d\npacote=%d\n",
         publico, protegido, privado, pacote);
   }

   private void prepare() {
      publico = 7;
      pacote = 5;
      protegido = 3;
      privado = 1;
   }

   public void reset() {
      publico = 0;
      pacote = 0;
      protegido = 0;
      privado++;
   }
}

Observe também que o método privado prepare() é usado pelo construtor público, pois os especificadores de acesso não estabelecem restrições internas, isto é, entre os membros da própria classe. Já o método reset() altera os campos protegido, zerando-o, e privado, incrementando-o.

A classe TestaAcesso possui apenas o método main, que instancia um objeto do tipo Acesso. Com a variável de instância denominada acesso, é acionado o método público toString(), cujo resultado é exibido no console, mostrando o estado inicial do objeto recém-criado. Depois, os campos público e pacote tem seus valores alterado; novamente o estado é exibido; então é acionado o outro método público reset(); e reexibido o estado do objeto, que retorna a situação inicial.

// Arquivo TestaAcesso.java
public class TestaAcesso {
   public static void main(String[] a) {
      Acesso acesso = new Acesso();
      System.out.println(acesso.toString());
      acesso.publico = 1000;
      acesso.pacote = 777;
      System.out.println(acesso.toString());
      acesso.reset();
      System.out.println(acesso.toString());
   }
}

Apenas os campos públicos podem ser acessados pelas instâncias da classe. A indicação de campos privados ou protegidos será apontada como erro durante a compilação, impedindo a geração e execução de código inválido.

O programa TestaAcesso, como em outros fragmentos visto até aqui, mostra como usar um objeto de outra classe.

Na próxima lição veremos como utilizar os especificadores de acesso para prover o encapsulamento efetivo de elementos de uma classe.

POO-F-07-Construtores POO-F-09-Getters e Setters

Referências Bibliográficas

[1] JAMSA, K.; KLANDER, L.. Programando em C/C++: a bíblia. São Paulo: Makron Books, 1999.
[2] PAGE_JONES, M.. Fundamentos do Desenho Orientado a Objeto com UML. São Paulo: Makron Books, 2001.
[3] SOMMERVILLE, I.. Software Engineering. 6th. Ed. Harlow: Pearson, 2001.
[4] DEITEL, H.M.; DEITEL, P.J.. Java: como programar. 6a. Ed. São Paulo: Pearson Prentice-Hall, 2005.
[5] SAVITCH, W.. C++ Absoluto. São Paulo: Pearson Addison-Wesley, 2004.
[6] JANDL JR., P. Introdução ao C++. São Paulo: Futura, 2003.
[7] JANDL JR., P.. Java - guia do programador. 3a. ed. São Paulo: Novatec, 2015.
[8] RUMBAUGH, J.; BLAHA, M.; PREMERLANI, W.; EDDY, F.; LORENSEN, W.. Object-oriented modeling and design. Englewoods Cliffs: Prentice-Hall, 1991.
[9] STROUSTRUP, B.. The C++ Programming Language. 3rd Ed. Reading: Addison-Wesley, 1997.
[10] LANGSAM, Y.; AUGENSTEIN, M. J.; TENENBAUM, A. M.. Data structures using C and C++. 2nd Ed. Upper Saddle River: Prentice-Hall, 1996.
[11] WATSON, K.; NAGEL, C.; PEDERSEN, J.H.; REID, J.D.; SKINNER, M.; WHITE, E.. Beginning Microsft Visual C# 2008. Indianapolis: Wiley Publishing, 2008.

domingo, 28 de janeiro de 2018

POO::Fundamentos-07-Construtores

POO-F-06-Sobrecarga POO-F-08-Encapsulamento
A existência de qualquer tipo de objeto, tanto no mundo real como na POO, exige que ele seja previamente criado, possibilitando que seja usado. As classes são novos tipos de dados definidos pelo programador para descrever entidades reais ou abstratas; que servem como modelo para criação dos objetos do seu tipo.

Cada objeto é uma ocorrência de uma classe, ou seja, é uma entidade distinta das demais, capaz de armazenar dados e realizar algumas ações, tal como descrito pelo seu tipo, isto é, por sua classe. Assim, vimos que o processo de criação de objetos implica em aspectos fundamentais da OO [6][7]:
  • Todo objeto é de um tipo específico/classe específica.
  • Os objetos da mesma classe, ou seja, do mesmo tipo, possuem as mesmas características (atributos) e comportamento (operações).
  • Todo objeto criado é distinto dos demais, independente de seus tipos.
  • Todo objeto tem um ciclo de vida.
  • Todo objeto é único e pode ser distinguido dos demais.
Por conta disso, a criação de objetos é uma operação essencial, denominada instanciação, muito importante na OO, que envolve um operador próprio (new nas linguagens de programação Java e C#) e também uma operação especial responsável pela inicialização dos objetos criados, chamada de construtor [4][7][11].

Um construtor é como um método ou função-membro, mas cuja responsabilidade é prover a preparação dos objetos de sua própria classe, pois os atributos (variáveis-membro) devem ser inicializados de modo adequado para que os objetos sejam consistentes e possam ser usados adequadamente. 

Embora os construtores sejam como métodos destinados à inicialização dos objetos da classe, são diferenciados das demais operações por três características [6]:
  • possuem, sempre, o exato mesmo nome da classe;
  • não declaram um valor de retorno, nem mesmo void, pois "retornam" um novo objeto de seu tipo; e
  • só podem ser acionados por meio do operador de instanciação new, ou seja, seu uso fica restrito à criação de novos objetos.
De maneira geral, a instanciação de objetos usa a sintaxe abaixo:

<Classe> objeto = new <construtor>([lista_argumentos]);

Aqui:
  • Classe indica o nome da classe que determina o tipo do objeto;
  • objeto é a variável que "contém" o objeto;
  • new é o operador especial de instanciação;
  • construtor é a operação especial da classe que prepara os novos objetos do tipo; e
  • lista_argumentos opcionalmente fornece dados para a criação do novo objeto.
A figura que segue ilustra o uso típico de construtores para uma classe Student.


Os construtores retornam uma referência para o objeto criado (que pode ser algo como seu endereço na memória ou um identificador próprio do sistema OO usado) que deve ser armazenada numa variável do tipo do objeto. Após a instanciação, o objeto criado fica disponível para utilização por meio da variável de referência, o que possibilita acessar seus atributos ou acionar seus métodos:

variavelObjeto.atributo = <expressaoTipoAtributo>;
variavalObjeto.método([listaParametros]);

Os atributos e métodos também podem ser utilizados em expressões compatíveis com o tipo do atributo ou do valor retornado pelo método.

Observe a classe Circulo, cujo código é equivalente em Java ou C#:

class Circulo {
   double raio;

   Circulo() { 
      raio = 1;
   }
   double area() {
      return 3.1415926*raio.raio;
   }
}

O tipo Circulo possui todos os elementos possíveis de uma classe [5][6][7][11]:
  • um atributo raio, que descreve uma característica dos objetos tipo círculo;
  • uma operação area(), funcionalidade que provê o cálculo da área do círculo baseada no atributo da classe;
  • um construtor Circulo(), que tem o mesmo nome da classe e inicializa novos objetos deste tipo com o valor padrão 1 para o raio.
Agora objetos do tipo Circulo podem ser criados e utilizados.

// Java
Circulo circulo = new Circulo();
System.out.println("Raio = " + circulo.raio);
System.out.println("Area = " + circulo.area());

//C#
Circulo circulo = new Circulo();
circulo.raio = 2.5;
Consule.WriteLine("Area = " + circulo.area());

Quando declaramos uma classe e não especificamos um construtor, o compilador automaticamente gera um construtor denominado default, que não realiza qualquer ação, apenas permitindo que novos objetos sejam criados. Se o programador fornece qualquer construtor, o compilador não incluir o construtor default.

Embora os atributos (campos) da classe sejam automaticamente inicializados (variáveis numéricas recebem zero, boleanas recebem false e de tipo objeto recebem null), é adequado que o programador, explicitamente, acrescente o construtor default (sem parâmetros) tornando clara a inicialização desejada para novos objetos da classe.

Tal como métodos, os construtores podem possuir parâmetros, permitindo que a criação dos objetos seja customizada. Assim como para os métodos, a sobrecarga (overload) também pode ser usada com construtores, é bastante conveniente em casos como este, para estabelecer formas diferentes para a criação de objetos.

Com uso da sobrecarga de construtores (constructor overload), a classe Circulo pode ser modificada para possuir tanto o construtor default (sem parâmetros), como um construtor parametrizado com o tamanho do círculo criado.

class Circulo {
   double raio;

   Circulo() { 
      raio = 1;
   }
   Circulo(double r) { 
      raio = r;
   }
   double area() {
      return 3.1415926*raio.raio;
   }
}

Com a presença de construtor parametrizado, a criação de novos objetos do tipo Circulo passa a exigir a indicação de um argumento que especifica o valor do raio do círculo criado, customizando a criação de objetos. Com isso temos:

Circulo outroCirculo = new Circulo(2.5);

A existência de construtores parametrizados torna mais flexível a criação de objetos, cujos campos podem ser iniciados com valores diversos adequados a cada programa. Somente com o construtor default, temos duas operações para criar um círculo customizado:

Circulo outroCirculo = new Circulo();
outroCirculo.raio = 2.5;

É muito importante observar que se forem fornecidos um ou mais construtores parametrizados, o compilador não gera o construtor default nesta situação. Quando qualquer construtor é fornecido pelo programador, o compilador não faz a adição automática do construtor default, de modo que, se desejado, o construtor default deve ser manualmente introduzido.

Sem o construtor default, torna-se obrigatório que os argumentos exigidos pelos construtores parametrizados sejam fornecidos. Isto é como afirmar que novos objetos não podem ser criados sem tais dados, o que é consistente com muitas situações da realidade: criar uma conta corrente exige um argumento com o CPF de seu titular; matricular um aluno numa disciplina requer um argumento com seu RA; fazer algum cadastro na web precisa de um endereço de e-mail; e assim por diante.

O uso de construtores permite, então, definir como novos objetos de uma classe serão construídos, possibilitando exigir dados na ocasião de sua instanciação, permitindo tanto sua customização, quanto economizando código no preparo de novas instâncias.

POO-F-06-Sobrecarga POO-F-08-Encapsulamento

Referências Bibliográficas

[1] JAMSA, K.; KLANDER, L.. Programando em C/C++: a bíblia. São Paulo: Makron Books, 1999.
[2] PAGE_JONES, M.. Fundamentos do Desenho Orientado a Objeto com UML. São Paulo: Makron Books, 2001.
[3] SOMMERVILLE, I.. Software Engineering. 6th. Ed. Harlow: Pearson, 2001.
[4] DEITEL, H.M.; DEITEL, P.J.. Java: como programar. 6a. Ed. São Paulo: Pearson Prentice-Hall, 2005.
[5] SAVITCH, W.. C++ Absoluto. São Paulo: Pearson Addison-Wesley, 2004.
[6] JANDL JR., P. Introdução ao C++. São Paulo: Futura, 2003.
[7] JANDL JR., P.. Java - guia do programador. 3a. ed. São Paulo: Novatec, 2015.
[8] RUMBAUGH, J.; BLAHA, M.; PREMERLANI, W.; EDDY, F.; LORENSEN, W.. Object-oriented modeling and design. Englewoods Cliffs: Prentice-Hall, 1991.
[9] STROUSTRUP, B.. The C++ Programming Language. 3rd Ed. Reading: Addison-Wesley, 1997.
[10] LANGSAM, Y.; AUGENSTEIN, M. J.; TENENBAUM, A. M.. Data structures using C and C++. 2nd Ed. Upper Saddle River: Prentice-Hall, 1996.
[11] WATSON, K.; NAGEL, C.; PEDERSEN, J.H.; REID, J.D.; SKINNER, M.; WHITE, E.. Beginning Microsoft Visual C# 2008. Indianapolis: Wiley Publishing, 2008.