3. MODELO DE OBJETOS
“De certo modo, programar é como pintar. Você começa com uma tela em branco e certas matérias-primas bem básicas. Você usa uma combinação de ciência, arte e habilidades de ofício para determinar o que fazer com elas.”
Andrew Hunt
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.
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. O 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 pseudocódigo em português estruturado:
// 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 pseudocó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:
// 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
// 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
# 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 a sintaxe for desconsiderada. 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 pseudocódigo de Copo:
// 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.
// 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 capacidade 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 pseudocó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 obrigatoriamente 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 NomeClassena 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 ou app.ts
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.
Considere um 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 é considerado 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.