[Pesquisar este blog]

quinta-feira, 1 de março de 2018

POO::Plena-16-Associações-composição de objetos

POO-P-15-Associações POO-P-17-Associações:agregação
Um sistema OO contém vários objetos inter-relacionados, ou seja, instâncias de tipos diferentes associadas de uma maneira apropriada para produzir os resultados desejados. Assim, as diversas associações existentes entre os objetos de um sistema explicitam as relações lógicas ou de negócios que existem entre seus objetos e, exatamente por isso, devem ser adequadamente representadas.

As associações ou vínculos podem existir entre objetos diferentes, de um mesmo tipo ou de tipos diferentes [2]. Estes ligações constituem os relacionamentos específicos entre tais objetos, o que é muito importante, pois em geral efetivam um serviço ou materializam uma construção.

Por exemplo, uma biblioteca possui livros e usuários, que são entidades de tipos distintos. O empréstimo de um livro, que é o serviço prestado pela biblioteca, vincula um livro específico a um usuário particular, ou seja, cria uma associação livro-usuário.

De maneira semelhante, a construção de um avião corresponde a ligação de vários elementos, como fuselagem, asas, trens de pouso, motor, etc. Também existe uma associação necessária entre tais elementos para constituir uma aeronave.

No post anterior, que tratou das associações, foram vistas as situações gerais onde um objeto está associado a um ou mais objetos. A quantidade de objetos associados é dita cardinalidade ou multiplicidade da associação. Além disso, a maneira com que as associações são construídas permite a que navegação seja unidirecional ou bidirecional.

Também existem associações especiais, conhecidas como associações todo/parte, que relacionam as partes de um todo. Em função do tipo de relação existente entre o conjunto de todos os elementos e suas partes, existem dois tipos particulares de associações todo/parte que merecem um estudo mais detalhado porque ocorrem com muita frequência no desenvolvimento de sistemas OO. São as composições, assunto tratado a seguir; e as agregações, que serão abordadas no próximo post.

Composições

Numa composição (composite), o objeto composto não pode existir como um todo sem que todos os seus componentes estejam presentes, pois ele não estaria íntegro ou completo [2][6]. Considere, rapidamente, uma caneta esferográfica sem sua carga de tinta; uma bicicleta sem guidão; ou um telefone celular sem sua bateria; são exemplos de objetos incompletos que não podem ser utilizados, nem desempenhar seus papeis. Completando tais conjuntos, temos composições válidas.

Existe também outra questão envolvendo o tempo de vida do objeto composto (o todo) e dos objetos componentes (suas partes):
  • O tempo de vida de um objeto composto não pode ultrapassar o tempo de vida de seus componentes.
  • No entanto, o tempo de um, vários ou todos os objetos componentes (as partes) pode ser maior que a vida do objeto composto.
Considerando uma bicicleta, como um todo - completa, ela não pode existir sem uma ou duas de suas rodas, sem seu selim ou guidão. Mas as partes da bicicleta podem existir, mesmo que não montadas como uma bicicleta (ou seja, existem mesmo sem constituir um todo).

Além disso, a qualquer tempo, um objeto componente só pode fazer parte um objeto composto específico. O guidão, o selim e cada parte da bicicleta só podem estar em uma bicicleta em particular. Retirar a roda de uma bicicleta, faz que ela deixe de ser um todo; montando tal roda para completar outra bicicleta, surge outro objeto composto; mas a roda em questão é um objeto único, que só pode estar numa bicicleta ou nenhuma.

Também é muito comum que as composições sejam, geralmente, heterômeras ou heteromorfas, ou seja, objetos compostos cujas partes não são semelhantes, isto é, feitos de partes de tipos diferentes.

É comum que as composições sejam usadas para expressar o interrelacionamento sistêmico entre elementos que devem coexistir para funcionarem adequadamente como um sistema.

Num diagrama de classes UML adicionamos um diamante preenchido nas associações todo/parte do tipo composição, posicionando-o do lado do objeto composto. Por conta da própria heteromorfia, as multiplicidades de cada componente podem ser diferentes, como ilustrado a seguir.


As associações numa composição também podem ser navegáveis. Mas a escolha do desenho da navegabilidade entre o objeto composto e seus objetos componentes deve levar em conta:
  • O quão frequentemente a composição precisa ser navegada de parte a parte.
  • Se os objetos componentes serão reutilizados em outras composições (situação onde se recomenda que os componentes não façam referência ao objeto composto).
Outro aspecto bastante comum é que as mensagens enviadas ao objeto composto se propaguem para todos os seus componentes.

Exemplo de Composição

Considere as classes Java/C# que seguem, nas quais não foram incluídos quaisquer membros, exceto toString(), para simplificação do exemplo. Tais classes são os componentes de uma bicicleta.

public class Roda { 
   public String toString() {
      return getClass().getName();
   }
}
public class Quadro { 
   public String toString() {
      return getClass().getName();
   }
}
public class Selim { 
   public String toString() {
      return getClass().getName();
   }
}
public class Guidao { 
   public String toString() {
      return getClass().getName();
   }
}

