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:
- 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
- implementando métodos especialmente destinados a manipulação destes campos, como meio de garantir que assumam apenas valores consistentes.
- 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.
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.
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).
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.
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é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.