Programação Orientada a Objetos
Programação Orientada a Objetos
Buy on Leanpub

Prefácio

O projeto desse conteúdo iniciou lá em 2016, quando eu lecionava a disciplina de Programação Orientada a Objetos (POO) para os cursos de Análise e Desenvolvimento de Sistemas e Técnico em Informática para Internet. Nas primeiras aulas eu sempre trazia anotações, que eram uma compilação das bibliografias recomendadas, misturadas com experiência de trabalho e envoltas em um contexto prático, já que os cursos de tecnologia e técnicos são voltados para a prática profissional.

Na verdade, o principal desafio era o de achar uma didática pragmática, mas que também oferecesse o alicerce teórico. Eu sabia que os estudantes podiam, no futuro, tanto atuar profissionalmente, como procurar qualificação continuada, como fazer um mestrado, ou realizar concursos. Então essas anotações de aula foram desconstruídas e reconstruídas muitas vezes para serem “ensináveis” e “aprendíveis”.

Sem mais delongas, este livro não estava sendo escrito para ser publicado. Era para ser usado como material didático e em sala de aula. Contudo, acabei o liberando antecipadamente no Leanpub, pensando na utilidade às pessoas que se esforçam para aprender POO sozinhas ou em outros cursos. Se esse conteúdo te for um pouquinho útil, o trabalho já se pagou.

Se quiseres me dar um retorno, saiba que sugestões e críticas são bem-vindas. Meu e-mail é marcio.torres@riogrande.ifrs.edu.br.

Introdução

Eu poderia dizer muita coisa aqui, mas vou ser pragmático e resumir o objetivo deste livro em uma frase: utilizar o paradigma de programação orientada a objetos para construir software de qualidade.

Abordagem

Este livro está sendo escrito a partir de notas de aula usadas na disciplina de Programação Orientada a Objetos, ministrada no curso de Análise e Desenvolvimento de Sistemas oferecido pelo Intituto Federal de Educação, Ciência e Tecnologia do Rio Grande do Sul (IFRS). Essa origem, digamos, dá à esse livro as seguintes propriedades:

  • Dedicado ao ensino: projetado para ser didático assim como as estratégias de ensino em sala aula;
  • Focado na prática: para cada explanação teórica há exemplos de aplicação prática, com exemplos e estudos de caso, pois ele é destinado à educação profissionalizante;
  • No nosso idioma e na nossa cultura: sem cara de tradução e escrito para os Brasileiros (com um leve sotaque gaúcho;)
  • Linguagem acessível: são evitadas expressões rebuscadas e formalidades linguísticas, o que é muito útil para as pessoas não dormirem nas aulas.

O ponto forte deste livro, eu penso, é a não-separação de teoria e prática. Logo, cada conceito ou princípio abordado virá junto com um exemplo “bobo”, um estudo de caso mais elaborado e um exercício. Como exemplo “bobo”, quero dizer uma amostra de implementação pequena e direta que, embora não muito útil como “vida real”, deve ser esclarecedor para o aprendiz. O estudo de caso, por outro lado, é mais elaborado e serve para observar uma aplicação mais séria e plausível. Os exercícios servem para a prática do conhecimento e apropriação da habilidade.

Os exemplos e exercícios são baseados em especificações e assertivas, que permitem validar e verificar se o software está conforme o planejado e com as respostas esperadas, que são premissas básicas de um controle de qualidade de software.

Linguagem de programação

Os códigos estão escritos na linguagem de programação Java, que é a linguagem preferida na grande maioria dos livros de referência para a Programação Orientada a Objetos. Além disso, Java é bastante usada pelas faculdades e escolas técnicas. Aqui mesmo, no IFRS, usamos Java na disciplina de POO do curso superior (usamos JavaScript na disciplina de POO do curso técnico).

No futuro pretendo lançar esse livro com outras linguagens de programação. Inclusive, é objetivo desse livro não se “apegar” a UMA linguagem de programação, sempre tentando abordar os conceitos, princípios e implementações de forma mais genérica possível, focando sempre no paradigma e não na linguagem de programação. Dito isso, este livro não é uma referência da linguagem Java. Em vez disso, é uma referência para entender e usar programação orientada a objetos!

Fechando, a maioria dos códigos presentes no livro são implementáveis em qualquer linguagem de programação que ofereça um suporte razoável à POO, com poucos ajustes. Entenda que a POO oferece certas funcionalidades que as linguagens podem oferecer na totalidade, parcialmente ou até mesmo não oferecer.

Todos os códigos estão disponíveis na on-line em um repositório no github.com neste endereço: https://github.com/marciojrtorres/poocomhonra. Fique à vontade para baixá-los, adaptá-los, testá-los, forkar o repositório, bem, faça como quiser, mi código es su código. (mas os use por sua conta e risco, hehe :).

Organização dos capítulos

O livro está organizado para cobrir tópicos de POO gradualmente, presumindo que o estudante já conheça o fundamental de programação, como detalhes de sintaxe, declaração, desvios condionais, laços, etc, como se já estivesse passado por uma disciplina de lógica ou introdução à programação ou computação. Na lista a seguir há uma ideia do que é visto em cada capítulo, na forma de quais questões deve responder.

  • Capítulo 0 – Programação Modular: quais são os fundamentos de modularização de código?
  • Capítulo 1 – História da Programação Orientada a Objetos: como surgiu a POO?
  • Capítulo 2 – Modelo de Objetos: como é um objeto? como descrevê-los e usá-los?
  • Capítulo 3 – Estado: como é o ciclo-de-vida de um objeto? como definir o conjunto de valores que representam?
  • Capítulo 4 – Comportamento: como são as operações realizadas pelos objetos? como defini-las?
  • Capítulo 5 – Polimorfismo ad-hoc: como uma mesma operação pode ser executada com diferentes entradas?
  • Capítulo 6 – Encapsulamento: como um objeto esconde sua complexidade e protege suas informações vitais?
  • Capítulo 7 – Identidade: como um objeto é diferente de outro objeto?
  • Capítulo 8 – Representação: como um objeto pode ser representado alternativamente? quais formatos são mais utilizados?
  • Capítulo 9 – Imutabilidade: como saber se um objeto deve ser constante ou variável?
  • Capítulo 10 – Coesão: como saber se as informações e operações de um objeto devem ficar juntas?
  • Capítulo 11 – Associação: como os objetos trabalham uns com os outros?
  • Capítulo 12 – Delegação: como os objetos delegam operações para outros objetos?
  • Capítulo 13 – Agregação: como os objetos se unem para resolver um problema maior?
  • Capítulo 14 – Modelagem: como descrever a colaboração desses objetos antes de escrever o código?
  • Capítulo 15 – Responsabilidades: como saber o que cada objeto deve fazer?
  • Capítulo 16 – Direcionalidade: como saber a direção das mensagens?
  • Capítulo 17 – Multiplicidade: como saber quantos objetos conectar a outro objeto?
  • Capítulo 18 – Composição: como saber se um objeto deve ser composto de “objetinhos”?
  • Capítulo 19 – Acoplamento: como as relações de dependência entre objetos afetam o projeto?
  • Capítulo 20 – Interface: como um objeto é usado por outros?
  • Capítulo 21 – Generalização e Especialização (Herança): como um objeto pode compatilhar parte de sua lógica e informação?
  • Capítulo 22 – Polimorfismo por Subtipagem: como os objetos específicos podem redefinir o comportamento?
  • Capítulo 23 – Abstração: quão genérico devem ser os objetos genéricos?
  • Capítulo 24 – Polimorfismo Paramétrico: e se eu não souber o tipo específico? como deixar o tipo em aberto?
  • Capítulo 25 – Considerações Finais: sabendo de onde eu vim, para onde eu vou?

Para quem é este livro

É importante sempre esclarecer que este é um livro voltado à educação profissional, baseado na minha experiência profissional e no ensino de programação para iniciantes. Ele é útil para estudantes que já passaram pela disciplina inicial de programação em seus cursos.

Contudo, essa obra também pode atender programadores formados, entusiastas e autodidatas de programação que desejam compreender melhor (e melhor aplicar) o paradigma orientado a objetos.

Profissionalmente, este livro pode ser usado para descobrir melhores estratégias para a modularização de sistemas.

Academicamente, este livro é adequado ao ensino em cursos voltados para a Educação Profissional, Técnica e Tecnológica, tais como: Técnico em Informática, Técnico em Informática para Internet, Tecnologia em Análise e Desenvolvimento de Sistemas, Tecnologia em Sistemas para Internet, além de cursos de formação inicial e continuada na área de desenvolvimento de sistemas. As disciplinas de POO em cursos de bacharelado podem tirar proveito desta obra se objetivarem um viés prático. Para ajudar, saiba que ele está sendo usado no IFRS para a disciplina de Programação Orientada a Objetos, cuja ementa está a seguir:

Conceitos e princípios de programação orientada a objetos: abstração, classes, instâncias, estado e comportamento, atributos e métodos, comandos e consultas, coesão, encapsulamento, associação, agregação, composição, delegação, dependência e acoplamento, herança e polimorfismo. Modelagem, implementação e testes. Noções de princípios e padrões de projeto.

Para quem não é este livro

Este livro não cobre construções básicas de programas e não tem o objetivo de ensinar a programar “do zero”. Se não sabes programar, se não passaste por uma disciplina de lógica de programação ou algoritmos, então ele pode te parecer muito avançado e, ao final, não te ser muito útil.

Não espere deste livro uma abordagem completamente teórica. Ele foi escrito por um praticante e para praticantes.

Não espere, também, uma cobertura abrangente da linguagem de programação, neste caso Java. O objetivo é estudar os conceitos do paradigma orientado a objetos. A linguagem é usada apenas para “materializar” esses conceitos.

Se tu já sabes POO e procuras um livro que aprofunde esse conhecimento, também pode não ser uma boa escolha. Para estes casos, uma referência sobre Projeto Orientado a Objetos te seria mais útil, como um livro de Padrões de Projeto ou de Arquitetura de Sistemas.

Convenções

A seguir algumas convenções a respeito da abordagem, tipografia e layout.

Acrônimos

Algumas palavras e nomes aparecem abreviados, usando siglas ou acrônimos. Na primeira vez que forem exibidos constará o nome completo e no restante do livro (salvo exceções) é usado o acrônimo. Por exemplo, Programação Orientada a Objetos será abreviada como “POO”, assim como Orientação a Objetos como “OO”.

Inglês

Este livro tem a proposta de proporcionar material técnico no nosso idioma. Entretanto, na área de desenvolvimento de softwares, o idioma inglês é predominante – frequentemente ele é inevitável. Por este motivo, termos e nomes amplamente conhecidos em inglês terão uma explicação em português, mas também serão apresentados na sua forma original (em inglês). É importante te habituares, pois mesmo que pareça estranho alguém dizer “dropa a tabela”, é o modo como as pessoas falam no ambiente de trabalho. É muito imporante conhecer esse “dialeto”, por assim dizer, sobretudo em um livro que te prepara para o mundo do trabalho.

Códigos

Trechos de código são exibidos usando fonte de largura fixa. Em meio ao texto eles aparecem como “… usamos o método isFuture(Date):boolean para …”. As assinaturas de métodos são apresentadas como Classe.metodo(TipoArgumento):TipoRetorno, semelhante à notação UML.

Trechos mais extensos de código são apresentados em blocos com título, linhas numeradas (quando for necessário) e link para obter o fonte (quando estiver disponível). Por exemplo:

Exemplo de um Código de Exemplo
 1 // Os códigos aparecerão em fonte de largura fixa
 2 // e com comentaŕios como este.
 3 class UmaClasse {
 4   String umAtributo;
 5   String umMetodo(int umParametro) {
 6     int umRetorno = umParametro * 2;
 7     return this.umAtributo + umRetorno;
 8   }
 9   // ...
10 }

Os comentários seguidos por reticências // ... significam código omitido, pois às vezes o código inteiro é muito longo para ser colocado na listagem. Nestes casos visite o link para acessá-lo na íntegra.

Dicas, avisos, observações e milongas

O livro está cheio de quadros. A maioria está relacionado com questões da programação orientada a objetos e quando ela está sendo empregada corretamente ou não.

Sobre o Autor

Atualmente sou Professor no IFRS e atuo nos cursos de Analise e Desenvolvimento de Sistemas e Técnico em Informática para Internet.

Os alunos engraçadinho perguntam: “professor, tu trabalha ou só dá aula?” (sic)

É a vida de quem ensina. Eu trabalho em sala de aula hoje, mas ainda participo de projetos internos e dou uns pitacos em sistemas alheios (atividade conhecida como consultoria :).

Atrás disso, tenho uma história longa, passei por vários marcos na linha do tempo da evolução da computação. Para me conheceres melhor vou contar um pouco dessa história no devaneio a seguir:

“Eu nasci a dez mil anos atrás”. Comecei programando em Basic, num CP500 da Prológica. Sem Internet, se aprendia lendo revistas técnicas, transcrevendo códigos e fazendo experiências. Mais tarde comecei a desenvolver aplicações comerciais com dBase e então Clipper, ambos sobre a plataforma MS-DOS. Joguei Prince of Persia, Wolfenstein e o primeiro DOOM - tinha que usar o DOS/4GW para liberar a memória estendida. Já montei meu próprio computador – quando se selecionava IRQ por jumpers/switches. Vivenciei a ascensão da interface gráfica - não aguentava mais ver caracteres em fósforo verde. Instalei o Windows 95 - malditos 13 disquetes. Tive um Kit Multimídia da Creative – e uma placa de vídeo Voodoo. Migrei meus sistemas de Clipper para Visual Basic e mais tarde Delphi. Usei a Internet quando ainda só existia HTML com “meia dúzia” de tags – nada de CSS ou JS. Acompanhei a ascensão da Internet e da Web. Presenciei o início do Linux, sua evolução e importância para a consolidação dos sistemas on-line – junto com Perl, Apache, MySQL, PHP, etc. Já instalei o Conectiva Linux, compilei o Kernel e aprendi a usar uma linha de comando de verdade. Comecei a programar em Java a partir da versão 1.3 – ainda sem enums, generics, autoboxing etc – e foi meu primero contato com Orientação a Objetos – velhos hábitos procedimentais são difíceis de perder. Observei a Googlificação – mas usei o Cadê e o AltaVista. Acompanhei o crescimento do Comércio Eletrônico - e também o estouro da bolha da Internet.

Tenho dúvidas se sou um programador que ensina ou um professor que programa. Gosto de pensar que sou ambos.

1. Programação Modular

O mundo é muito grande, mãe. (jovem Clark Kent)

Então faça-o ficar pequeno.

– Martha Kent

Existem muitas definições para o que é Programação Modular, mas vamos lidar com a que está disponível no wiki do Cunningham & Cunningham:

Programação Modular é o ato de projetar e escrever programas como interações entre funções onde cada uma realiza uma única e clara funcionalidade, e que tem o mínimo de efeitos colaterais entre elas.

1.1 Modularização