A implementação da operação toString() de todos os tipos componentes da bicicleta é idêntica e produz uma String contendo o nome da classe.

Como uma bicicleta é um objeto composto, o qual só pode estar completo com a presença de todos os seus componentes, a classe Java/C# que segue representa uma possível implementação de Bicicleta.

public class Bicicleta {
   private Roda rodaDianteira, rodaTraseira;
   private Quadro quadro;
   private Selim selim;
   private Guidao guidao;

   public Bicicleta(Roda rd, Roda rt, Quadro q, Selim s, Guidao g) {
      if (rd == null || rt == null || rd == rt) {
         throw new RuntimeException("Bicicleta requer duas rodas.");
      }
      if (q == null) {
         throw new RuntimeException("Bicicleta requer um quadro.");
      }
      if (s == null) {
         throw new RuntimeException("Bicicleta requer um selim.");
      }
      if (g == null) {
         throw new RuntimeException("Bicicleta requer um guidao.");
      }
      rodaDianteira = rd; rodaTraseira = rt;
      quadro = q;
      selim = s;
      guidao = g;
   }

   public Roda getRodaDianteira() { return rodaDianteira; }
   public Roda getRodaTraseira() { return rodaTraseira; }
   public Quadro getQuadro() { return quadro; }
   public Selim getSelim() { return selim; }
   public Guidao getGuidao() { return guidao; }

   public String toString() {
      return
            String.format("%s:\n\t%s,\n\t%s,\n\t%s,\n\t%s,\n\t%s\n",
            getClass().getName(), rodaDianteira, rodaTraseira,
            quadro, selim, guidao);
   }  
}

Os membros privados rodaDianteira, rodaTraseira, quadro, selim e guidao são as associações requeridas pelo tipo composto Bicicleta. São declaradas privadas para garantir que não sejam substituídas por null, o que tornaria a composição inválida com a ausência de qualquer um de seus componentes.

O construtor toma como argumentos os componentes exigidos para a composição, validando e rejeitando componentes nulos. Apenas quando fornecidas duas instâncias distintas de Roda, uma instância de Quadro, de Selim e de Guidao, torna-se possível a instanciação de um objeto Bicicleta, como no fragmento que segue:
Bicicleta b = new Bicicleta(new Roda(), new Roda(),
      new Quadro(), new Selim(), new Guidao());
System.out.println(b);

Quando executado, este fragmento produz:
Bicicleta:
        Roda,
        Roda
        Quadro
        Selim
        Guidao

As operações de acesso getter permitem acessar e, portanto, navegar, unidirecionalmente, da instância de Bicicleta para as suas instâncias componentes.

O método toString() de Bicicleta produz uma String que concatena o nome da classe do tipo Bicicleta e, também, os nomes das classes de seus componentes, cujos métodos toString() são implicitamente acionados quando suas referências são usadas para suprir um argumento de tipo %s, que indica um tipo String. Este método mostra uma possível forma de propagação de mensagens entre o objeto composto e seus componentes.

Considerações Finais

As composições representam associações todo/parte, onde todas as partes (componentes), de tipos iguais ou diferentes, devem existir na quantidade adequada para que o todo possa ser considerado completo.

Composições são adequadas para expressar o inter-relacionamento sistêmico entre elementos, como mostrado no exemplo do tipo Bicicleta (composição) e seus componentes Roda, Quadro, Selim e Guidao.

É praticamente impossível construir sistemas onde não existam associações, sendo que as composições são frequentemente utilizadas.


POO-P-15-Associações POO-P-17-Associações:agregação

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.

segunda-feira, 26 de fevereiro de 2018

POO::Plena-15-Associações

POO-P-14-Objetos que usam objetos POO-P-16-Associações:composições
No mundo real, os muitos tipos de objetos que existem aparecem tanto de maneira isolada, como elementos independentes, como associados a outros objetos. As técnicas de construção de sistemas por meio da Programação Orientada a Objetos (POO) contemplam maneiras para representar as diferentes associações que podem existir entre os objetos.

Associações

Uma associação representa um vínculo (ou ligação) possível entre instâncias de classes, ou seja, entre objetos que podem ser tanto de um mesmo tipo, como de tipos diferentes [2].

O vínculo existente entre instâncias representa um relacionamento de natureza específica, como:
  • Um aluno de um curso e as disciplinas ou módulos que cursou;
  • Um usuário de uma biblioteca e um livro por ele emprestado;
  • Uma propriedade imóvel e seu proprietário;
  • Um sócio ou um membro de um clube ou agremiação;
  • As rodas de um veículo ou partes de uma máquina.
Assim, as associações podem e, de fato, devem ser documentadas em qualquer projeto, pois representam os relacionamentos requeridos entre as entidades do seu modelo (de negócio, de dados, etc).

A Unified Modeling Language (UML) é uma linguagem gráfica que possui vários diagramas para representar as visões diferentes de um projeto de software [2][8]. O diagrama de classes UML, em particular, possibilita a representação gráfica de classes, seus atributos e suas operações; possuindo também todos os elementos necessários para expressar as associações existentes entre elas, como ilustrado na figura que segue.


O diagrama exibido contém duas classes, Pessoa e Veiculo, representados por retângulos com três divisões: uma para a identificação da classe (seu nome e, opcionalmente, seu pacote/namespace); uma para os atributos da classe; e outra para as operações da classes. Os atributos e operações são precedidos por um símbolo que indica sua visibilidade: '+' para elementos públicos, '#' para elementos protegidos e '-' para elementos privados; sendo que a ausência de símbolo denota visibilidade de nível pacote.

Também podem existir ligações entre classes, como mostra a figura abaixo, que destaca a ligação entre a classe Pessoa e a classe Veiculo


A associação (ligação) entre as classes Pessoa e Veiculo é denominada propriedade. Cada extremidade também é identificada, o que permite estabelecer como se dá o vínculo de propriedade especificamente para cada tipo (classe) envolvida:
  • Pessoa tem frota de 0..* (zero ou mais) veículos; e
  • Veículo tem 1 (um) proprietário.
A indicação numérica existente em cada extremo de uma associação é a cardinalidade ou multiplicidade da relação, que pode ser:
  • 0..1 -- zero ou um;
  • 0..* -- zero ou mais (qualquer número);
  • 1 -- (apenas) um;
  • 1..* -- um ou mais do que um; e
  • * -- qualquer número.
Assim, tal diagrama pode ser lido como:
  • Pessoas e veículos têm um vínculo denominado propriedade. Uma pessoa possui uma frota (de veículos), com zero até muitos veículos. Um veículo possui apenas um proprietário.

Importância das associações

Modelar as associações entre classes constitui a espinha dorsal da análise técnica que se denomina modelagem de informações. Assim, os diagramas de classes que contêm associações permitem obter informações sobre várias perspectivas da modelagem:
  • Uma associação ou relacionamento é nomeada por uma forma verbal: "propriedade" pode ser também "possuído por".
  • A multiplicidade de relações que é possível, incluindo a ausência de relação (0) quando permitida.
  • Os nomes das associações e dos papéis não são obrigatórios na UML, mas mostram-se muito úteis na etapa de implementação e na interpretação dos diagramas.

Associação básica ou binária

O diagrama que segue mostra associação básica ou binária entre (classes) Pessoa e Cachorro, a qual é denominada PosseDeCachorro:
  • Um Cachorro tem 0..1 (zero ou um) dono (Pessoa).
  • Uma Pessoa possui 0..* (qualquer número) de Cachorros. 

Associação representada por Classe

Uma associação entre classes, tal como a básica entre Pessoa e Cachorro, pode também ser representada por uma classe (dita de associação). Desta maneira, a relação PosseDeCachorro, entre Pessoa e Cachorro, poderia ser expressa assim.


Navegabilidade

É um conceito da UML reservado inteiramente para modelos de implementação orientados a objeto.

A navegabilidade é indicada por setas acrescentadas nas linhas que representam as associações. Ela mostra quando um objeto pode se referir ao outro associado, o que permite acessá-lo, ou seja, navegar até tal objeto. Com isso é possível executar operações relacionadas à associação em si.

A navegação pode ser unidirecional e bidirecional, assim entre duas classes existem três possibilidades de navegação, como mostra a figura que segue, onde se vê a navegação unidirecional de Pessoa para Cachorro, a navegação unidirecional de Cachorro para Pessoa, e também bidirecional entre Pessoa e Cachorro.



Associação, Navegabilidade e Código OO

Cada forma de associação e sua possível multiplicidade tem uma expressão própria em termos de código.

Para ilustrarmos como o código OO pode ser construído para representar as associações de navegação uni e bidirecional, com diferentes multiplicidades, considere a existência das classes A e B, como segue:
public class A {
   // atributos, operações e construtores
   :
}

public class B {
   // atributos, operações e construtores
   :
}

Neste momento, não é necessário conhecermos e considerarmos os eventuais atributos, operações e construtores pertencentes a estas classes.

Navegação unidirecional

Se for desejada uma associação denominada papel, unidirecional, de cardinalidade 0..1 (zero ou um), com navegação de A para B, devemos ter:
public class A {
   // associação de A para B, 0..1
   public B papel;
   // atributos, operações e construtores
   :
}

public class B {
   // atributos, operações e construtores
   :
}

Este código possibilita que:
  • Quando o atributo papel de uma instância da classe A é nulo, temos a situação de nenhum objeto do tipo B está associado ao primeiro (multiplicidade = 0).
  • Quando o atributo papel de uma instância da classe A contém uma referência válida, temos a situação de um objeto do tipo B associado ao primeiro (multiplicidade = 1).
  • Quando a instância de A contém um referência válida, é possível navegar do objeto  de tipo A para o objeto de tipo B. O contrário não é possível, assim temos uma navegação unidirecional.
Então, temos que este código permite que cada instância de A possa, livremente, ser associada a zero ou uma instância de B, permitindo a navegação de A para B apenas (é a classe que possui um campo para representar a associação que possibilita suas instâncias de acessar a - navegar para - instância associada).