A modularização dos códigos é um dos principais recursos para construir softwares com alto nível de qualidade. Em poucos palavras, a meta é simples: extrair uma lógica repetida (código redundante) e compartilhar através de partes reaproveitáveis (módulos).

Nos primórdios da programação a modularização era realizada através de Rotinas e mais tarde por Procedimentos, bem antes de então chegarmos no paradigma Orientado a Objetos (OO). Materializando isso, geralmente os módulos são separados em diversos arquivos, que contém as rotinas/procedimentos/funções/classes/métodos. Entenda que, antes de tudo, é perfeitamente possível escrever um programa inteiro em um único arquivo e até em uma única listagem “corrida” de código (sem separações). Entenda, também, que a granularidade pode ser variável, isto é, o mesmo programa pode ser separado em dezenas ou centenas de arquivos, cada um com uma, duas ou até dúzias de rotinas/procedimentos/funções/classes/métodos.

Embora modularizar traga um trabalho adicional de projetar (pensar) na separação, ainda assim os benefícios são incontestáveis, pagando todo o esforço inicial extra, valendo o trade-off1. Entre os benefícios alguns mais importantes são:

  • Usando módulos há menos código em cada arquivo, resultando em estruturas lógicas mais simples e compreensíveis;
  • As lógicas são reutilizáveis, evitando a rescrita do mesmo código várias vezes;
  • Os membros da equipe podem trabalhar em módulos diferentes;
  • Existe uma maior facilidade para identificar e corrigir erros, dado o isolamento, quando eles estão contidos em módulos;
  • O mesmo módulo pode ser reaproveitado em vários softwares;
  • Cada módulo pode ser testado separadamente.

Os benefícios citados são, entre outros, os principais responsáveis por uma gestão básica da qualidade do software. No tópico a seguir vem o primeiro exemplo de como construir partes reusáveis de códigos através de procedimentos (funções).

1.2 Modularização na Programação Procedimental

A separação da lógica repetida/reusável em procedimentos é a base da programação procedimental. A implementação desses procedimentos depende muito da linguagem de programação utilizada, por exemplo, são usadas as keywords Procedure em Pascal, Sub e Function em Visual Basic. A seguir um procedimento em Pascal para (exemplo “bobo”) mostrar a soma de dois números:

Procedure Soma (Var Valor_1, Valor_2 : Integer);
Var Soma : Integer;
Begin
  Soma := Valor_1 + Valor_2;
  Writeln ('Soma = ', Soma);
End;
// https://gist.github.com/marciojrtorres/3065c164af0b89eb033a108b1936ed3a

Nas linguagens modernas os procedimentos são implementados como funções (ou métodos). Existem diversas sintaxes2 para declarar uma função, dependendo da linguagem de programação, por exemplo: def em Python e Ruby, func em Google GO, fun em Kotlin e, mais óbvio, function em JavaScript e PHP.