Para inverter o sentido da navegação, bastaria colocar o atributo de navegação na outra classe, ou seja, para uma associação denominada papel, unidirecional, de cardinalidade 0..1 (zero ou um), com navegação de B para A, devemos ter:

public class A {
   // atributos, operações e construtores
   :
}

public class B {
   // associação de B para A, 0..1
   public A papel;
   // atributos, operações e construtores
   :
}

As características desta associação são as mesmas, apenas com a navegabilidade invertida.

Para navegação bidirecional, o campo de associação deve estar presente nas duas classes, como segue.

public class A {
   // associação de A para B, 0..1
   public B papel;
   // atributos, operações e construtores
   :
}

public class B {
   // associação de B para A, 0..1
   public A papel;
   // atributos, operações e construtores
   :
}

O exemplo acima permite a navegação bidirecional entre A e B, mantendo a multiplicidade 0..1 em cada extremidade da associação.

No entanto, esta estrutura de código não é robusta. Cabe ao programador garantir a consistência entre quaisquer associações construídas!

Considerações finais

Como um sistema OO contém vários objetos interrelacionados, é necessário representar adequadamente as diferentes associações que podem existir entre os objetos, isto é, os vínculos entre instâncias de tipos diferentes, pois tais associações explicitam as relações lógicas ou de negócios que existem entre tais objetos.

Assim é muito importante modelar as associações entre classes, atividade essencial na análise técnica que se denomina modelagem de informações, a qual culmina num projeto orientado a objetos.

As associações, com sua identificação, cardinalidade e possibilidade de navegação expressam o uso típico que os objetos fazem dos demais. Particularmente existem as associações todo/parte, dentre as quais a composição e a agregação merecem estudo mais cuidadoso, a ser visto nos próximos posts!

POO-P-14-Objetos que usam objetos POO-P-16-Associações:composições

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.

quinta-feira, 22 de fevereiro de 2018

POO::Plena-14-Objetos que usam objetos

POO-F-13-Tratamento de exceções POO-P-15-Associações
A Programação Orientada a Objetos (POO) é uma técnica de construção de sistemas onde um conjunto de objetos se inter-relacionam para produzir os resultados desejados. Todo objeto é uma ocorrência de uma classe, combinando dados e operações específicas, de maneira a definir um conjunto particular de responsabilidades.

A construção de sistemas orientados a objetos (OO) é bastante interessante, pois como os objetos têm suas responsabilidades delimitadas, é possível que as classes que os originam sejam utilizados em situações diferentes, ou seja, a OO promove a reusabilidade de código.

Outro aspecto positivo é que a manutenção do código se torna mais simples, pois, dependendo da qualidade do projeto, podem ser feitas modificações apenas nas classes relacionadas a objetos específicos, sem que isso implique em alterações em outras classes, ou seja, nas demais partes do sistema [7].

Divisão de responsabilidades

Existe um importante princípio no projeto de software que é conhecido como coesão (cohesion) ou divisão de responsabilidades (responsability division). Segundo este princípio, um módulo, uma classe ou um componente de software deve ter uma, e apenas uma, responsabilidade.

Tal responsabilidade deve ser claramente definida, de maneira que, num projeto OO, cada classe deva ser escrita, modificada e mantida para apenas um propósito, representando um tipo único de entidade ou de ator.

Quando uma classe possui muitas responsabilidades é usualmente difícil compreender seu papel, o que torna sua manutenção é mais complicada; além disso, será pouco provável seu reuso, devido a combinação peculiar de responsabilidade.

Por outro lado, quando uma classe é bem coesa, isto é, quanto é centrada numa responsabilidade única, somente uma razão específica leva à sua modificação, o que  simplifica e flexibiliza a realização de mudanças no futuro, pois provavelmente tais alterações não criarão impactos em outras entidades do sistema.

Embora possa existir alguma dificuldade em dividir as responsabilidades que surgem num problema, tal trabalho de separar é compensado pelas facilidades em evoluir.

Problema-Exemplo

Uma empresa especializada na instalação de pisos e carpetes precisa, com grande frequência, calcular a área e o perímetro dos espaços onde tais acabamentos serão instalados, pois tais informações possibilitam determinar a lista de materiais necessários, da qual deriva o orçamento e, depois, o pedido e entrega dos itens envolvidos para realização do serviço. Tais espaços, tipicamente salas e quartos, são tipicamente retangulares.

Assim, um programa simples que determina a área de tais espaços é bastante útil para agilizar o cálculo da área do piso a ser instalado e também o perímetro de rodapés. Existem outros detalhes neste serviço, que serão omitidos por serem menos importantes, de maneira que o problema seja reduzido a "como determinar a área e perímetro de uma sala retangular".

O retângulo é uma entidade do problema, o que sugere a existência de uma classe Retangulo. Um programa simples pode ser construído para obter os dados necessários para criar um objeto retângulo e, com este, determinar as informações requeridas de área e perímetro.

Então é necessário modelar uma classe Retangulo, para o cálculo de área e perímetro; e outra para o programa em si, que interage com o usuário. Isto separa as responsabilidades identificadas.

Modelagem da classe Retangulo