Na linguagem Java a definição de funções/procedimentos é feita com métodos estáticos (não é a única, em C# também por exemplo). A presença de métodos estáticos em códigos escritos na linguagem Java caracteriza funcionalidades Procedurais em meio à Orientação a Objetos.

Os procedimentos são projetados para executar uma lógica, recebendo ou não uma entrada e devolvendo ou não um valor, isto é, tanto a entrada de dados como a saída de dados é opcional. Para exemplificar, considere um programa simples, que lê dois números e imprime o MMC. A seguir uma implementação inicial que não faz uso de procedimentos:

Calculando um MMC sem um procedimento
 1 import java.util.Scanner; // scanner é um módulo!
 2 public class Proc1 {
 3   public static void main(String[] args) {
 4     // ler dois números
 5     Scanner scan = new Scanner(System.in);
 6     int numero1 = scan.nextInt();
 7     int numero2 = scan.nextInt();
 8     // descobrir o maior número
 9     int maior = numero1 > numero2 ? numero1 : numero2;
10     int mmc = maior;
11     // somar o maior enquanto não for divisível por ambos
12     while (mmc % numero1 != 0 || mmc % numero2 != 0) {
13       mmc += maior;
14     }
15     // imprimir mmc
16     System.out.println(mmc);
17   }
18 }

A implementação sem separar a lógica em um procedimento incorre na duplicação do código em operações subsequentes, digamos que o cálculo do MMC seja necessário em outras partes do programa, o código entre as linhas 8 e 16, pelo menos, teriam que ser reintroduzidos. Considere o mesmo exemplo com duas operações:

Calculando dois MMC’s sem um procedimento
 1 import java.util.Scanner; // scanner é um módulo!
 2 public class Proc2 {
 3   public static void main(String[] args) {
 4     // ler dois números
 5     Scanner scan = new Scanner(System.in);
 6     int numero1 = scan.nextInt();
 7     int numero2 = scan.nextInt();
 8     // descobrir o maior número
 9     int maior = numero1 > numero2 ? numero1 : numero2;
10     int mmc = maior;
11     // somar o maior enquanto não for divisível
12     while (mmc % numero1 != 0 || mmc % numero2 != 0) {
13       mmc += maior;
14     }
15     // imprimir mmc
16     System.out.println(mmc);
17 
18     // ler mais dois números
19     int numero3 = scan.nextInt();
20     int numero4 = scan.nextInt();
21     // descobrir o maior número
22     maior = numero3 > numero4 ? numero3 : numero4;
23     mmc = maior;
24     // somar o maior enquanto não for divisível
25     while (mmc % numero3 != 0 || mmc % numero4 != 0) {
26       mmc += maior;
27     }
28     // imprimir mmc
29     System.out.println(mmc);
30   }
31 }

A extração do procedimento deve ser identificada através dos dados de entrada e lógica necessária, observando o que há de comum nos dois cálculos subsequentes: leitura de dois números, descoberta do maior, loop e print. Somente a entrada é diferente, levando a escrever o seguinte procedimento entre as linhas 7 e 18 do código a seguir:

Um procedimento para calcular o MMC
 1 import java.util.Scanner; // scanner é um módulo!
 2 public class Proc3 {
 3   // static = método estático (representa uma função/procedimento em Java)
 4   // void = sem retorno (return)
 5   // mmc = nome da função
 6   // int n1, int n2 = dados de entrada necessários
 7   static void mmc(int n1, int n2) {
 8     // seja qual for a variável elas entram como n1 e n2
 9     // descobrir o maior número
10     int maior = n1 > n2 ? n1 : n2;
11     int mmc = maior;
12     // somar o maior enquanto não for divisível
13     while (mmc % n1 != 0 || mmc % n1 != 0) {
14       mmc += maior;
15     }
16     // imprimir mmc
17     System.out.println(mmc);
18   }
19 
20   public static void main(String[] args) {
21     // ler dois números
22     Scanner scan = new Scanner(System.in);
23     int numero1 = scan.nextInt();
24     int numero2 = scan.nextInt();
25     // chamada/invocação da função/procedimento
26     mmc(numero1, numero2);
27     // ler mais dois números
28     int numero3 = scan.nextInt();
29     int numero4 = scan.nextInt();
30     // chamada/invocação da função/procedimento
31     mmc(numero3, numero4);
32   }
33 }

Na linha 7 está a assinatura do método, que neste exemplo representa um procedimento. A assinatura, em poucas palavras, descreve o nome do procedimento, entrada e retorno, podendo ser descrita como Proc3.mmc(int, int):void, que significa um procedimento com o nome de mmc, pertencente ao módulo Proc3 (em Java um módulo é uma classe) que recebe dois parâmetros inteiros int, int e não possui retorno void (void pode ser traduzido livremente como vazio).

O procedimento anterior funciona bem, tu mesmo podes fazer os testes. Mas, como se testa? A maneira trivial de testar é chamar o procedimento com MMCs conhecidos e observar se a reposta é esperada, por exemplo, digitando 3 e 5 o resultado deve ser, inevitavelmente, 15. Realizar testes manuais, entrando com os valores e observando as saidas, mesmo com valores triviais, também é a testagem mais simplificada possível.

Testagem

Sem ir muito ao fundo do assunto, a testagem adequada é realizada com a introdução de um conjunto de entradas variadas, válidas e inválidas, as quais tenham saídas previsíveis (esperadas). A previsibilidade é o ponto-chave da testabilidade e para isso os procedimentos devem fornecer alguma saída, seja a resposta correta ou um tratamento de erro. O procedimento anterior não oferece uma saída, não tem retorno (void), o println faz parte da lógica do procedimento. Os dois problemas dessa abordagem são:

  • a estratégia de impressão do resultado é “fixa”,
  • o procedimento não oferece um retorno testável.

Por esses motivos o procedimento será alterado para oferecer um retorno, conforme exemplo a seguir:

Um procedimento com retorno
 1 import java.util.Scanner;
 2 
 3 public class Proc4 {
 4  // esse int antes do nome signigica um retorno inteiro
 5   static int mmc(int n1, int n2) {
 6     int maior = n1 > n2 ? n1 : n2;
 7     int mmc = maior;
 8     while (mmc % n1 != 0 || mmc % n2 != 0) {
 9       mmc += maior;
10     }
11     // aqui trocamos o print pelo return
12     return mmc;
13   }
14 
15   public static void main(String[] args) {
16     Scanner scan = new Scanner(System.in);
17     int numero1 = scan.nextInt();
18     int numero2 = scan.nextInt();
19     // agora a chamada traz um retorno
20     // que pode ser atribuído e impresso
21     int resultado = mmc(numero1, numero2);
22     System.out.println(resultado);
23     // ou então ser feito diretamente:
24     System.out.println(mmc(scan.nextInt(), scan.nextInt()));
25   }
26 }

Na linha 5, o int antes do nome do procedimento declara um retorno. Isto quer dizer que o procedimento devolve um inteiro que pode ser usado para qualquer fim, como ser impresso na linha 22. É importante notar as mudanças nas responsabilidades: antes a responsabilidade do println era do procedimento e agora é da seção principal main.

O próximo passo, agora, é escrever os testes. Preparar a testagem não é complicado, exige apenas duas medidas:

  • eliminar a entrada do usuário e introduzir constantes literais (ex.: int numero1 = 3),
  • escrever assertivas simples (expressões booleanas) para declarar o resultado esperado.

Um true impresso significa que o teste passou e um false (ou exceção) significa que o teste falhou (ou uma situação inesperada foi encontrada, um bug). Confira a testagem do procedimento mmc no código a seguir:

Testando o procedimento com constantes literais e assertivas
 1 import java.util.Scanner;
 2 public class Proc5 {
 3 
 4   static int mmc(int n1, int n2) {
 5     int maior = n1 > n2 ? n1 : n2;
 6     int mmc = maior;
 7     while (mmc % n1 != 0 || mmc % n2 != 0) {
 8       mmc += maior;
 9     }
10     return mmc;
11   }
12 
13   public static void main(String[] args) {
14     int numero1 = 3;
15     int numero2 = 5;
16     int resultado = mmc(numero1, numero2);
17     // esperado que o mmc entre 3 e 5 seja 15
18     // deve imprimir `true`
19     System.out.println(resultado == 15);
20     // fazendo testes diretamente:
21     System.out.println(mmc(5, 6) == 30);
22     // testando no limite:
23     System.out.println(mmc(14223, 77323) == 1099765029);
24   }
25 }

As expressões booleanas nas linhas 19, 21 e 23 representam três testes: dois óbvios, que podem ser obtidos de cálculo mental, e um extrapolado (no limite, o mais importante!). Testar os limites é importante porque é “nas bordas” que falhas e bugs são revelados e, grave isso na memória: o objetivo de testar é provar que o programa não funciona. Neste exemplo, na verdade, foram realizados poucos testes, além de testar situações limites é importante adicionar mais testes afim de encontrar falhas e corrigí-las, até todos os testes passarem. Para minha felicidade (ou não, lembre :P) todos os testes passaram:

javac Proc5.java; java Proc5

true
true
true

Especificação

Procedimentos e, posteriormente, objetos são projetados e implementados a partir de especificações. As especificações devem ser corretas, precisas, claras e válidas! Sem uma especificação válida é impossível assegurar que as respostas são corretas ou mesmo provar que não são. Então, vamos obter a especificação do MMC:

Em aritmética e em teoria dos números o mínimo múltiplo comum (mmc) de dois inteiros a e b é o menor inteiro positivo que é múltiplo simultaneamente de a e de b. Se não existir tal inteiro positivo, por exemplo, se a =0 ou b = 0, então mmc(a, b) é zero por definição.

– Wikipédia em https://pt.wikipedia.org/wiki/M%C3%ADnimo_m%C3%BAltiplo_comum

Partindo dessa especificação, podemos assegurar que o procedimento implementado está em conformidade com ela? Vamos tentar provar que não, explorando as situações excepcionais e escrevendo os testes a seguir contra a especificação:

Testando contra a especificação
    // menor inteiro positivo que é múltiplo
    // simultaneamente de a e de b
    System.out.println(mmc(5, -6) == 30);
    System.out.println(mmc(-5, 6) == 30);
    System.out.println(mmc(-5, -6) == 30);
    // se a = 0 ou b = 0, então mmc(a, b)
    // é zero por definição
    System.out.println(mmc(0, 10) == 0);
    System.out.println(mmc(0, -10) == 0);
    System.out.println(mmc(10, 0) == 0);
    System.out.println(mmc(-10, 0) == 0);
    System.out.println(mmc(0, 0) == 0);

Não trate como decepcionante, mas como esclarecedor. Dos três primeiros testes, com números negativos, o primeiro e terceiro falham. A seção de testes com zero causa um erro logo no primeiro Exception in thread "main" java.lang.ArithmeticException: / by zero. Obter false indica uma falha e quer dizer que não está conforme a especificação pois retorna uma resposta incorreta (as duas falhas retornaram 5 e -5 respectivamente). Uma exceção, ou “quebra” do programa, indica um erro. Resumindo, o procedimento não cumpre a especificação, isto foi provado (e provar que não funciona é, acredite, bom!).

1.3 Modularização na Programação Orientada a Objetos

A POO se difere da programação procedimental quando combina os dados e o algoritmo em uma única unidade (módulo). Na POO a lógica é acessada a partir dos dados. Ainda temos o livro inteiro para discutir isso, mas apenas para ilustrar considere um exemplo “bobo”: calcular o dobro de um número inteiro. Na programação procedimental basta criar um procedimento dobro(int):int, ele recebe um inteiro e devolve outro, não é complicado, veja a seguir:

Procedimento para obter o dobro de um número
public class Proc7 {

  static int dobro(int n) {
    return n + n; // ou n * 2
  }

  public static void main(String[] args) {
    System.out.println(dobro(0) == 0);
    System.out.println(dobro(5) == 10);
    System.out.println(dobro(90) == 180);
  }
}

No procedimento o número é um parâmetro do procedimento. Na POO não há procedimento, há uma classe e por consequência um objeto que retém a informação (o número) e a lógica (o que seria um “procedimento” ou, no dialeto OO, o método!). A seguir a versão OO do dobro de um número:

Objeto para obter o dobro de um número
class Numero {

  int n;

  Numero(int n) {
    this.n = n;
  }

  int dobro() {
    return this.n + this.n; // ou this.n * 2
  }

  public static void main(String[] args) {
    Numero n1 = new Numero(5);
    System.out.println(n1.dobro() == 10);
    System.out.println(new Numero(90).dobro() == 180);
    Numero zero = new Numero(0);
    System.out.println(zero.dobro() == 0);
  }
}

Se nunca viste POO antes, o código anterior pode parecer intimidador, confuso e até exagerado, numa avaliação mais crítica, pois POO geralmente exige mais código que o Procedimental para declarar as estruturas básicas. A POO oferece uma nova visão e interpretação (por isso paradigma) de como implementar uma especificação. Direto e em poucas palavras, no código anterior a noção de Número foi implementada com uma classe class Numero, da qual foram obtidas instâncias através de um construtor new Numero(5), que inicializa um atributo int n, até ser invocado o método dobro(), que então retorna o dobro do valor inicializado. O MMC de dois números também pode ser portado para POO, no mesmo sentido: cria-se uma classe que represente números, instancia e inicializa o objeto com 2 números e então invoca o método mmc(). Não será explicado agora, mas considere um desafio, se conseguires realizá-lo então estás a meio caminho andado.

1.4 Subprocedimentos

Procedimentos isolados não servem para construir um programa completo, normalmente os procedimentos (e vale o mesmo depois para os objetos) são compostos para resolver determinados problemas, buscando o reuso de algoritmos comuns e separação de responsabilidades em procedimentos menores.

Considere de volta o MMC e agora também o MDC. O MMC pode ser calculado a partir do MDC com a seguinte expressão mmc(a, b) = a / mdc(a, b) * b. No final podemos fazer o MMC composto pelo subprocedimento mdc(int,int):int, que abre outra oportunidade para um subprocedimento de seleção do menor número menor(int,int):int. A seguir o código que demonstra essa utilização de subprocedimentos:

Procedimentos que “chamam” outros procedimentos
public class Proc8 {

  static int menor(int a, int b) {
    System.out.println("calculando o menor");
    int menor = a < b ? a : b;
    System.out.println("menor calculado");
    return menor;
  }

  static int mdc(int a, int b) {
    System.out.println("calculando o mdc");
    int mdc = menor(a, b);
    while (a % mdc != 0 || b % mdc !=0) {
      mdc = mdc - 1;
    }
    System.out.println("mdc calculado");
    return mdc;
  }

  static int mmc(int a, int b) {
    System.out.println("calculando o mmc");
    int mmc = a / mdc(a, b) * b;
    System.out.println("mmc calculado");
    return mmc;
  }

  public static void main(String[] args) {
    // testando o mmc
    System.out.println(mmc(5, 6) == 30);
  }
}
Resultado da execução
java Proc8

calculando o mmc
calculando o mdc
calculando o menor
menor calculado
mdc calculado
mmc calculado
true

Existe uma consideração importante para tratar: as dependências. O procedimento mmc depende da resposta do procedimento mdc. Essa relação de dependência é para o bem e para o mal, quero dizer, se o procedimento mdc falha ou “quebra” então o procedimento mmc também falha ou “quebra”. Como mdc depende de menor, se menor falha então mdc falha e mmc também falhará, devido a dependência indireta.

Mesmo com a consideração da dependência existe pontos extremamente positivos em separar procedimentos: subprocedimentos são menores e podem ser testados separadamente. Na prática, testar e garantir o bom funcionamento da função menor assegura a qualidade da função mdc e todas as outras que dependem de menor. O mesmo vale em garantir mdc para respaldar mmc.

Outra forma de combinar procedimentos é usar a saída (o retorno) de um procedimento como entrada de outro. Por exemplo, considerando que existem os procedimentos raizQuadrada(double):double, raizCubica(double):double e menor(double, double):double, qual seria a resposta da seguinte instrução menor(raizQuadrada(225), raizCubica(216))? Assim como um expressão matemática, primeiro resolve-se os parenteses internos e depois os externos (a resposta não é difícil de obter).

Para compreender melhor a composição de procedimentos vamos para um exemplo completo e complexo: o cálculo do seno de um ângulo. Considere uma função seno(double):double onde a entrada é em radianos, contudo o ângulo que temos é em graus (por exemplo 75°). Então para calcular o seno antes temos de converter o ângulo em radianos com uma função radianos(doule):double. A seguir como ficaria a composição:

double graus = 75.0;
System.out.println(seno(radianos(graus)));

A implementação de radianos(double):double é simples, calculada como PI * graus / 180.0. A implementação de seno(double):double não é tão simples e depende, claramente, de subprocedimentos; o seno pode ser calculado pela Série de Maclaurin (não se preocupe, esse não é um livro de matemática :) simplificada como:

Série de Maclaurin
Série de Maclaurin

Observando o algoritmo na imagem percebe-se a necessidade de potência e fatorial. Enfim, o procedimento seno(double):double depende de potencia(double, double):double e fatorial(double):double. É uma opção criar esses subprocedimentos ou codificar tudo em seno, mas há questões a ponderar, como o reaproveitamento dos subprocedimentos, o aumento da testabilidade, contra o aumento da granularidade dos módulos.

Para estudo eu implementei os procedimentos e subprocedimentos, enfim, todas as funções estão no código que vem a seguir. Não é a melhor implementação possível, há um problema de imprecisão, da própria implementação como da essência do ponto flutuante, mas é boa suficiente para estudo.

Combinando procedimentos
public class Proc9 {

  static double potencia(double base, int expoente) {
    double potencia = base;
    for (int i = 1; i < expoente; i++) {
      potencia *= base;
    }
    return potencia;
  }

  static int fatorial(int numero) {
    int fatorial = 1;
    for (int i = 1; i <= numero; i++) {
      fatorial = fatorial * i;
    }
    return fatorial;
  }

  static double seno(double radianos) {
    double seno = 0;
    seno = radianos - (potencia(radianos, 3) / fatorial(3))
                    + (potencia(radianos, 5) / fatorial(5))
                    - (potencia(radianos, 7) / fatorial(7))
                    + (potencia(radianos, 9) / fatorial(9));
    return seno;
  }

  static double radianos(double graus) {
    double PI = 3.1416;
    double radianos;
    radianos = PI * graus / 180.0;
    return radianos;
  }

  public static void main(String[] args) {
    // System.out.println(fatorial(5));
    // combinando os procedimentos
    System.out.println(seno(radianos(75)));
                    //(seno(1.309))
                    //(0.9659270976271132)
    // é uma resposta aproximada
    // a esperada de 0.96592583
    // além disso, double sempre trabalha
    // com aproximações (ver problemas de precisão)
  }
}

1.5 Procedimentos e Dados Estruturados

Ao contrário dos exemplos anteriores, os procedimentos trabalham mais com informações elaboradas do que com dados isolados. Na prática, quer dizer que os procedimentos (e depois objetos e seus métodos) raramente processarão uma variável numérica ou textual simples, mas sim um agrupamento de variáveis. Por exemplo, um cálculo de ICMS seria feito com a Nota Fiscal inteira e não somente com o valor final. Por isso, há a necessidade de agrupar dados para definir uma informação autocontida.

As linguagens de programação disponibilizam estruturas para conter dados relacionados. A estrutura de dados mais popular é o array (arranjo, em português), que também é conhecida como vetor (ou matrix quando é bidimensional). Arrays estão disponíveis em todas linguagens de programação populares, com poucas diferenças de sintaxe, permitindo guardar valores e recuperá-los através de um índice numérico. Existem, portanto, diversas estruturas de dados, como list (lista), set (conjunto), graph (grafo), tuple (tupla), struct (conhecida como registro), associative array (vetor associativo), class (classe, fundamental para a POO e vista no restante do livro), etc.

Para os exemplos deste capítulo vamos usar vetores como estrutura de dados. Se tu ainda não conheces, vetor é uma estrutura e um tipo, que pode ser declarado e atribuído a variáveis, permitindo armazenar valores conforme um índice numérico. Para entender melhor, considere o exemplo:

Trabalhando com vetores básicos
 1 // declarando um vetor de 6 inteiros
 2 int[] copas = new int[6];
 3 copas[0] = 1994;
 4 copas[1] = 1998;
 5 copas[2] = 2002;
 6 // copas[3] = 2006;
 7 copas[4] = 2010;
 8 copas[5] = 2014;
 9 // comprimento (length) do vetor
10 System.out.println(copas.length); // 6
11 // primeiro elemento
12 System.out.println(copas[0]); // 1994
13 // terceiro elemento
14 System.out.println(copas[2]); // 2002
15 // se não atribuído (ver linha 6 comentada) vale zero (valor default)
16 System.out.println(copas[3]); // 0
17 // último elemento sempre length - 1
18 System.out.println(copas[copas.length - 1]); // 2014
19 // a linha a seguir, se descomentada,
20 // causará uma exceção IndexOutOfBoundException
21 // System.out.println(copas[6]); // fora dos limites
22 // percorrendo e imprimindo os valores
23 for (int i = 0; i < copas.length; i++) {
24   System.out.println(copas[i]);
25 }

A linha 2 apresenta a declaração de um vetor. Neste exemplo foi usado um vetor de inteiros de tamanho 6, mas poderia ser um vetor de números reais (double[] valores) ou outros tipos de dados. Três propriedades devem ficar bem claras:

  • vetores têm um comprimento,
  • vetores têm um índice,
  • o maior índice sempre é menor que o comprimento (na verdade é o comprimento - 1).

Essas propriedades foram exemplificadas nas linhas 18 e 23.

Para exemplificar a necessidade e uso de estruturas de dados, considere um programa “bobo” para ler duas datas e imprimi-las. Datas, em si, são informações compostas por três dados: dia, mês e ano. Em vez de codificar os dados em três variáveis, codifica-se como um vetor. No primeiro exemplo, como ficaria usando apenas variáveis isoladas:

Dados desestruturadas, informação fragmentada
import java.util.Scanner; // scanner é um módulo!

public class Estrutura1 {

  public static void main(String[] args) {
    Scanner scan = new Scanner(System.in);
    int diaInicio = scan.nextInt();
    int mesInicio = scan.nextInt();
    int anoInicio = scan.nextInt();

    int diaFim = scan.nextInt();
    int mesFim = scan.nextInt();
    int anoFim = scan.nextInt();

    System.out.println(diaInicio + "/"
                     + mesInicio + "/"
                     + anoInicio + " a "
                     + diaFim + "/"
                     + mesFim + "/"
                     + anoFim);
  }
}

O exemplo anterior pode ser refatorado para usar vetores na representação das datas, como pode ser visto no código a seguir:

Dados estruturados, informação contida
import java.util.Scanner;

public class Estrutura2 {

  public static void main(String[] args) {
    Scanner scan = new Scanner(System.in);
    // dois vetores de três posições
    int[] dataInicio = new int[3];
    int[] dataFim = new int[3];
    // zero based indexes
    dataInicio[0] = scan.nextInt();
    dataInicio[1] = scan.nextInt();
    dataInicio[2] = scan.nextInt();

    dataFim[0]    = scan.nextInt();
    dataFim[1]    = scan.nextInt();
    dataFim[2]    = scan.nextInt();

    System.out.println(dataInicio[0] + "/"
                     + dataInicio[1] + "/"
                     + dataInicio[2] + " a "
                     + dataFim[0]    + "/"
                     + dataFim[1]    + "/"
                     + dataFim[2]);
  }
}

Conter os dados em vetores (e depois em classes/objetos) facilita o trânsito das informações, permitindo que os dados andem juntos. Para demonstrar vamos evoluir o exemplo anterior, extraindo a apresentação da data “d/m/a” para um procedimento formataData(int[]):String como pode ser visto no código a seguir:

Estruturas de dados passando por funções
import java.util.Scanner;

public class Estrutura3 {

  static String formataData(int[] data) {
    return data[0] + "/"
         + data[1] + "/"
         + data[2];
  }

  public static void main(String[] args) {
    int[] dataInicio = new int[3];
    int[] dataFim = new int[3];

    dataInicio[0] = scan.nextInt();
    dataInicio[1] = scan.nextInt();
    dataInicio[2] = scan.nextInt();

    dataFim[0]    = scan.nextInt();
    dataFim[1]    = scan.nextInt();
    dataFim[2]    = scan.nextInt();

    System.out.println(formataData(dataInicio) + " a "
                     + formataData(dataFim));
  }
}

Se os dados não fossem contidos a função teria a assinatura formataData(int,int,int):String, recebendo os três int’s (dia, mês e ano) e devolvendo a String com a data formatada. O procedimento parece bom suficiente, mas falta um detalhe importante que é indispensável: os testes.

Testando estruturas e procedimento
 1 public class Estrutura4 {
 2 
 3   static String formataData(int[] data) {
 4     return data[0] + "/"
 5          + data[1] + "/"
 6          + data[2];
 7   }
 8 
 9   public static void main(String[] args) {
10     int[] dataInicio = { 1, 7, 2016};
11     int[] dataFim    = {31, 7, 2016};
12 
13     System.out.println(
14       formataData(dataInicio).equals("1/7/2016"));
15     System.out.println(
16       formataData(dataFim).equals("31/7/2016"));
17     System.out.println(
18       formataData(new int[]{10,10,2010}).equals("10/10/2010"));
19   }
20 }

Este último código de exemplo, além de testar o nosso procedimento “bobo” formataData, apresenta detalhes básicos importantes para lidar com vetores. Nas linhas 10 e 11 os vetores são declarados e inicializados com os valores inteiros literais. Na linha 18 o vetor é criado e inicializado ao mesmo tempo que é passado como argumento para o procedimento.

Agora vamos a um exemplo mais elaborado. Além disso, para avançar um pouco mais na programação modular, vamos separar o corpo principal do programa (o main) da unidade com os procedimentos: Data.formataData e Data.somaDias(int[], int):int[] dentro de um módulo Data (class Data em Java).

Neste estudo de caso, vamos projetar um procedimento para adicionar dias a uma data. O procedimento recebe um vetor com a data e a quantidade de dias em inteiro e então devolve outro vetor com o resultado, segundo essa assinatura: somaDias(int[], int):int[]. Não é uma lógica simples ou trivial, trabalhar com datas é sempre desafiador, pois as regras não têm um padrão simples: alguns meses têm 30 dias, outros têm 31, um tem 28 ou 29 nos anos bissextos. Entretanto, o programador pode usar a estratégia de reduzir o problema em parte menores e mais simples de serem resolvidas. Essa estratégia é conhecida como Computational Thinking (Pensamento Computacional) e consiste em analisar uma instância do problema, reconhecer um padrão, decompor o problema, analisar solucionar e solucionar as partes até projetar uma solução genérica para o problema inteiro. Começando com um esboço:

// arquivo Data.java
public class Data {
  static int[] somaDias(int[] data, int dias) {
    int[] resultado = new int[3];
    // aqui está o problema!!!
    return resultado;
  }
  static String formataData(int[] data) {
    return data[0] + "/"
         + data[1] + "/"
         + data[2];
  }
}
// arquivo DataMain.java
public class DataMain {
  public static void main(String[] args) {
    int[] dataInicio = { 1, 7, 2016};
    int[] dataFim    = {31, 7, 2016};

    System.out.println(
      Data.formataData(dataInicio).equals("1/7/2016"));
    System.out.println(
      Data.formataData(dataFim).equals("31/7/2016"));
    System.out.println(
      Data.formataData(new int[]{10,10,2010}).equals("10/10/2010"));

    int[] umaData   = {1, 1, 2016};
    int[] resultado = Data.somaDias(umaData, 40);
    System.out.println(resultado[0] == 10);
    System.out.println(resultado[1] == 2);
    System.out.println(resultado[2] == 2016);
  }
}

Esse código pode confundir um pouco, pois antes de implementar todo o procedimento somaDias já se tem o corpo principal do programa e um teste. Antecipar testes é uma ótima estratégia para pensar como devem ser projetados e que respostas devem dar os procedimentos ou objetos4. Projetar um código longo e que cumpra tantas regras pode levar um tempo, então, em vez disso, podemos começar decompondo o problema em um subproblema mais simples e que traz um resultado mais rápido. Considere um procedimento para descobrir o próximo dia de uma data: Data.amanha(int[]):int[] - que é uma simplificação de soma dias para soma um dia. Considere os seguintes casos de teste:

Testes para o procedimento amanha
int[] dataInicio = { 1, 7, 2016};
int[] dataFim    = {31, 7, 2016};
// cumprindo primeiro o amanhã (soma um dia)
int[] dataResult = Data.amanha(dataInicio);
System.out.println(dataResult[0] == 2);
System.out.println(dataResult[1] == 7);
System.out.println(dataResult[2] == 2016);
dataResult = Data.amanha(dataFim);
System.out.println(dataResult[0] == 1);
System.out.println(dataResult[1] == 8);
System.out.println(dataResult[2] == 2016);

Fazer a funcionalidade amanha é percorrer 90% do caminho para implementar um somaDias, que pode ser visto como uma extensão do amanhã, em outras palavras, somar dias é avançar vários amanhã’s. A seguir a solução do amanha:

Resolvendo o somaDias a partir do amanha
 1 class Data {
 2                        // Jan Fev Mar Abr Mai Jun
 3   static int[] diasMes = {31, 28, 31, 30, 31, 30,
 4                           31, 31, 30, 31, 30, 31};
 5                        // Jul Ago Set Out Nov Dez
 6   static int[] amanha(int[] data) {
 7     // copiar o estado
 8     int[] amanha = {data[0] + 1, data[1], data[2]};
 9     // se ultrapassou o último dia do mês
10     if (amanha[0] > diasMes[amanha[1] - 1]) {
11       amanha[0] = 1; // dia 1
12       // se antes de dezembro
13       if (amanha[1] < 12) {
14         amanha[1] = amanha[1] + 1; // próximo mês
15       } else { // se em dezembro
16         amanha[1] = 1; // janeiro do
17         amanha[2] = amanha[2] + 1; // próximo ano
18       }
19     }
20     return amanha;
21   }
22 
23   static int[] somaDias(int[] data, int dias) {
24     int[] resultado = {data[0], data[1], data[2]};
25     for (int i = 0; i < dias; i++) {
26       resultado = amanha(resultado);
27     }
28     return resultado;
29   }

Este exemplo demonstra a estratégia de decomposição do problema em partes menores que ajudam na solução do problema maior. A simplicidade em saltar um dia é visível nas expressões condicionais entre as linhas 10 e 19. É possível escrever estas regras de várias formas, a apresentada foi um exemplo didático. O ponto alto do exemplo está entre as linhas 25 e 27, que utiliza o amanha como um subprocedimento de somaDias, executado n (ou dias) vezes. Embora tenha passado nos Casos de Teste, ainda há um bug eminente de uma regra não implementada para ver no exerício a seguir:

Representação de Texto (char e String)

Fechando este tópico, mais uma coisa a considerar: a representação de texto. As linguagens em geral disponibilizam o tipo string, representado literalmente por caracteres entre aspas, por exemplo "#poocomhonra". As strings, embora fundamentais nas linguagens, não são elementares, mas sim compostas por caracteres individuais (em Java um char) e podem ser vistas como um vetor de caracteres. Assim, elas compartilham as propriedades dos vetores, como posição e tamanho, conforme exemplo a seguir:

Lendo strings como um vetor
// dica: marque os índices
                //012345678901
String hashtag = "#poocomhonra";
// comprimento da string
System.out.println(hashtag.length() == 12);
// primeiro caractere
System.out.println(hashtag.charAt(0)  == '#');
// em Java um char é representado
// por aspas simples
System.out.println(hashtag.charAt(4)  == 'c');
System.out.println(hashtag.charAt(11) == 'a');
char[] caracteres = hashtag.toCharArray();

As strings assim como os vetores, em Java, são objetos, referenciáveis, mas com uma diferença: enquanto vetores são mutáveis, strings não são. Essa característica de imutabilidade da string, e que veremos como fazer com nossos objetos nos capítulos a seguir, garante que as strings originais não sofram alterações quando passadas para procedimentos ou métodos. Já os vetores, estão propensos a alterações por onde passam, fenômeno conhecido como efeito colateral do procedimento (e que vale o mesmo para métodos em OO). Para exemplificar, vamos retomar o procedimento amanha:

Procedimento amanha com retorno de cópia modificada da entrada
 1 class Data {
 2                        // Jan Fev Mar Abr Mai Jun
 3   static int[] diasMes = {31, 28, 31, 30, 31, 30,
 4                           31, 31, 30, 31, 30, 31};
 5                        // Jul Ago Set Out Nov Dez
 6   static int[] amanha(int[] data) {
 7     // copiar o estado
 8     int[] amanha = {data[0] + 1, data[1], data[2]};
 9     // se ultrapassou o último dia do mês
10     if (amanha[0] > diasMes[amanha[1] - 1]) {
11       amanha[0] = 1; // dia 1
12       // se antes de dezembro
13       if (amanha[1] < 12) {
14         amanha[1] = amanha[1] + 1; // próximo mês
15       } else { // se em dezembro
16         amanha[1] = 1; // janeiro do
17         amanha[2] = amanha[2] + 1; // próximo ano
18       }
19     }
20     return amanha;
21   }

O procedimento amanha faz uma cópia do vetor de entrada e devolve essa cópia, contudo, é possível alterar o próprio vetor de entrada e não criar um vetor novo. Essa técnica é econômica, pois reduz processamento e memória, contudo, perde-se o estado anterior do vetor. Para demonstrar essa técnica de mutabilidade considere um novo procedimento chamado amanha2, nele o vetor original é alterado conforme o exemplo:

Procedimento amanha2 sem retorno e com efeito colateral
  static void amanha2(int[] data) {
    data[0] = data[0] + 1;
    if (data[0] > diasMes[data[1] - 1]) {
      data[0] = 1;
      if (data[1] < 12) {
        data[1] = data[1] + 1;
      } else {
        data[1] = 1;
        data[2] = data[2] + 1;
      }
    }
  }

O procedimento amanha2, embora declare retorno void (vazio), gera um retorno refletido na própria entrada. Considere os seguintes testes para entender:

Testando um procedimento que causa efeito colateral
int[] umaData = {31, 7, 2016};
System.out.println(umaData[0] == 31);
System.out.println(umaData[1] == 7);
System.out.println(umaData[2] == 2016);
Data.amanha(umaData); // sem atribuição
System.out.println(umaData[0] == 1);
System.out.println(umaData[1] == 8);
System.out.println(umaData[2] == 2016);
Data.amanha(umaData); // age sobre a data modificada
System.out.println(umaData[0] == 2);
System.out.println(umaData[1] == 8);
System.out.println(umaData[2] == 2016);

Os efeitos colaterais estão propensos quando se usam tipos referenciados, objetos. Contudo, alguns objetos não são mutáveis. Considere o seguinte procedimento com string, que não produz resultado:

Procedimentos para cortar strings
class Texto {

  static void corta(String texto, int tamanho) {
    texto.substring(0, tamanho);
  }

  static String corta2(String texto, int tamanho) {
    String cortada = texto.substring(0, tamanho);
    return cortada;
  }

  public static void main(String[] args) {
    String hashtag = "#poocomhonra";
    // cortar a string a partir
    // do 4º caractere (exclusivo)
    Texto.corta(hashtag, 4);
    // o teste não passa (false)
    // a hashtag continua sendo #poocomhonra
    System.out.println(hashtag.equals("#poo"));
    // cortar e devolver em uma cópia
    String hashtagmod = Texto.corta2(hashtag, 4);
    System.out.println(hashtagmod.equals("#poo"));
    System.out.println(hashtag.equals("#poocomhonra"));
  }
}

Mesmo que strings sejam referenciadas lembre que elas são imutáveis, ou seja, o objeto original nunca é alterado. É por esse motivo que essa instrução String s = s.toUpperCase(); altera o valor em s e essa instrução s.toUpperCase() não. Na primeira instrução a versão maiúscula da string repõe a antiga string, na segunda a cópia não é reatribuída e a string de s permanece original, minúscula.

Para fechar esse tópico, vamos trabalhar um pouco com strings no modo “roots”, isto é, não vamos usar os procedimentos e métodos embutidos da linguagem, em vez disso, vamos trabalhar caractere a caractere, em outras palavras, “lidar com strings no braço”! Como exemplo, vamos retomar o procedimento corta(String,int):String, só que desta vez sem usar o método substring:

Procedimentos para cortar strings
 1 class Texto2 {
 2 
 3   static String corta(String texto, int tamanho) {
 4     // reservando o espaço para os caracteres
 5     char[] caracteres = new char[tamanho];
 6     // copiando da string para o vetor
 7     for (int i = 0; i < tamanho; i++) {
 8       caracteres[i] = texto.charAt(i);
 9     }
10     // usando o vetor para uma String
11     return new String(caracteres);
12   }
13 
14   public static void main(String[] args) {
15     String hashtag = "#poocomhonra";
16     System.out.println(corta(hashtag, 1).equals("#"));
17     System.out.println(corta(hashtag, 2).equals("#p"));
18     System.out.println(corta(hashtag, 3).equals("#po"));
19     System.out.println(corta(hashtag, 4).equals("#poo"));
20     System.out.println(corta(hashtag, 5).equals("#pooc"));
21     // faltam as situações excepcionais, por exemplo:
22     // System.out.println(corta(hashtag, 20).equals("#poocomhonra"));
23   }
24 }

Esse procedimento traz alguns recursos importantes para o aprendizado, como manipular strings caractere a caractere para criar novas strings (ver linha 11).

1.6 “Procedimentos Orientados a Objetos?!”

É possível implementar procedimentos que recebam objetos, já que objetos são Estruturas de Dados e podem substituir os vetores nessa tarefa. No fim, é um misto: usa-se o procedimento para codificar a lógica e o objeto para carregar os dados. Considere o exemplo da data revisitado, agora com objetos, a seguir:

Procedimentos misto com Objeto de dados para data
class DadosData {
  int dia, mes, ano;
}

class DataMisto {
  static int[] diasMes = {31, 28, 31, 30, 31, 30,
                          31, 31, 30, 31, 30, 31};

  static DadosData amanha(DadosData data) {
    DadosData amanha = new DadosData();
    amanha.dia = data.dia + 1;
    amanha.mes = data.mes;
    amanha.ano = data.ano;
    if (amanha.dia > diasMes[amanha.mes - 1]) {
      amanha.dia = 1;
      if (amanha.mes < 12) {
        amanha.mes = amanha.mes + 1;
      } else {
        amanha.mes = 1;
        amanha.ano = amanha.ano + 1;
      }
    }
    return amanha;
  }

  public static void main(String[] args) {
    DadosData umaData = new DadosData();
    umaData.dia = 31;
    umaData.mes = 7;
    umaData.ano = 2016;
    DadosData resultado = DataMisto.amanha(umaData);
    System.out.println(resultado.dia == 1);
    System.out.println(resultado.mes == 8);
    System.out.println(resultado.ano == 2016);
  }
}

Neste exemplo, o módulo DataMisto e o procedimento amanha agem sobre DadosData, que é um substituto do vetor, servindo para “transportar” os dados _dia, mês e ano__.

1.7 POO com Honra!

Um característica interessante na programação e desenvolvimento de software é a possibilidade de resolver o mesmo problema de várias maneiras diferentes, com códigos diferentes, estruturas diferentes, enfim, programar é uma atividade criativa. Quando se fala em paradigmas, cada um orienta, influencia e impacta fortemente no código resultante, mas para usá-los corretamente o programador deve “chavear” seu modo de pensar, pois o entendimento do paradigma é fundamental para escrever um código sólido e uma solução adequada.

Importante, escolher um paradigma é também abraçar os conceitos e diretrizes presentes nele, projetando seus programas segundo os construtos e ideias (e ideais) principalmente. Por exemplo, aderir a Programação Guiada por Eventos, implica em especificar o fluxo do programa reagindo a eventos, assim como aderir à Programação Funcional implica em encapsular a lógica do programa como funções sem estado.

Programar aderindo ao paradigma Orientado a Objetos implica em seguir as orientações e manter o código consistente com os conceitos básicos e fundamentais, tais como o que são objetos e como se escreve um programa com eles. A maioria das linguagens, no entanto, não “obrigam” os programadores a seguir esses conceitos. Na prática, as linguagens modernas juntam conceitos e técnicas disponíveis de vários paradigmas, por exemplo, Ruby é une programação estruturada, procedimental, orientada a objetos e tem alguns aspectos funcionais e reflexivos.

Então, como Programar Orientado a Objetos do jeito certo?* Basta honrar os conceitos do paradigma. No entanto, não quer dizer que, às vezes, não se possa usar outra abordagem, graças as linguagens multiparadigmas isto não é nenhum motivo de embaraçamento. O que não se deve, de verdade, é violar os princípios de OO e afirmar, ainda, que o programa é OO - o pior enganador é o que engana a si mesmo. Por exemplo, um dos princípios mais básicos de OO é manter as estruturas de dados e algoritmos no mesmo módulo (neste caso, na mesma classe). Na prática, significa colocar no mesmo escopo os atributos e os métodos. Como exemplo, considere novamente o exemplo da data em OO “honrado”:

A Data Orientada a Objetos
 1 public class POOComHonra {
 2   public static void main(String[] args) {
 3     Data umaData = new Data();
 4     umaData.dia = 31;
 5     umaData.mes = 7;
 6     umaData.ano = 2016;
 7     Data resultado = umaData.amanha();
 8     System.out.println(resultado.dia == 1);
 9     System.out.println(resultado.mes == 8);
10     System.out.println(resultado.ano == 2016);
11   }
12 }
13 
14 class Data {
15   int dia, mes, ano;
16   static final int[]
17       diasMes = {31, 28, 31, 30, 31, 30,
18                  31, 31, 30, 31, 30, 31};
19 
20   Data amanha() {
21     Data amanha = new Data();
22     amanha.dia = this.dia + 1;
23     amanha.mes = this.mes;
24     amanha.ano = this.ano;
25     if (amanha.dia > diasMes[amanha.mes - 1]) {
26       amanha.dia = 1;
27       if (amanha.mes < 12) {
28         amanha.mes = amanha.mes + 1;
29       } else {
30         amanha.mes = 1;
31         amanha.ano = amanha.ano + 1;
32       }
33     }
34     return amanha;
35   }
36 }

Esta classe honra o princípio mais básico de acomodar dados e algoritmos na mesma unidade*. Os dados são representados por atributos da classe (linha 15) e o algoritmo no método amanha (linha 20) que não é mais estático (o que se equipara a um procedimento), em vez disso ele é uma operação do objeto Data (métodos não-estáticos são operações/ações dos objetos).

Outro detalhe importante está entre as linhas 22 a 24, que é o acesso aos atributos através do this (este objeto). Se tem muito a discutir ainda, a rigor, a classe Data não está de acordo com vários outros conceitos essenciais de OO, como encapsulamento e validade do estado, por exemplo, que ficam para os capítulos a seguir.

2. A história da Programação Orientada a Objetos

Ou você morre herói ou vive suficiente para ver si mesmo tornar-se o vilão.

– Harvey Dent

As primeiras bases da Programação Orientada a Objetos (POO) apareceram no início dos anos sessenta, mas não todas de uma vez só. A POO foi o resultado de algumas ideias reunidas a partir das pesquisas realizadas no Massachusetts Institute of Technology (MIT), no Norwegian Computing Center (NR), e outros estudos mais práticos no Xerox Palo Alto Research Center (Xerox PARC), Bell Labs e Sun Microsystems. Cada uma dessas iniciativas serviu para moldar a POO como conhecemos hoje. Os principais marcos tecnológicos que contribuiram para o desenvolvimento do paradigma orientado a objetos são apresentados nos tópicos a seguir.

2.1 LISP: átomos

O termo objeto foi mencionado no MIT como sinônimo de átomo, que é um dos dois tipos de dados disponíveis na linguagem de programação LISP junto com as listas. Os átomos (ou objetos) possuem propriedades (ou atributos) que podem ser outros átomos. A linguagem LISP foi uma ferramenta essencial para a pesquisa no trabalho pioneiro desenvolvido no MIT’s Artificial Intelligence Lab no fim da década de cinquenta e início de sessenta. Ela foi usada para processar dados simbólicos, conforme foi projetada por John McCarthy e implementada por Steve Russel. É importante dizer que o próprio John McCarthy cunhou o termo Inteligência Artificial (IA). LISP desempenhou um papel importante nas origens do conceito de orientação a objetos, pensando em pequenos e inseparáveis pedaços de dados e lógica que contém e processam mais pedaços de dados e lógica.

2.2 Sketchpad: mestre e definição

Os termos objeto e instância também foram mencionados na MIT PhD Thesis de Ivan Sutherland entitulada Sketchpad: A man-machine graphical communications system, onde ele apresentou os resultados do seu trabalho seminal com interfaces gráficas e formas inovadoras de interação, ainda no início dos anos sessenta. Os termos objeto e instância eram usados alternativamente e com o mesmo significado dos termos principais chamados mestre e definição, que eram respectivamente os nomes conceituais dados à ideação de formas geométricas e suas subsequentes cópias concretas. Nos capítulos seguintes, será visto que essas noções de uma ideia abstrata e suas representações concretas estão alinhadas com as noções atuais de classes e instâncias. Dito isso, o Sketchpad de Sutherland foi um passo essencial em direção ao amadurecimento das ideias de orientação a objetos.

2.3 ALGOL: procedimentos + estruturas de dados

A linguagem de programação ALGOL tinha tido várias versões quando, durante seu desenvolvimento nos anos sessenta, houveram os primeiros esforços para juntar a lógica dos procedimentos com os dados do programa, declarando ambos dentro da mesma unidade de código. Essa funcionalidade foi a predecessora da ideia dos métodos dos objetos como conhecemos hoje, que são funções membros das estruturas de dados. ALGOL influenciou várias linguagens de programação subsequentemente, entre elas Pascal e C, mas foi com Simula 67 que a POO deu um salto.

2.4 Simula: classes, subclasses e herança

Simula 67 foi a primeira linguagem de programação a apresentar os conceitos de classe, subclasse, herança e objetos, juntos. Também conhecida como ALGOL com classes, ela foi projetada pelos noruegueses Kristen Nygaard e Ole-Johan Dahl no Norwegian Computing Center (NR). Naturalmente, Simula carecia de alguns conceitos e princípios que só apareceriam anos depois; contudo, ela influenciou várias linguagens de programação modernas, entre elas as populares C++ e Java, servindo de inspiração como declarado pelos seus respectivos criadores Bjarne Stroustrup e James Gosling. Simula foi um marco, podendo-se dizer que houveram as épocas pré e pós-Simula 67 na linha do tempo da POO.

2.5 Smalltalk: troca de mensagens

A segunda onda na evolução do pensamento orientado a objetos veio com a linguagem de programação Smalltalk. Ela foi projetada por Alan Kay, Dan Ingalls e Adele Goldberg nas instalações do Xerox PARC. Seu projeto foi motivado e influenciado principalmente pelos avanços nas interfaces gráficas e suas ideias de objetos virtuais (widgets), que possuiam algumas propriedades e permitiam algumas ações, semelhante ao Sketchpad de Sutherland. Essa motivação foi liderada por Alan Kay, que previu aplicações educacionais dentro de seu projeto específico chamado The Dynabook, que era um dispositivo para crianças muito semelhante ao tablet que conhecemos hoje.

Smalltalk definiu ou solidificou a maioria dos conceitos e princípios da orientação a objetos conhecidos na atualidade, por exemplo, as noções de classe, objeto, estado e a passagem de mensagem e colaboração entre os objetos para cumprir uma meta. Sobre este último, “passagem de mensagem”, Alan Kay declarou como o princípio central da Programação Orientada a Objetos.

A linguagem de programação Smalltalk não teve profunda penetração de mercado ou o mesmo apelo comercial que as outras linguagens concorrentes tiveram no seu tempo. Ainda assim, ela causou uma evolução e revolução ao mesmo tempo, oxigenando ideias e apresentando um novo modelo mental para a organização dos algoritmos dentro dos programas, liderando um movimento que após seria seguido por outros cientistas da computação e engenheiros de software, sejam teóricos ou praticantes.

Finalmente, Alan Kay cunhou o temo “Object-Oriented” (Orientado a Objetos), inspirando-se em todos os avanços tecnológicos anteriores (Sketchpad, Simula), suas ideações e recursos (“sobre o ombro de gigantes”). Para alcançar esse modelo, sendo um Matemático e Biólogo, imaginou os objetos como sendo pequenos computadores em uma rede compartilhada, cada qual com seu estado e recursos, trocando mensagens e, logo, colaborando para concluir uma tarefa. Ele pensou, também, como se os objetos fossem análogos às células biológicas, que se comunicam entre si e juntas formam tecidos, órgãos e por fim o sistema inteiro. Ambas visões são precisas e um modo bem elegante de definir objetos.

2.6 C++: pondo POO no jogo

A primeira linguagem de programação orientada a objetos usada amplamente foi a C++ (C mais mais ou “C plus plus” em inglês). Ela foi projetada e implementada por Bjarne Stroustrup no Bell Labs (laboratórios Bell) no início dos anos oitenta. Inspirado na linguagem Simula, Bjarne construiu a C++ sobre a já conhecida e estabelecida linguagem C5. Talvez este tenha sido o motivo de por que ela foi (e é) bem sucedida. Aproveitando a sintaxe e construtos básicos da linguagem C, C++ foi até chamada inicialmente de “C com classes” por Bjarne, habilitando a maioria dos conceitos conhecidos de POO tais como abstração, herança, polimorfismo e encapsulamento. Ela é usada, por quase quatro décadas, para desenvolver sistemas operacionais, jogos, drivers, firmwares, utilitários de sistema e uma ampla variedade de softwares.

2.7 Java: POO para as massas

No início dos anos noventa, uma equipe trabalhando na Sun Microsystems (hoje Oracle), liderada por James Gosling, estava trabalhando em uma linguagem de programação orientada a objetos que, apesar da inspiração em Smalltalk e C++, fosse mais “versátil”. Seu nome seria Oak (carvalho), depois Greentralk, mas por fim ficou conhecida como Java (em homenagem ao famoso café indonésio produzido na ilha de Java).

Inicialmente, Java foi projetada para ser usada em uma variedade de set-top boxes (tipo um aparelhinho de TV a cabo da época). Assim, ela foi pensada para ser portável, usando a mesma base de código e execuando em qualquer dispositivo que tivesse um subsistema conhecido como Java Virtual Machine (JVM), que é responsável por “reproduzir” os programas. Ela foi lançada em 1995 e tornou-se rapidamente a mais influente e popular linguagem de programação orientada a objetos que já existiu devido, oportunamente, ao rápido crescimento da Internet e dos aplicativos web.

Java foi responsável pela difusão do paradigma de POO, sendo consistente com linguagens de modelagem visual, como a Unified Modelling Language (UML), e razoavelmente simples se comparada com suas predecessoras C++ e Smalltalk (mas não com suas sucessoras), sendo que não possui herança múltiplia (recurso alegando como sendo uma significante fonte de problemas) e possui gerenciamento automático de memória alocada realizado pelo seu Garbage Collector (coletor de lixo).

Finalmente, Java inspirou muitas outras linguagens de programação modernas no seu “jeito de fazer POO”, como PHP, C#, ECMAScript, Scala, Kotlin e outras. Ela também passou a ser usada em boa parte das instituições educacionais como primeira linguagem e para o ensino de POO. Hoje em dia, Java é usada em aplicativo corporativos de larga escala (plataforma Java EE), além de ser a linguagem primária para o desenvolvimento de aplicativos para smartphones e embarcados na plataforma Android da Google.

2.8 A supremacia Orientada-a-Objetos

Olhando para os índices e histórico de popularidade das linguagens de programação, agora (Maio/2019), as orientadas a objetos estão sempre no topo. O PYPL (ver ilustração a seguir) mostra Python, Java, JavaScript, C#, PHP, C, Objective-C e Swift no top ten. Nas posições inferiores, nos índices PYPL e Tiobe, existem outras como Visual Basic.NET, Ruby, TypeScript, Perl, Scala, Kotlin, Golang, onde todas, em maior ou menor escala, suportam o paradigma orientado a objetos.

PYPL, Maio de 2019
PYPL, Maio de 2019

É essencial mencionar que, mesmo com esse volume de linguagens de programação, elas não tratam a orientação a objetos da mesma maneira. Uma parte considerável delas são class-based (baseadas em classes), isto é, os objetos são criados usando templates (modelos), conhecidos como classes, de onde eles são instanciados. Esse é o caso da Java, PHP, C++, Python e Ruby, por exemplo. Por outro lado, existem as prototype-based (baseadas em protótipos), isto é, as classes não são a estrutura primária e qualquer objeto anteriormente declarado pode servir como protótipo (modelo) para novos objetos. Esse é o caso das linguagens JavaScript, Lua e Self, por exemplo. Ainda, existem linguagens que suportam orientação a objetos, mas que não são estritamente baseadas nem em classes ou protótipos, como Golang.

Apesar da sua fan-base, as linguagens prototype-based não são tão populares quanto as class-based. Mesmo JavaScript, a linguagem prototype-based mais bem sucedida, também oferece construtos na forma de classes, tentando agradar programadores(as) das “duas escolas”.

2.9 Considerações

Os conceitos e princípios da programação orientada a objetos se aplicam mais ou menos do mesmo modo nas linguagens C++, Java, C#, Objective-C, Swift e PHP, entre outras. Eles também são aplicáveis, com algumas adaptações aqui e ali, em JavaScript, Python, Ruby e Golang, por exemplo.

Este livro não é sobre uma linguagem de programação. Em vez disso, a meta é a compreensão e a confiança na implementação dos conceitos e princípios da POO em qualquer que seja a linguagem de programação que os suportem. Em outras palavras, é mais valorizado o pensamento orientado a objetos do que o conhecimento específico sobre sintaxes e bibliotecas disponíveis nas linguagens.

Assim, cada capítulo e tópico seguinte discorrerá sobre conceitos e princípios que podem ser escritos em (quase) qualquer linguagem de programação. Há exemplos de código em principalmente em Java. Mas eventualmente podem ser apresentados códigos em outras linguagens, no livro ou no repositório. Como última sugestão, se tu não tens muita experiência com qualquer linguagem, escolha uma e use-a para aprender orientação a objetos.

Espero que aprecies a experiência e aprendizado.

2.10 Exercícios

3. Modelo de Objetos

O que é real? Como você define o “real”? Se você está falando sobre o que você pode sentir, o que você pode cheirar, o que você pode saborear e ver, então “real” são simplesmente sinais elétricos interpretados pelo seu cérebro.

– Morpheus

Existe um entendimento compartilhado nas comunidade de engenharia de softwares de quais seriam os conceitos e princípios centrais da programação orientada a objetos. Entre as palavras-chave frequentes que emergem dessa comunidade estão: abstração, encapsulamento, polimorfismo, passagem de mensagens, herança, classes, tipos, instâncias, modularidade. Esses e outros termos seriam parte do modelo de objetos, isto é, configuram os tópicos essenciais a estudar e dominar a fim de declarar-se competente na programação orientada a objetos. Entretanto, existem diferentes “caminhos”, em que esses assuntos podem se encaixar ou não (ou sim, com alguns ajustes). Dito isso, é preciso traçar uma rota.

Como mencionado no capítulo anterior, algumas linguagens (como JavaScript e Self) os objetos são projetados pela declaração de protótipos, enquanto em outras (como Java e PHP) eles são declarados como classes. Ainda, outras linguagens (como Simula e C++) tratam os objetos como pertencentes a um tipo, isto é, os objetos são tipificados, pertencem a um tipo particular. Em outras linguagens os objetos são classificados, isto é, pertencem a uma classe particular.

Assim, neste livro optei pela abordagem class-based. Isto é, os conceitos e princípios da POO são estreitados àqueles presentes nas linguagens de programação que descrevem os objetos através da classificação. Contudo, é possível o entendimento e aplicação da maioria dos conceitos e princípios também nas linguagens type-based e prototype-based, sendo que elas compartilham muitas características.

Esse capítulo abrange os conceitos principais para um modelo de objetos: abstração, classes, atributos, objetos e inicialização (construção).

3.1 Abstração: a arte da redução da representação

A POO ajuda a descrever os objetos do “mundo real” começando por elencar suas características e operações essenciais. Após isso, o objeto é classificado, isto é, as características e operações elencadas são codificadas, respectivamente, como atributos e métodos em uma unidade chamada classe.

Objetos reais, sejam tangíveis como uma laranja (a fruta) ou intangíveis como a dívida no cartão de crédito, devem ser descritos com detalhes mínimos em vez de nos mínimos detalhes. De fato, apenas poucos atributos e métodos são selecionados para formar uma classe. Vários outros detalhes são ignorados ou movidos para outras unidades (classes), dada sua irrelevância para a solução do problema pontual. Este ato é chamado de abstração.

A abstração é um dos conceitos centrais da POO. Sem ela, seria impossível classificar qualquer coisa. Por exemplo, considere que tenhamos de classificar copos (de beber). A primeira questão “quais são as características essenciais de um copo?” leva a outras questões, como “ele é feito de vidro? porcelana? plástico? de qual material ele é feito? quanto líquido em tem quando cheio? qual sua capacidade?”. Pensando um pouco mais chegamos em mais questões: “ele é redondo? qual é o diâmetro? se não, qual é o perímetro?. Mais raciocínio sobre o copo leva aos mínimos detalhes, como decoração, cor, medida do gargalo, peso e até questões existenciais, linguísticas e metafísicas do copo (como se os copos com alça mantém sua “copocidade” ou se tornam canecas).

Então, sabemos que não podemos classificar um copo átomo por átomo, no espaço e no tempo. Logo, a capacidade de abstração desempenha um papel essencial na classificação dos objetos. A meta, portanto, é projetar um simulacro bom o suficiente, sem preocupar-se com as pequenas imprecisões.

Dito isso, o conceito e técnica de abstração é profundamente usada neste livro. Até porque os exemplos devem ser sucintos para caber nele. Então, sempre que avistares um objeto supersimplificado, foi de propósito.

3.2 Classes: o gabarito para os objetos

Comecemos com um exemplo: considere um Livro. Quais são as características relevantes para representar um Livro? Naturalmente, depende de duas coisas: do contexto e do observador. A ponto-de-vista seria diferente dependendo da pessoa (física ou jurídica), se: um leitor, um bibliotecário, a editora ou a livraria.

Mantendo simples, considere que um Livro tem um título, um autor e uma quantidade de páginas. O sujeito da classificação é o substantivo Livro. Os atributos são extraídos dos adjetivos (ex.: título). Com esse dados, é possível classificar e representar vários livros. O código resultante varia de uma linguagem de programação para outra. Então considere o seguinte pseudo-código em português estruturado:

Pseudo-código da classificação de Livro
// Livro.pseudocódigo
classe Livro
  atributo Titulo : textual
  atributo Autor : textual
  atributo Paginas : númerico
fim classe

// App.pseudocódigo
usando Livro de Livro.pseudocódigo
procedimento App
  um_livro = inicializa um Livro
  um_livro.Titulo = "Eu Robô"
  um_livro.Autor = "Asimov, Isaac"
  um_livro.Paginas = 256

  outro_livro = inicializa um novo Livro
  outro_livro.Titulo = "Neuromancer"
  outro_livro.Autor = "Gibson, William"
  outro_livro.Paginas = 271
fim procedimento

Esse pseudo-código não está muito longe do código real, segundo a sintaxe das linguagens orientadas a objetos mais populares. Primeiro, define-se uma classe e, então, define-se os atributos. Essa classe pode ser usada/importada/incluída em outros arquivos/módulos. Ela pode ser inicializada, construindo (guarde essa palavra) um objeto a partir dela, que é preenchido com os valores específicos da instância. (Quase) Infinitos livros individuais podem ser instanciados a partir do gabarito: a classe Livro. A seguir, o mesmo modelo é implementado em três linguagens de programação diferentes:

Classificação de Livro na linguagem Java
// Livro.java
class Livro { // declaração da classe
  String titulo = ""; // atributo textual
  String autor = "";
  int paginas = 0; // atributo numérico inteiro
} // fim da classe
// App.java
class App {
  // procedimento principal
  public static void main(String[] args) {
    Livro um_livro = new Livro(); // construindo um novo Livro
    um_livro.titulo = "I Robot";
    um_livro.autor = "Asimov, Isaac";
    um_livro.paginas = 256;

    Livro outro_livro = new Livro(); // construindo outro novo Livro
    outro_livro.titulo = "Neuromancer";
    outro_livro.autor = "Gibson, William";
    outro_livro.paginas = 271;

    System.out.println(um_livro.titulo);
    System.out.println(outro_livro.titulo);
  }
}
// executar com o comando: javac App.java ; java App
Classificação de Livro na linguagem JavaScript
// livro.js
class Livro { // declaração da classe
  construct() {
    this.title = ""; // atributo textual
    this.author = "";
    this.pageCount = 0; // atributo numérico
  }
} // fim da classe
module.exports = Livro

// app.js
const Livro = require("./livro.js");
// procedimento principal
let um_livro = new Livro(); // construindo um novo Livro
um_livro.titulo = "Eu Robô";
um_livro.autor = "Asimov, Isaac";
um_livro.paginas = 256;

let outro_livro = new Livro(); // construindo outro Livro
outro_livro.titulo = "Neuromancer";
outro_livro.uthor = "Gibson, William";
outro_livro.paginas = 271;

console.log(um_livro.titulo);
console.log(outro_livro.titulo);
// executar com o comando: node app.js
Classificação de Livro na linguagem Python
# livro.py
class Livro: # declaração da classe
  def __init__(self):
    self.titulo  = "" # atributo textual
    self.autor   = ""
    self.paginas = 0 # atributo numérico

# app.py
from livro import Livro

um_livro = Livro() # construindo um novo Livro
um_livro.titulo = "Eu Robô"
um_livro.autor = "Asimov, Isaac"
um_livro.paginas = 256

outro_livro = Livro() # construindo mais um Livro
outro_livro.titulo = "Neuromancer"
outro_livro.autor = "Gibson, William"
outro_livro.paginas = 271

print(um_livro.titulo)
print(outro_livro.titulo)

Existem diferenças sutis entre uma linguagem e outra. Sobretudo se desconsider a sintaxe. O modelo, claro, foi supersimplificado. Se a meta fosse desenvolver um comércio eletrônico (e-commerce), estariam faltando diversas características importantes para a venda dos livros, tais como: edição, ISBN, data de publicação, e até mesmo o peso do exemplar que é usado para o cálculo do frete.

3.3 Objetos: instâncias de uma classe

Ao fim deste tópico, certifique-se de que a diferença e a relação entre classes e objetos estejam bem claras na tua mente. Isso é essencial.

Retomando o exemplo do Livro, a nossa classificação descreve as características que esperamos estar presentes, que representam um livro em particular, no caso: titulo, autor e páginas. A classe é um gabarito, um modelo (um template como dizem em Inglês), que será usado para garantir certas funcionalidades às instâncias, que são os objetos em si. Tecnicamente, a instanciação de uma classe resulta em um novo objeto, e essa ação pode ser repetida inúmeras vezes gerando inúmeros objetos (instâncias) que seguem o mesmo gabarito. No exemplo anterior, todas as instâncias de Livro terão os atributos titulo, autor e páginas.

Para refinar esse entendimento, podemos revisitar o primeiro exemplo do capítulo: o Copo. Vamos abstrair rigorosamente, mantendo apenas duas características relevantes: o material de que é feito e a capacidade em mililitros. Segue pseudo-código de Copo:

Classificação de Copo em pseudo-código
// Copo.pseudocódigo
classe Copo
  atributo material : textual
  atributo mililitros : numérico
fim classe

Com essa noção, é possível implementar em qualquer linguagem moderna que suporte classes.

Classificação de Copo em Java
// Copo.java
class Copo {
  String material   = "";
  int    mililitros = 0;
}

A partir dessa classe podemos construir (instanciar) os copos:

// App.java
class App { // java exige o método main envolto em uma classe
  public static void main(String[] args) {
    Copo copo1 = new Copo(); // "Copo()" é o construtor de Copo
    copo1.material = "Plástico"; // o "." é usado para ler/alterar um atributo
    copo1.mililitros = 300;
    System.out.println("Copo de " + copo1.material + "/"
      + copo1.mililitros + "ml"); // Copo de Plástico/300ml

    Copo copo2 = new Copo();
    copo2.material = "Vidro";
    copo2.mililitros = 250;
    System.out.println("Copo de " + copo2.material + "/"
      + copo2.mililitros + "ml"); // Copo de Vidro 250ml
  }
}

Conforme o exemplo, foram construídos (instanciados) dois copos, armazenados nas variáveis copo1 e copo2 (referências). Como especificado na classe, os copos têm dois atributos: material e mililitros. Por enquanto, tu podes pensar no objeto como um agrupamento de dados. Porém, mais tarde, veremos que os objetos agrupam também os algoritmos (funções ou métodos).

Sempre que ver o nome da classe à direita de uma variável, um objeto está sendo construído. Se houver uma class Cartao, seus objetos são construídos no código com new Cartao() (Java e JS), Cartao.new (Ruby), ou apenas Cartao() (Python).

O mesmo exemplo escrito em JavaScript, Python, PHP e C# pode ser visto na pasta src/2.2-objetos.

3.4 Atributos: as “qualidades” do objeto

Nos exemplos anteriores eu classifiquei Livro e Copo, com poucas características. Para Copo foram definidos material e mililitros, e para Livro foram definidos titulo, autor e paginas. Essas características são chamadas de atributos. Assim, dizemos que um Livro tem um atributo titulo e que um Copo tem um atributo material.

Os atributos têm seus tipos pré-definidos nas linguagens de tipagem estática, como Java e C# por exemplo. Essas linguagens disponibilizam os tipos básicos, sendo comuns: int e long para números inteiros, double e float para números reais, boolean (bool em C#) para booleanos, char para um (único) caractere e String para cadeias de caracteres (dados textuais). No exemplo do Livro, enquanto o atributo titulo é do tipo String, isto é, textual, o atributo paginas é do tipo int, isto é, um número inteiro.

Nas linguagens de tipagem dinâmica (ou não-tipadas), os atributos podem armazenar valores de qualquer tipo e não apenas aquele declarado na classe. Por exemplo, embora você declare self.material = "" em Python, isto só quer dizer que ele é inicializado como textual, mas não há restrição de atribuição de valores de outros tipos nas instâncias (como livro1.material = 9 é ok). Outra diferença notável nas linguagens dinâmicas é que mais atributos podem ser adicionados diretamente aos objetos, mesmo sem serem predefinidos nas classe. Por exemplo, seria possível declarar livro1.editora = "Uma editora" em JS, Python e PHP, mesmo que o atributo editora não tenha sido pré-definido, ou seja, os atributos e seus tipos podem ser pós-definidos nas linguagens dinâmicas.

Ditas essas nuances de implementação, o(a) programador(a) deve decidir (projetar) quais atributos são relevantes para classificar um objeto e a restrição de tipo pretendida (mesmo nas dinâmicas, você não vai querer um Livro com a editora "123" ou "asdfg" páginas). O detalhamento pode variar. Então, normalmente o problema é colocado em um contexto. Por exemplo, considere a classificação de Gato (o bicho, não o furto de energia elétrica) no contexto de uma Clínica Veterinária, e pense quais atributos seriam necessários. Eu pensei em “idade, peso, sexo, nome e dono”. Talvez você tenha pensado em mais ou diferentes características, não tem problema. A seguir um código que compila perfeitamente em Java:

// Gato.java
class Gato {
  int    idade;
  int    peso;
  char   sexo;
  String nome;
  String dono;
}

Essa classificação funciona, mas pode melhorar bastante. Antes de ler o restante deste parágrafo, dedique um minuto para ler o código novamente e prever as possíveis más interpretações. É difícil perceber problemas na especificação. Às vezes é melhor ver essa classificação sendo instanciada:

// App.java
class App {
  public static void main(String[] args) {
    Gato g  = new Gato();
    g.nome = "Fito";
    g.idade = 2;
    g.peso  = 2;
    g.sexo  = 'M';
    g.dono  = "Márcio Torres";
  }
}

A instância demonstrada no exemplo apresenta algumas ambiguidades. Por exemplo, qual é a idade do gato? 2 meses ou 2 anos? quanto pesa o gato? deve ser 2kg, pois seria absurdo se fossem 2g, certo? mas e se o gato tem 700g? Dadas essas perguntas, podemos chegar em classificações um pouco melhores, ao esclarecer esses dois atributos. A seguir estão duas opções:

// Gato1.java
class Gato {
  int    idade; // valor e unidade em dois atributos
  String idadeUnidade; // semanas, meses, anos, ...
  int    peso;
  String pesoUnidade; // g, kg, hg, ...
  char   sexo;
  String nome;
  String dono;
}
// Gato2.java
class Gato {
  int    anos; // codificando a unidade no nome do atributo
  double kilogramas; // double para permitir decimais, como 0.7kg == 700g
  char   sexo;
  String nome;
  String dono;
}

Existem várias formas de eliminar ambiguidades e mitigar as possibilidades de más interpretações do modelo. As mudanças nos exemplos anteriores, ressignificando os atributos, são soluções viáveis, mas não as melhores. Uma outra abordagem é de ver idade e peso como objetos por si só, digo, classificá-los (ex.: class Peso { ...) e associá-los à classe Gato, o que será visto no Capítulo 11: Associação.

3.5 Inicialização: construção de objetos

Neste ponto, sabe-se que as características dos objetos são especificadas nas classes na forma de atributos. O próximo passo é a especificação de quais características são essenciais. Por exemplo, informações textuais que não devem estar “em branco” ou numéricas que não devem estar zeradas. Pense nos dados imprescindíveis do objeto. Em outras palavras, é a definição de quais atributos são obrigatórios e quais são suas restrições.

Nas linguagens tipadas (Java, C#, C++), já há a restrição de tipo, por exemplo, int idade deve permitir apenas números inteiros. Isto é, não entrarão dados como 5.5 ou "7 anos" ou "adsasdf". Entretanto, ainda são necessárias outras restrições, que evitem, por exemplo, valores como -1, que é válido para um int, mas não para “representar” uma idade (-1 anos?). Essas restrições e como impô-las será visto no próximo capítulo em Validade do Estado.

Neste tópico vamos usar blocos conhecidos como inicializadores ou construtores para especificar uma obrigatoriedade de informação. De volta ao exemplo do Copo, que possui material e cacidade em mililitros, considere que ambas informações são obrigatórias na instanciação do Copo e não podem ficar em branco.

Para cumprir essa funcionalidade devemos adicionar um construtor à classe Copo, exigindo como parâmetros as informações necessárias para popular os atributos. Os construtores variam de linguagem para linguagem. A seguir um pseudo-código demonstrando uma classe com construtor:

// Copo.pseudocódigo
classe Copo

  atributo material : textual
  atributo mililitros : numérico

  construtor (parâmetro material, parâmetro mililitros)
    atributo material = parâmetro material
    atributo mililitros = parâmetro mililitros
  fim construtor

fim classe

Em Java, os construtores têm o mesmo nome da classe, como a seguir:

// Copo.java
class Copo {
  String material;   // este é o   this.material
  int    mililitros; // e este é o this.mililitros
  // construtor exigindo material e mililitros na forma de parâmetros
  Copo(String material, int mililitros) {
    // repassando os valores recebidos nos parâmetros aos
    // atributos do objeto no formato: this.atributo = parametro;
    this.material   = material;
    this.mililitros = mililitros;
    // this.material é o atributo e material é o parâmetro
  }
}

Essa classificação define a inicialização obrigatória de Copo passando obrigagoriamente ambos parâmetros material e mililitros, nesta ordem e com os tipos determinados, por exemplo:

// javac App.java; java App
class App {
  public static void main(String[] args) {
    Copo copo1 = new Copo("Plástico", 300);
    System.out.println(copo1.material); // Plástico
    System.out.println(copo1.mililitros); // 300

    Copo copo2 = new Copo("Vidro", 250);
    System.out.println(copo2.material); // Vidro
    System.out.println(copo2.mililitros); // 250
    // as seguintes instruções falham (não compilam)
    // Copo copo3 = new Copo(); // falha por não passar os argumentos
    // Copo copo4 = new Copo("Louça"); // falha por não passar o argumento de mililitros
    // Copo copo5 = new Copo(300, "Vidro"); // falha por não respeitar a ordem
  }
}

Encerrando o tópico, é importante mencionar que os construtores, além de determinar obrigatoriedade, também simplificam a inicialização dos objetos.

3.6 Considerações

Alguns pontos-chave desse capítulo que devem ser lembrados:

  • Objetos têm características;
  • Para especificá-las nós classificamos os objetos;
  • A classificação é feita com a declaração de class NomeClasse na maioria linguagens;
  • Vários objetos podem ser instanciados a partir da mesma classe;
  • Todos os objetos terão os mesmos atributos definidos na classe;
  • Contudo, cada objeto terá valores específicos para estes atributos;
  • Declaramos construtores para garantir que alguns (ou todos) atributos sejam informados;
  • Existem algumas diferenças entre as linguagens de tipagem estática e dinâmica.

Entre as questões que ficaram para serem discutidas adiante destaca-se a validade do objeto. Por exemplo, não é possível construir um Copo() sem informar material e mililitros, mas é possível construir Copo("", 0), que é executável por informar os parâmetros, mas não cria um objeto Copo válido ou útil.

3.7 Exercícios

Implemente Chuveiro

Considere um Chuveiro, classificado segundo sua marca, modelo, tensão (110v ou 220v) e potência (Watts). Escreva a classe Chuveiro que passe nos seguintes testes:

// App.java
Chuveiro chu = new Chuveiro();
chu.marca = "Ducha10";
chu.modelo = "D103500";
chu.potencia = 3500;
System.out.println(chu.marca); // Ducha10
System.out.println(chu.modelo); // D103500
System.out.println(chu.potencia); // 3500
System.out.println(chu.tensao); // 220
chu.tensao = 110;
System.out.println(chu.tensao); // 110
# app.py
from chuveiro import Chuveiro
chu = Chuveiro()
chu.marca = "Ducha10"
chu.modelo = "D103500"
chu.potencia = 3500
print chu.marca # Ducha10
print chu.modelo # D103500
print chu.potencia # 3500
print chu.tensao # 220
chu.tensao = 110
print chu.tensao # 110
// app.js
let chu = new Chuveiro();
chu.marca = "Ducha10";
chu.modelo = "D103500";
chu.potencia = 3500;
console.log(chu.marca); // Ducha10
console.log(chu.modelo); // D103500
console.log(chu.potencia); // 3500
console.log(chu.tensao); // 220
chu.tensao = 110;
console.log(chu.tensao); // 110
// php.js
$chu = new Chuveiro();
$chu->marca = "Ducha10";
$chu->modelo = "D103500";
$chu->potencia = 3500;
print($chu->marca ."\n"); // Ducha10
print($chu->modelo ."\n"); // D103500
print($chu->potencia ."\n"); // 3500
print($chu->tensao ."\n"); // 220
$chu->tensao = 110;
print($chu->tensao ."\n"); // 110
// App.cs
var chu = new Chuveiro();
chu.marca = "Ducha10";
chu.modelo = "D103500";
chu.potencia = 3500;
System.Console.WriteLine(chu.marca); // Ducha10
System.Console.WriteLine(chu.modelo); // D103500
System.Console.WriteLine(chu.potencia); // 3500
System.Console.WriteLine(chu.tensao); // 220
chu.tensao = 110;
System.Console.WriteLine(chu.tensao); // 110

Projete e Implemente a classe de cartões de memória

Considere os detalhes que diferenciam uns cartões de memória de outros e classifique, especificando os atributos e seus tipos (se for o caso). Crie um arquivo principal (App ou Main) e instancie cartões variados a partir da classe.

Escreva um construtor para Chuveiro

Implemente um construtor tornando possível informar os atributos ao instanciar os chuveiros. Considere o seguinte caso de teste:

// App.java
Chuveiro chu = new Chuveiro("Ducha10", "D103500", 3500)
System.out.println(chu.marca); // Ducha10
System.out.println(chu.modelo); // D103500
System.out.println(chu.potencia); // 3500
System.out.println(chu.tensao); // 220
chu.tensao = 110;
System.out.println(chu.tensao); // 110

A mesma estrutura é aplicável às demais linguagens.

Escreva um construtor para cartões de memória

Implemente um construtor na classe de cartões conforme seu projeto e escreva casos de teste.

Implemente Elevador

Considere um Elevador para passageiros em prédios e seus dados básicos:

  • Fabricante e modelo, por exemplo, “ElevaSilva TR5500”;
  • Capacidade em kg e passageiros, por exemplo, “600kg/8 passageiros” ou “900kg/10 passageiros”, sabendo que é condiderado 75kg por passageiro;
  • Percurso em metros e paradas, por exemplo, “30m/10 paradas” ou “105m/35 paradas”, sabendo que são necessários 3 metros por parada;
  • Velocidade em metros por segundo, variando de “1.5 m/s” à “3 m/s”.
Projete, especifique e implemente um objeto a sua escolha

Pense em um objeto real, tangível ou ideia, que possa ser projetado e classificado. Procure levantar as características e seus tipos. E não esqueça a abstração.

4. Estado

Como podes provar se neste momento estamos dormindo, e que todos nossos pensamentos são um sonho; ou então se estamos acordados, e falando um com o outro em um estado desperto?

– Platão

Para projetar sistemas confiáveis é importantíssimo controlar o estado dos objetos. É preciso assegurar-se de que os objetos sejam consistentes e continuem válidos durante toda a execução do programa. O estado inválido de um único objeto pode levar todo o sistema ou aplicativo à falhar. Imagine o “estrago” que faria um objeto Hora com número negativo de minutos num app de agendamento. Por esse motivo, deve-se especificar quais características podem mudar, e quais não devem mudar nos objetos. Isto é, definir o estado mutável e o imutável. Também envolve escrever as regras de negócio (aquele punhado de if's, else's, for's, while's, ...) pensando se elas devem alterar estado e, caso sim, com que restrições.

4.1 Conceito de Estado do Objeto

Retomando o modelo de objetos, sabe-se que eles têm características codificadas na forma de atributos que guardam valores. Logo, cada objeto (ou instância) possui valores específicos para esses atributos, diferenciando as instâncias umas das outros, mesmo que compartilhem uma mesma classe. Por exemplo, enquando um instância de Gato pode ser descrita por {nome: "Tom", idade: 6, peso: 4.8}, outra pode ter os valores {nome: "Angela", idade: 5, peso: 3.9}.

O estado é definido como o conjunto de valores armazenados em todos os atributos de um objeto em um instante no tempo. Isto é, o objeto está. Por exemplo, o gato “Tom”, por ora, está com peso: 4.8 e, conhecendo o gato doméstico, logo estará com peso: 5.0.

Tenha em mente que nem sempre o objeto está, às vezes ele é. Quer dizer, nem todos os atributos irão (ou devem) mudar de valor no tempo. Os objetos podem ter estado constante e variável, ao mesmo tempo. Por exemplo, a idade e peso do gato “Tom” pode aumentar enquanto ele mantém o mesmo nome[^tah].

Portanto, durante o projeto da classe é preciso pensar sobre quais atributos devem ser constantes e quais exigem variabilidade. Essas questões são discutidas no tópico seguinte.

4.2 Estado constante e instantâneo

Primeiro, é importante mencionar que nem todas as linguagens de programação permitem declarar um atributo constante. Isso não é possível em JavaScript e Python6, por exemplo. As linguagens projetadas para declarar estado constante o fazem com diferentes palavras-chave (keyword). Por exemplo, em Java se diciona a keyword final na declaração do atributo, enquanto em C# a keyword é readonly7.

Para entender a necessidade de um atributo imutável é preciso exercitar a classificação de um objeto que possua uma característica constante. Mantendo a linha de exemplos simples, considere uma Caneta. Entre várias informações sobre canetas, vamos abstrair e selecionar apenas duas: cor e carga. Isto é, consideramos o pigmento e o nível da tinta como informações úteis e imprescindíveis para o modelo. Seguindo o modelo de objeto apresentado no capítulo anterior, uma implementação possível está a seguir:

// Caneta.pseudo
classe Caneta
  atributo cor : textual
  atributo carga : numérico
fim classe

// App.pseudo
usando Caneta de Caneta.pseudo
procedimento App
  can = nova Caneta
  can.cor = "Azul"
  imprime(can.cor) // "Azul"
  can.cor = "Vermelha";
  imprime(can.cor) // "Vermelha"
fim procedimento

Embora funcional, essa implementação ignora um detalhe importante sobre as canetas. Elas possuem uma certa carga, que decresce com o uso, mas sua cor não muda. Claro, estamos ignorando a questão da troca da carga (abstração). Assim, uma caneta “Azul” será sempre “Azul”, e uma caneta “Vermelha” seria uma nova caneta. Isso pode ser resolvido declarando a cor como constante e adicionando um construtor:

// Caneta.pseudo
classe Caneta

  atributo constante cor : textual
  atributo carga : numérico

  construtor (parâmetro cor, parâmetro carga = 1000)
    atributo cor = parâmetro cor
    atributo carga = parâmetro carga
  fim construtor
fim classe

// App.pseudo
usando Caneta de Caneta.pseudo
procedimento App
  can = nova Caneta "Azul"
  imprime(can.cor) // "Azul"
  // can.cor = "Vermelha" // não é possível, cor é constante
  can2 = nova Caneta "Vermelha"
  imprime(can2.cor) // "Vermelha"
fim procedimento

Este exemplo em pseudocódigo sintetiza a ideia de estado constante. É importante frisar que não são proibidas canetas de outras cores, apenas é restrito que a mesma caneta mude de cor. A seguir como é implementado em Java:

// Caneta.java
class Caneta {
  final String cor; // final é a keyword para constante
  int carga;
  Caneta(String cor) {
    this.cor = cor;
    this.carga = 1000;
  }
}
// Main.java
class Main {
  public static void main(String[] args) {
    Caneta can1 = new Caneta("Verde");
    System.out.println(can1.cor); // "Verde"
    // can1.cor = "Azul"; // NÃO COMPILA, a cor é imutável, constante
    Caneta can2 = new Caneta("Azul");
    System.out.println(can2.cor); // "Azul"
    System.out.println(can2.carga); // 1000
    can2.carga = 800; // carga é mutável, instantânea
    System.out.println(can2.carga); // 800
  }
}

O pequeno exemplo anterior demonstra um objeto com dois atributos, onde um é instantâneo (carga) e o outro constante (cor). Ainda assim, a caneta não está livre de “furos”. Não há restrições para o estado. Por exemplo, a instrução can.carga = -200 é válida, compila e executa sem problemas. A instrução new Caneta(""), que instancia uma caneta sem cor, também é válida. Portanto, o próximo passo é definir regras tanto para obter um estado inicial válido quanto fazê-lo permanecer válido o tempo todo.

4.3 Invariante, consistência e validade do estado

A confiabilidade de um objeto é obtida pela consistência e validade do seu estado. Essa questão pode (deve) ser tratada em dois momentos: (1) na inicialização do objeto, (2) na interação com o objeto.

Todos os atributos imutáveis devem ser inicializados no construtor. No construtor, também, são validados os atributos mutáveis, para garantir que objeto seja (tenha um estado) válido desde o começo.

Por exemplo, considere uma garrafa térmica. Elas possuem várias características que as diferenciam. Mas, vamos abstrair, desconsiderando a marca, modelo, cor, formato, etc, e considerando apenas duas informações: a capacidade e quantidade de líquido.

Vamos analisar! A capacidade é constante, isto é, uma garrafa com capacidade para 2L sempre terá esta capacidade, ela não está com 2L, ela é de 2L. Por outro lado, a quantidade é variável, instantânea, a garrafa pode estar vazia (0L), com líquido para apenas mais um mate (100mL), ou cheia (2L). O que é invariante na classe das garrafas térmicas? A quantidade nunca é negativa ou maior que a capacidade8. Além dessa, também vamos adicionar uma restrição de capacidade, mínima é de 100mL e máxima de 5L, para não criar garrafas “bizarras”, como um conta-gotas de 1mL ou caixa d’água de 1000L.

As invariantes de classe são uma coleção de condições predefinidas e servem para restringir e garantir o estado do objeto. A construção e inicialização do objeto e subsequentes operações devem respeitar essas condições.

Vamos completar o exemplo com duas operações (métodos). A primeira é encher. Quanto a segunda, considere que há um botão para servir água na nossa garrafa térmica, digital claro, que dispensa 100mL de água ou emite um beep se estiver vazia. A seguir um pseudo-código:

// GarrafaTérmica.pseudo
classe GarrafaTérmica
  
  atributo constante capacidade : numérico
  atributo quantidade : numérico
  
  construtor (parâmetro capacidade : numérico)
    se parâmetro capacidade < 100 então // regra
      lança a exceção "Capacidade mínima de 100mL"
    fimse

    se parâmetro capacidade > 5000 então // regra
      lança a exceção "Capacidade máxima de 5L"
    fimse

    atributo capacidade = parâmetro capacidade
    atributo quantidade = 0
  fim construtor

  método encher
    atributo quantidade = atributo capacidade
  fimmétodo

  método servir : boolean
    se atributo quantidade >= 100 // regra
      atributo quantidade = atributo quantidade - 100
      retorna Verdadeiro // foi servido
    fimse
    retorna Falso // não foi servido
  fimmétodo
fim classe

// App.pseudo
usando GarrafaTérmica de GarrafaTérmica.pseudo
procedimento App
  chimarrita = nova GarrafaTérmica 1000
  imprime(chimarrita.capacidade) // 1000
  imprime(chimarrita.quantidade) // 0
  chimarrita.encher()
  imprime(chimarrita.quantidade) // 1000
  chimarrita.servir()
  imprime(chimarrita.quantidade) // 900
fim procedimento

Esse programa ficou bem mais longo que os anteriores, mas não se preocupe, eu vou explicar os detalhes. A invariante da classe está comentada como // regra ao longo do código. Como podes ver, elas são pré e pós-condições que se esperam para a validade do objeto durante seu “uso”. Duas coisas são novas: a exceção e o método. As exceções são usadas para interromper o fluxo normal e providenciar informações sobre o motivo dessa interrupção. Neste exemplo, elas são usadas para evitar a instanciação de uma garrafa térmica inválida. As regras no construtor cumprem essa tarefa. Nunca haverão garrafas com capacidades menores que 100mL nem maiores que 5L, isso é garantido. Os métodos são usados para definir as operações do objeto e intermediar as mudanças de estado. Esses dois conceitos serão discutidos com mais detalhes no Capítulo 4: Comportamento.

Esse pseudo-código é implementável em quase qualquer linguagem moderna. A seguir essa lógica está escrita em Java:

// GarrafaTermica.java
class GarrafaTermica {
  
  final int capacidade;
  int quantidade;
  
  GarrafaTermica (int capacidade) {
    if (capacidade < 100) { // se inválida, lança exceção
      throw new IllegalArgumentException("Capacidade mínima de 100mL");
    }
    if (capacidade > 5000) { // se inválida, lança exceção
      throw new IllegalArgumentException("Capacidade máxima de 5L");
    }
    this.capacidade = capacidade;
    this.quantidade = 0;
  }

  void encher() { // operação/método encher
    this.quantidade = this.capacidade;
  }

  boolean servir() { // operação/método servir
    if (this.quantidade >= 100) { // é possível servir?
      this.quantidade = this.quantidade - 100;
      return true; // foi servido
    }
    return false; // não foi servido
  }
}

// Main.java
class Main {
  public static void main(String[] args) {
    GarrafaTermica chimarrita = new GarrafaTermica(1000);
    System.out.println(chimarrita.capacidade); // 1000
    System.out.println(chimarrita.quantidade); // 0
    System.out.println(chimarrita.servir()); // false
    chimarrita.encher();
    System.out.println(chimarrita.quantidade); // 1000
    System.out.println(chimarrita.servir()); // true
    System.out.println(chimarrita.quantidade) // 900
    System.out.println(chimarrita.servir()); // true
    System.out.println(chimarrita.quantidade) // 800
    while (chimarrita.servir()) {
      System.out.println(chimarrita.quantidade); // 700,600,500,400,300,200,100
    }
    System.out.println(chimarrita.quantidade); // 0
    chimarrita.encher();
    System.out.println(chimarrita.quantidade); // 1000
  }
}

Retomando o primeiro parágrafo, a confiabilidade de um objeto é obtida pela consistência e validade do seu estado em dois momentos: (1) inicialização e (2) na interação.

  1. O código anterior garante a consistência e validade do estado desde o início, no construtor. Preste atenção às linhas if (capacidade < 100) throw new IllegalArgumentException, elas interrompem a criação do objeto e tratam da validade no momento da inicialização;
  2. Agora, preste atenção à condição if (this.quantidade >= 100), ela protege o estado impedindo que a quantidade fique negativa ao subtrair 100mL e garante a validade do estado durante as interações com o objeto.

4.4 Estratégias para validar o estado

Um objeto válido é aquele que todos os seus atributos armazenam valores esperados, isto é, dentro das regras especificadas. Essas regras podem ser aplicadas logo na inicialização, no construtor. Neste caso, um objeto nunca é inválido, pois nunca será inicializado. Validar cedo é uma estratégia segura para o projeto de sistemas conhecida como fail-fast. O objetivo é interromper a operação normal assim que uma inconsistência é encontrada.

A segunda opção, é usar uma estratégia permissiva, tardia, ou leniente, que permite a criação de objeto inválidos e disponibiliza um meio de validá-los, ou adaptá-los, antes de uma operação final. A primeira estratégia, de validar no construtor, já foi discutida no tópico anterior. Então, este tópico trata da segunda.

Para exemplificar a validação tardia, considere uma playlist para a reprodução de músicas. É preciso que ela tenha um nome e, para reproduzí-la, é preciso criá-la com as músicas. Ou seja, não é possível reproduzir uma playlist vazia ou sem nome. Esse estado válido, no entanto, só é necessário antes da reprodução, assim que uma playlist pode começar inválida e continuar sendo trabalhada até ser possível reproduzí-la.

// Playlist.pseudo
classe Playlist
  
  atributo nome : textual
  atributo musicas : lista
  
  construtor
    atributo nome = "Sem nome"
    atributo musicas = nova Lista
  fim construtor

  método adicionarMusica(musica)
    musicas.adiciona(musica)
  fimmétodo

  método renomear(novoNome)
    atributo nome = novoNome
  fimmétodo

  método reproduzir
    se atributo musicas.comprimento > 0 // se há músicas
      para cada musica de musicas
        imprime("Reproduzino " + musica)
        player.play(musica)
      fimpara
    senão // se vazia
      lança a exceção "Playlist " + atributo nome + " está vazia"
    fimse
  fimmétodo
fim classe

// App.pseudo
usando Playlist de Playlist.pseudo
procedimento App
  nada = nova Playlist
  // nada.reproduzir() // lança uma exceção
  u2 = nova Playlist
  // u2.reproduzir() // lançaria uma exceção
  u2.renomear("Favoritas do U2")
  u2.adicionarMusica("Numb")
  u2.adicionarMusica("Where the Streets Have no Name")
  u2.adicionarMusica("City of Blinding Lights")
  u2.reproduzir() // reproduzindo Numb
fim procedimento

O pseudocódigo anterior apresenta um possível projeto de classe Playlist que valida o estado apenas no momento da sua reprodução. Assim, o código de validação está presente no método reproduzir em vez de no construtor. Esse projeto pode ser implementado com poucas adaptações em qualquer linguagem com suporte a orientação a objetos. A seguir o mesmo exemplo em Java:

// Playlist.java
import java.util.ArrayList;

class Playlist {
  
  String nome;
  ArrayList musicas; // ArrayList é uma estrutura de Java.Util
  
  Playlist() { // construtor
    this.nome = "Sem nome";
    this.musicas = new ArrayList();
  }

  void adicionarMusica(String musica) {
    musicas.add(musica);
  }

  void renomear(String novoNome) {
    this.nome = novoNome;
  }

  void reproduzir() {
    if (this.musicas.size() > 0) { // se há músicas
      for (String musica : this.musicas) {
        System.out.println("Reproduzindo " + musica);
        // player.play(musica); // não temos um player, ainda :/
      }
    } else {
      throw new RuntimeException ("Playlist " + this.nome + " está vazia");
    }
  }
}
// Main.java
class Main {
  public static void main(String[] args) {
    Playlist nada = new Playlist();
    // nada.reproduzir(); // lança uma exceção
    Playlist u2 = new Playlist();
    // u2.reproduzir() // lançaria uma exceção
    u2.renomear("Favoritas do U2");
    u2.adicionarMusica("Numb");
    u2.adicionarMusica("Where the Streets Have no Name");
    u2.adicionarMusica("City of Blinding Lights");
    u2.reproduzir(); // reproduzindo Numb, ...
  }
}

Essa classe considera tanto o valor padrão, como o texto "Sem nome" para uma playlist recém criada, como permite que a playlist fique vazia sem lançar exceções até o momento em que é solicitada sua reprodução no método reproduzir.

Sabendo que o estado do objeto pode ser validado adiantado, logo no construtor, ou atrasado, no método que conclui a operação, um pode ficar na dúvida: devo validar adiantado ou atrasado? Cada caso é um caso. Como sugestão, considere sempre validar adiantado, porque é mais seguro, e avalie se deve-se validar atrasado judiciosamente, isto é, pondere bem os motivos para.

A lógica de validação é fortemente associada ao que se espera da interface do usuário. Por exemplo, nas interfaces que iniciam com um modelo vazio e exigem interação do usuário para completar, é tipicamente adequado validar atrasado. Por exemplo, considere um serviço de streaming. Se é preciso selecionar uma música para criar uma playlist ele já pressupõe que a playlist não estará, nunca, vazia. Se permite primeiro criar a playlist para depois adicionar as músicas, neste caso, ele inicia com um objeto vazio, “inválido”, por um tempo. Outros sistemas que podem precisar um objeto vazio e inválido inicial incluem editores de texto (documento em branco) e carrinhos de compra (como um carrinho vazio), entre outros.

Para fechar este tópico, a seguir está a mesma classe Playlist com validação adiantada (fail-fast).

// Playlist.java
import java.util.ArrayList;

class Playlist {
  
  String nome;
  final ArrayList musicas = new ArrayList();

  // construtor: obrigatório nome e uma música
  Playlist(String nome, String musica) {
    this.nome = nome;
    this.adicionarMusica(musica);
  }

  void adicionarMusica(String musica) {
    musicas.add(musica);
  }

  void renomear(String novoNome) {
    this.nome = novoNome;
  }

  void reproduzir() {
    for (String musica : this.musicas) {
      System.out.println("Reproduzindo " + musica);
      // player.play(musica); // não temos um player, ainda :/
    }
  }
}
// Main.java
class Main {
  public static void main(String[] args) {
    // Playlist nada = new Playlist(); // não compila sem um nome e música
    Playlist u2 = new Playlist("Favoritas do U2", "Numb");
    u2.adicionarMusica("Where the Streets Have no Name");
    u2.adicionarMusica("City of Blinding Lights");
    u2.reproduzir(); // reproduzindo Numb, ...
  }
}

4.5 Considerações

O controle do estado dos objetos é uma tarefa essencial e que merece muito atenção. Deve-se sempre procurar projetar classes que produzam objetos que não quebram. Algumas atitudes recomendadas são:

  • preferir estado constante sempre que possível: declarar atributos sempre como final (Java) ou const (C#), e tornar variáveis apenas aqueles que tem um motivo para mudar e regras de mudança claras;
  • fornecer um único ponto de entrada para alteração do estado: de modo que se possa rastrear a passagem da informação em uma sessão de debug, por exemplo, interceptando e logando o parâmetro de um método/construtor;
  • validar cedo (fail-fast) sempre que possível: validar no construtor e mover as regras para uma validação tardia apenas se houver um motivo claro e regras claras de progressão do estado até atingir um método que concluir a finalidade de um objeto.

4.6 Exercícios

Implementar Forno

Considere um Forno sofisticado de controle via app Android/iOS. É possível ligar, desligar, ajustar temperatura e outros detalhes. Os objetos variam segundo seu volume, tensão, potência e dimensões (na forma largura, altura e profundidade em cm). Implemente conforme especificação a seguir:

// Main.java
class Main {
  public static void main(String[] args) {

    Forno f = new Forno(45, 220, 1700, 66, 40, 54);
    System.out.println(f.volume == 45);
    System.out.println(f.tensao == 220);
    System.out.println(f.potencia == 1700);
    System.out.println(f.largura == 66);
    System.out.println(f.altura == 40);
    System.out.println(f.profundidade == 54);
    // esse atributo não consta no construtor
    System.out.println(f.temperatura == 0);
    // todos esses atributos devem ser constantes,
    // as atribuções a seguir não podem compilar
    // verifique e comente-as
    f.volume = 450;
    f.tensao = 2200;
    f.potencia = 17000;
    f.altura = 400;
    f.largura = 660;
    f.profundidade = 540;

    Forno forno = new Forno(84, 220, 1860, 61, 58, 58);
    System.out.println(forno.volume = 84);
    System.out.println(forno.tensao = 220);
    System.out.println(forno.potencia = 1860);
    System.out.println(forno.altura = 58);
    System.out.println(forno.largura = 61);
    System.out.println(forno.profundidade = 58);

    System.out.println(forno.temperatura); // 0
    System.out.println(forno.temperatura == 0); // true
    forno.aumentarTemperatura();
    System.out.println(forno.temperatura); // 50
    System.out.println(forno.temperatura == 50); // true
    forno.aumentarTemperatura();
    System.out.println(forno.temperatura); // 100
    System.out.println(forno.temperatura == 100); // true
    forno.aumentarTemperatura();
    System.out.println(forno.temperatura); // 150
    System.out.println(forno.temperatura == 150); // true
    forno.aumentarTemperatura();
    System.out.println(forno.temperatura); // 200
    System.out.println(forno.temperatura == 200); // true
    forno.aumentarTemperatura();
    System.out.println(forno.temperatura); // 220
    System.out.println(forno.temperatura == 220); // true
    forno.aumentarTemperatura();
    System.out.println(forno.temperatura); // 250
    System.out.println(forno.temperatura == 250); // true
    forno.aumentarTemperatura();
    System.out.println(forno.temperatura); // 300
    System.out.println(forno.temperatura == 300); // true
    forno.aumentarTemperatura(); // já está no máximo
    System.out.println(forno.temperatura); // 300
    System.out.println(forno.temperatura == 300); // true
    // reduzindo
    forno.diminuirTemperatura();
    forno.diminuirTemperatura();
    forno.diminuirTemperatura();
    System.out.println(forno.temperatura); // 150
    System.out.println(forno.temperatura == 150); // true
    // desligando direto
    forno.desligar();
    System.out.println(forno.temperatura); // 0
    System.out.println(forno.temperatura == 0); // true
    // já está desligado
    forno.diminuirTemperatura();
    System.out.println(forno.temperatura == 0); // true
  }
}

Projetar e implementar TV

Considere um aparelho de televisão. Cada uma tem um fabricante, modelo, tamanho e resolução. Além disso, a operação da TV é bem simples, permitir aumentar e baixar o volume, numa escala de 0 a 100%, e mudar o canal, suportando a UHF apenas e indo, então, do canal 2 ao 69.

Dada essa especificação, projete (com pseudo-código ou descritivo textual) e implemente uma classe TV, que guarde as características mencionadas, respeitando a imutabilidade e os métodos com as operações descritas.

Escreva pelo menos 20 Casos de Teste, para situações comuns e excepcionais.

Desafio: implementar o mudo, ir para canal e voltar canal.

Notes

1cada escolha possível é elencada e pesada segundo seus prós e contras em uma balança e não há uma decisão perfeita onde se ganha em todos os quesitos, é preciso escolher o que pode-se perder com mais segurança, é preciso selecionar o tradeoff.

2regras da linguagem de programação para definir o conjunto de palavras (símbolos) permitidos e suas combinações.

3o WolframAlpha é excelente para testar procedimentos (e depois objetos/métodos) que envolvam matemática!

4Existe uma corrente a favor de primeiro criar o teste unitário e então implementar o código que cumpra o especificado. O nome desta técnica é Test-Driven Development (TDD, em português desenvolvimento guiado por testes). Para saber mais a respeito experimente essa leitura http://tdd.caelum.com.br/.

5C é uma linguagem procedimental criada em 1972 e usada até hoje (o ++ em C++ é uma analogia a um “incremento” da C). A maioria dos sistemas embarcados, núcleos de sistemas operacionais como Windows, Linux e Mac OS, por exemplo, são escritos em C ou C++.

6Até é possível definir estado constante em JS e Python com algumas “manobras”, mas que deixam claro que não é um recurso “natural”.

7Os exemplo a seguir estarão na linguagem Java, e cabe apenas investigar se há e qual keyword está disponível para declarar uma propriedade constante na linguagem que pretendes programar.

8Tá, eu sei que tu podes encher a garrafa até transbordar e que passaria a capacidade nominal, shhhh, não bugue o exemplo!