Uma sala ou área retangular é um retângulo, formado por quatro lados perpendiculares, onde os lados opostos têm a mesma medida: a medida maior (de dois dos lados opostos) será denominada comprimento e a medida menor (dos outro lados) de largura. Da geometria sabemos que:
  • área = comprimento * largura
  • perímetro = 2 * (comprimento + largura)
A representação mais simples de um retângulo é uma dupla de medidas (comprimento e largura), pois os seus quatro ângulos são sempre 90°. Isto indica que comprimento e largura são atributos (ou campos) desta entidade (da classe Retangulo), cujos valores são números reais (ou de tipo double) e positivos, ou seja, comprimento>0.0 e largura>0.0.

Como tais atributos tem uma restrição (são apenas positivos), mas o tipo double aceita valores positivos e negativos, devem ser privados, evitando seu acesso direto e indiscriminado. Métodos de acesso ou getters podem ser providos para que o valor de comprimento e largura de um retângulo sejam obtidos e conhecidos; assim como métodos de alteração ou setters devem ser inclusos para que seja possível modificar as medidas do retângulo, respeitando as regras dadas.

Como não é possível definir (e portanto existir) um retângulo sem estas duas medidas, o construtor da classe deve exigir dois argumentos reais para suprí-las. Não deve ser incluso um construtor default, pois não parece existir um retângulo de medida padrão.

Métodos adicionais podem calcular a área e o perímetro, sem demandar outros dados, pois apenas as medidas de comprimento e largura são necessárias. Tais métodos podem ser denominados area ou getArea, perimetro ou getPerimetro.

Esta análise resulta numa classe Java que pode ser como segue.
public class Retangulo {
// atributos
   private double comprimento, largura;

   // construtor parametrizado exige dimensões
   public Retangulo (double comprimento, double largura)
         throws RuntimeException {
      setComprimento(comprimento);
      setLargura(largura);
   }

// métodos de ajuste/mutação
   public void setComprimento (double comprimento)
 
         throws RuntimeException {
      // verifica dimensão
      if (comprimento<=0) {
         throw new RuntimeException("Comprimento invalido: "
            + comprimento);
      }
      // dimensão ok
      this.comprimento = comprimento;
   }
   public void setLargura (double largura)
         throws RuntimeException {
      // verifica dimensão
      if (largura<=0) {
         throw new RuntimeException("Largura invalida: " + largura);
      }
      // dimensão ok
      this.largura = largura;
   }

// métodos de acesso/observação
   public double getComprimento () { return comprimento; }
   public double getLargura () { return largura; }

   public double area() { return comprimento*largura; }
   public double perimetro() { return 2*(comprimento+largura); }

// método de acesso "padronizado"
   public String toString() {
      return String.format("Retangulo[C:%.2d x L:%.2d]",
            comprimento, largura);
   }
}

Um objeto do tipo Retangulo pode ser instanciado com:
Retangulo ret1 = new Retangulo(4.20, 2.70);

As medidas podem ser modificadas com os métodos setter:
ret1.setComprimento(4.30);
ret1.setLargura(2.75);

As medidas podem ser obtidas individualmente com os métodos getter:
double c = ret1.getComprimento();
double l = ret1.getLargura();

Uma String que descreve o objeto pode ser obtida com toString():
// uso direto - explícito
String desc = ret1.toString();
// uso indireto - implícito
System.out.println(ret1);

A área e o comprimento podem ser obtidas com:
double area = ret1.area();
double perim = ret1.perimetro();

Caso medidas inválidas sejam fornecidas, na instanciação ou na alteração de uma das medidas do retângulo, será lançada uma exceção do tipo RuntimeException:
// na instanciação
Retangulo ret2 = new Retangulo(-2, 3.5);
|  java.lang.RuntimeException thrown: Comprimento invalido: -2.0
|        at Retangulo.setComprimento (#1:15)
|        at Retangulo.<init> (#1:7)
|        at (#2:1)

// na alteração
ret1.setComprimento(0);
|  java.lang.RuntimeException thrown: Comprimento invalido: 0.0
|        at Retangulo.setComprimento (#1:15)
|        at (#4:1)

Com isso dispomos de uma classe Retangulo, que modela adequadamente as entidades deste tipo e cujas instâncias podem representar convenientemente salas retangulares, provendo os serviços de cálculo da área e do perímetros destas salas.

Modelagem do programa

O programa mais simples que permite resolver o problema de determinar a área e o perímetro de uma sala retangular precisa:
  1. obter as medidas da sala (comprimento e largura), solicitando-as para o usuário;
  2. criar um objeto de tipo Retangulo com as medidas dadas;
  3. obter a área e o perímetro da sala por meio do uso do objeto Retangulo;
  4. exibir os resultados (área e o perímetro da sala).
Uma classe denominada APS (Área e Perímetro da Sala) é um programa Java válido se contiver um método main(String[]), como o que segue, que realiza as ações listadas acima.
import java.util.Scanner;

public class APS {
   public static void main(String[] args) {
      // obtém medidas da sala
      Scanner teclado = new Scanner(System.in);
      System.out.print("Sala--comprimento: ");
      double comprimento = teclado.nextDouble();
      System.out.print("Sala--largura: ");
      double largura = teclado.nextDouble();

      // cria objeto Retangulo
      Retangulo sala = new Retangulo(comprimento, largura);

      // obtém área e perímetro da sala
      double area = sala.area();
      double perimetro = sala.perimetro();

      // exibe resultados
      System.out.println("Sala--area = " + area);     
      System.out.println("Sala--perimetro = " + perimetro);     

      // finalização do programa
      teclado.close();
   }
}

Quando executado, este programa produz resultados como:
>java APS
Sala--comprimento: 4,3
Sala--largura: 2,75
Sala--area = 11.825
Sala--perimetro = 14.1

Considerações finais

Embora simples, esta solução permite o cálculo da área e do perímetros de uma sala retangular. Além disso, devemos observar que:
  • A classe APS realiza a interação com o usuário, ou seja, solicita valores e exibe resultados.
  • A classe APS não tem conhecimento de como os resultados são obtidos (como são calculados), mas utiliza um objeto de outra classe (Retangulo) que é capaz de realizar tais cálculos.
  • A classe APS é como um cliente da classe Retangulo.
  • A classe Retangulo não realiza qualquer interação com usuário (não existe leitura de valores ou exibição de mensagens), possibilitando seu uso por qualquer tipo de programa (de console, GUI ou web).
  • Cada classe tem suas próprias responsabilidades: mudanças na interação com usuário não afetam a classe Retangulo; mudanças na classe Retangulo - desde que não modifiquem sua interface - não afetam seus clientes.
Esta divisão de responsabilidades acaba por criar uma organização dos elementos da aplicação, isto é, das classes componentes deste sistema que é conhecida como organização em camadas, uma arquitetura bastante conhecida e consagrada.

A figura que segue ilustra o modelo de camadas tradicional:


Considerando apenas as camadas não opcionais, temos:
  • Presentation Layer, ou Camada de Apresentação, que é responsável pela interação da aplicação com seus usuários. Esta camada contém a interface da aplicação.
  • Logic Layer, ou Camada de Lógica ou de Regras de Negócio, contém os elementos necessários para a realização das operações e transações requeridas pela aplicação.
  • Persistence Layer, ou Camada de Persistência, tem como responsabilidade prover os serviços de armazenamento e recuperação dos dados da aplicação, envolvendo bancos de dados, sistemas de arquivos e outros elementos presentes em sistemas locais, em rede, centralizados ou em nuvem.
Nosso exemplo é aderente a esta arquitetura, pois:
  • Classe APS constitui a camada de apresentação da aplicação, interagindo com o usuário na obtenção de dados e exibição de resultados.
  • Classe Retangulo faz parte da camada de lógica, pois é responsável pelas operações da aplicação (cálculo da área e do perímetro de salas retangulares).
  • Não existem elementos da camada de persistência, pois nossa aplicação não armazena dados, o que revela uma fragilidade justificável por sua simplicidade.
Um outro aspecto importante deste exemplo é que a utilização de um objeto por parte de um segundo objeto pode indicar uma associação entre tais objetos, ou seja um vínculo ou um relacionamento. Este é o assunto do próximo post!

POO-F-13-Tratamento de exceções POO-P-15-Associações

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.

segunda-feira, 19 de fevereiro de 2018

POO::Fundamentos-13-Tratamento de exceções

POO-F-12-Exceções e seu tratamento POO-P-14-Objetos que usam objetos
As linguagens OO modernas utilizam as exceções para realizar o tratamento de qualquer situação anormal ou erro no código dos programas. As exceções são objetos especiais que sinalizam a ocorrência de problemas que devem ser tratados pelo programador, possibilitando a criação de código mais robusto e também seguro.

Como vimos, no Java, as exceções são objetos da família da classe java.lang.Exception, que tanto sinalizam a ocorrência de uma anormalidade, como também carregam informações adicionais sobre o problema encontrado. Embora a ocorrência das exceções, como o próprio nome indica, não seja algo normal ou desejado, são previsíveis no projeto como o recebimento de argumentos inválidos, entradas de dados inconsistentes, recursos (arquivos ou urls) não encontrados ou indisponíveis, assim como o time-out das operações executadas.

O tratamento de exceções é, portanto, uma necessidade da programação OO.

Fluxo do tratamento de exceções

O uso das exceções envolve um ciclo de tratamento simples: identificação do problema, criação, lançamento e tratamento.

Identificação do problema

Vários tipos de problemas podem ocorrer durante a execução de um problema, como um dado que não é validado ou um recurso não localizado ou indisponível. Se o problema pode ser resolvido localmente, não precisamos lançar mão do mecanismo de exceções.

Criação de exceção

Mas quando o problema detectado não pode ser resolvido no mesmo local (no mesmo método ou contexto onde foi identificado), deve ser criada uma exceção, do tipo adequado, eventualmente com informações adicionais do problema. A criação de uma exceção é uma operação de instanciação, que envolve o operador new e a classe de exceção selecionada [4][7]. As exceções criadas podem ser do tipo monitorado (checked exception) ou não monitorado (unchecked exception).


Lançamento de exceção

De posse de um objeto de exceção de qualquer tipo, é possível lançar tal exceção (to throw an exception), com uso da diretiva throw. Isto provoca a entrega do objeto de exceção lançado por throw no trecho de código do contexto superior (método que invocou a execução do método onde o problema foi encontrado) [4][7].

Tratamento de exceção

Nos locais do código onde podem ser lançadas exceções não monitoradas (unchecked exceptions), o programador pode optar pelo seu tratamento local (no contexto de sua ocorrência), pela indicação explícita de seu lançamento (para o contexto superior), ou mesmo não fazer nada (que é prática ruim).

Aqui vale lembrar que caso uma exceção, de qualquer tipo, alcance o nível da JVM, que corresponde ao contexto mais alto, pois ela acionou o programa, a execução é interrompida imediatamente e sinalizada para o usuário [7].

Quando são lançadas exceções monitoradas (checked exceptions), o programador deve optar ou pelo seu tratamento local, ou pela indicação explícita de seu lançamento para o contexto superior. Assim as exceções monitoradas exigem melhores práticas de programação!

Se a opção for o lançamento explícito, o método onde a exceção ocorre deverá empregar uma cláusula throw para indicar o lançamento da exceção, como visto no post anterior Exceções e seu lançamento.

Se a opção for o tratamento local, será necessário apanhar a exceção (to catch an exception) com uso de diretivas try associadas com cláusulas catch ou finally, como segue.


Tratamento de Exceções

Para tratarmos as exceções usamos a diretiva try, que em sua forma mais simples, emprega cláusulas catch, com a sintaxe abaixo:
try {
   // bloco de código monitorado
:
} catch (Exception e) {
   // bloco de código de tratamento
:
}

Cada par de chaves { e } delimita um bloco de código.

Aquele que segue a diretiva try é considerado o bloco de código monitorado, no qual se localizam as instruções do programa (acionamento de métodos ou outras operações) onde pode ocorrer uma ou mais exceções.

O outro bloco, que segue a cláusula catch, é o bloco de tratamento de exceções, onde as exceções de um tipo específico são apanhadas e tratadas com o código ali presente.

As exceções lançadas no bloco de código não monitorado que não apanhadas pelo bloco de tratamento de exceções seguem o fluxo usual para o contexto superior.

Se não ocorre qualquer exceção no bloco de código monitorado (bloco try), o bloco catch não é executado, ou seja, a execução sem exceções do programa desvia do bloco catch.

Mas quando ocorre uma exceção no bloco try, a execução é imediatamente desviada para o bloco catch que trata o tipo específico da exceção.

Em ambos os casos a execução continua após a diretiva try/catch, exceto quando são executadas operações de retorno, ou ocorre o encerramento do programa, ou ainda novas exceções são lançadas.

Observe o programa Java que segue, onde o método main converte o primeiro argumento recebido, de String para inteiro, exibindo uma contagem simples.
public class Contagem {
   public static void main (String args[]) {
      int max = Integer.parseInt(args[0]);
      for (int i=0; i<max; i++) {
         System.out.print(i + " ");
      }
      System.out.println();
   }
}

Neste programa, se o usuário não fornece um argumento ou fornece um argumento inválido (que não corresponde a um valor inteiro), o programa é interrompido, com a exibição de várias mensagens que podem confundir o usuário.

Por exemplo, execução do programa Contagem sem argumentos:
>java Contagem
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
        at Contagem.main(Contagem.java:3)

Ou, outro exemplo, execução do programa Contagem com um argumento inválido:
>java Contagem Jandl
Exception in thread "main" java.lang.NumberFormatException: For input string: "Jandl"
        at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
        at java.base/java.lang.Integer.parseInt(Integer.java:652)
        at java.base/java.lang.Integer.parseInt(Integer.java:770)
        at Contagem.main(Contagem.java:3)

Neste exemplo, se o usuário não fornece um argumento válido na linha de comando, podem ocorrer dois tipos de exceção na linha:
int max = Integer.parseInt(args[0]);

Se não existe argumento (nada é digitado além do nome da classe), ocorre a exceção: ArrayIndexOutOfBoundsException, que mostra que tentamos usar uma posição do array cujo índice não é válido (no caso o índice 0 que corresponde ao primeiro argumento.

Se o argumento existe mas é inválido (não corresponde a um valor inteiro), ocorre a exceção: NumberFormatException, que indica que a conversão realizada não pode ser concluída devido ao argumento incorreto fornecido (no caso o valor que o usuário digitou).

Para resolver o problema, a linha onde ocorre a exceção deve ser substituída por um trecho que inclua o tratamento de erro. Ou seja, deveríamos substituir:
int max = Integer.parseInt(args[0]);

Pelo trecho dado a seguir:
int max = 0;
try {
   max = Integer.parseInt(args[0]);
} catch (Exception e) {
   // tratamento da exceção
   :
}

Com isso, uma opção melhor para obtermos um programa mais robusto e adequado para o usuário seria como segue:
public class Contagem {
   public static void main (String args[]) {
      int max = 0; // use zero ou outro valor default
   // código que ESTÁ sujeito a exceções
      try {
         max = Integer.parseInt(args[0]);
      } catch (Exception e) {
   // tratamento da exceção
         System.out.println("É necessário um argumento inteiro válido!");
         System.exit(-1); // finalização do programa devido ao erro
      }
   // código que NÃO ESTÁ está sujeito a exceções
      for (int i=0; i<max; i++) {
        System.out.print(i + " ");
      }
      System.out.println();
   }
}

Com este programa, caso não seja fornecido um argumento ou caso seja inválido, será exibida uma mensagem melhor para o usuário, como nos exemplos que seguem:
>java Contagem
É necessário um argumento inteiro válido!

>java Contagem Jandl
É necessário um argumento inteiro válido!

>java Contagem 7
0 1 2 3 4 5 6

Nesta situação, foi escolhido o tratamento de Exception, o tipo mais genérico de exceção, que inclui todas as outras, ou seja, inclui as exceções específicas deste programa que são ArrayIndexOutOfBoundsException e NumberFormatException.

Note que o uso de exceções evita poluir o código que se deseja executar com testes e validações diversas, feitas com diretivas if e switch, além de funções de validação. O bloco de código monitorado contém a sequência de instruções necessárias ao programa, construída para quando tudo está correto. Se ocorrem problemas, a execução é desviada para o bloco de tratamento de erros, que apenas lida com o problema encontrado.

Tratamento seletivo de exceções

Outro aspecto muito positivo da diretiva try é que são permitidas várias cláusulas catch, para exceções diferentes. Assim é possível atrelar dois ou mais blocos de tratamento diferentes para o tratamento de cada exceção possível.

A única regra para isso é primeiro apanhar as exceções mais específicas e depois as mais genéricas, assim tratar a exceção Exception será sempre a última opção, quando necessário.

O programa Contagem agora pode ter um tratamento de exceções bem sofisticado.
public class Contagem {
   public static void main (String args[]) {
      int max = 0; // use zero ou outro valor default
   // código que ESTÁ sujeito a exceções
      try {
         max = Integer.parseInt(args[0]);
   // tratamento de um tipo exceção
      } catch (ArrayIndexOutOfBoundsException e) {
System.out.println(“Faltou argumento.”);
max = 3; // aqui usamos um valor default,
                 // sem finalizar o programa
   // tratamento de outro tipo exceção
      } catch (NumberFormatException e) {
        System.out.println(“Argumento invalido.”);
System.exit(-1); // aqui finalizamos o programa
      }
   // código que NÃO ESTÁ está sujeito a exceções
      for (int i=0; i<max; i++) {
        System.out.print(i + " ");
      }
      System.out.println();
   }
}

Observe que cada exceção possível de ocorrer tem um trecho de código associado, por meio de cláusula catch, que realiza seu tratamento. Apenas um bloco catch é executado, sendo desviados os demais. Outro aspecto positivo é não não necessários testes (com diretivas if ou switch) para determinar o tipo de erro e selecionar seu tratamento.

Se o trecho de código do tratamento torna-se grande, pode ser criado um método de tratamento específico, que separa ainda mais a ocorrência do erro de seu tratamento.

Objetos de exceção apanhados

Deve ser observado que a variável e declarada nos blocos de tratamento de exceção, pode ter qualquer nome determinado pelo programador, como exc, excecao ou outro.

Esta variável permite o acesso e uso do objeto exceção apanhado, o que permite, por exemplo, obter uma String com a mensagem de erro associada ao tipo da exceção:
String msg = e.getMessage();

Tais mensagens podem conter detalhes que podem tanto ser exibidos para o usuário, quando é conveniente e adequado fornecer informações adicionais do problema; como também armazenados em arquivos de registro (logs), para análise posterior.

Também é possível imprimir a rota da exceção no console, isto é, o caminho percorrido pelo programa até a ocorrência da exceção:
e.printStackTrace();

Tal rota também pode ser obtida e processada de outras maneiras, mas isto deixa de ser básico e foge ao escopo desta lição!

Considerações finais

As exceções permitem um tratamento simples do ponto de vista de programação, mas bastante sofisticado quando consideradas as escolhas possíveis entre seu tratamento local e seu envio para contextos superiores. 

Recomenda-se que a ocorrência de erros sempre seja sinalizada com o lançamento de exceções, o que possibilita a separação clara entre o código normal (aquele executado na ausência de erros) e o código de tratamento de erros. Novamente é necessário que tal separação é uma grande vantagem, pois permite, a partir do ponto onde ocorreu exceção, desviar o fluxo de processamento para um contexto superior ou outro ponto mais adequado do programa; mas sem uso de variáveis globais, funções de erro, códigos de erro genéricos e outras técnicas improvisadas.

O tratamento seletivo de erros também possibilita melhor organização do código, o que facilita seu reuso e, principalmente, sua manutenção.


POO-F-12-Exceções e seu tratamento POO-P-14-Objetos que usam objetos

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.