Você está na página 1de 18

Os 4 pilares da programação orientada à

objectos.

Classes e objetos

Em linguagens orientadas a objeto, nós organizamos a maior parte do nosso


código em estruturas chamadas classes.
Você pode entender uma classe como sendo inicialmente um molde, molde este
que geralmente representa alguma estrutura do mundo real com a qual nosso código terá
que lidar. Vamos imaginar que estejamos trabalhando com uma aplicação que lida com
carros…
Provavelmente, nossa aplicação irá lidar com carros e, por causa disso, precisaremos de
um molde para definirmos o que é um carro dentro do nosso código. Esse molde seria
responsável por estabelecer o que é um carro e o que pode fazer um carro.
Se pararmos para pensar, nós podemos falar que um carro pode ser caracterizado
pelos seguintes itens:

 Marca;
 Modelo;
 Cor;
 Placa;
 E várias outras características…

Também podemos dizer que um carro pode ter as seguintes ações:

 Ligar;
 Acelerar;
 Desligar;
 E várias outras ações…

Veja que, quando falamos do que um carro é, estamos falando de suas


características, ou seja: falamos do que caracteriza e define um determinado carro. Já
quando falamos do que um carro pode fazer, estamos falando das ações que um carro
pode desempenhar.
Trazendo para termos técnicos, nós podemos chamar as características do carro
de atributos, euquanto nós chamamos as ações de métodos. No final, os métodos e
atributos ficam agrupados em uma classe.
Nós podemos representar estes atributos e métodos através de uma linguagem de
modelagem chamada UML (Unified Modeling Language). A UML prevê alguns
diagramas que visam auxiliar o processo de modelagem de um software. Entre estes
diagramas, nós temos justamente o diagrama de classes.
Perceba que a partir deste molde, nós podemos especificar vários carros…
Poderíamos ter um Fiat Línea prata com a placa ABC-1234, um Volswagen Gol preto
com a placa DEF-4567 ou mesmo um Hyundai HB20 branco com a placa GHI-8901…
Todos eles são carros, já que derivam do mesmo molde: todos eles têm marca, modelo,
cor e placa, além de poderem ser ligados, desligados, freados e acelerados, atributos e
métodos todos estabelecidos pela classe Carro. Nesse caso, o Fiat Línea, o Volkswagen
Gol e o Hyundai HB20 são objetos, pois foram “fabricados” a partir do molde que
definimos, que é a classe Carro. Objetos são como variáveis que criamos para utilizar
as nossas classes, seja para definir seus atributos, como também para invocar seus
métodos. É o encadeamento coordenado entre escritas e leituras de atributos com a
invocação de métodos que dá a tônica de uma aplicação escrita com uma linguagem
orientada a objetos.
Se fôssemos utilizar o Java para representar nossa classe e nossos objetos de uma
maneira primitiva, teríamos o código abaixo.

// Pacotes e demais estruturas omitidas para clareza...


public class Carro {
public String modelo;
public String marca;
public String cor;
public String placa;

public void ligar() {


System.out.println("O veículo ligou!");
}

public void desligar() {


System.out.println("O veículo desligou!");
}
}

// ...
Carro gol = new Carro();
gol.modelo = "Gol";
gol.marca = "Volkswagen";

Carro linea = new Carro();


linea.modelo = "Línea";
linea.marca = "Volkswagen";

gol.ligar();
gol.desligar();
linea.ligar();
linea.desligar();

No exemplo acima, temos a classe Carro fazendo o papel de nosso molde. As


variáveis gol e linea são objetos que são do tipo Carro ou, utilizando os termos técnicos
corretos, gol e linea são instâncias da classe Carro. Por derivarem da classe Carro, estes
objetos têm todos os atributos e métodos previstos pela classe Carro.

Encapsulamento

Ainda levando em consideração o exemplo com a classe Carro, poderíamos


imaginar um indicador para verificarmos se o carro está ligado ou não. Isso até daria
mais “qualidade” à nossa classe Carro: nós poderíamos, por exemplo, garantir que o
carro só pudesse ser acelerado ou freado caso estivesse ligado. Esse indicador ainda
poderia ser modificado pelos métodos ligar()e desligar()…
Partindo dessa idéia, nossa classe Carro ficaria da seguinte maneira:

public class Carro {

public String modelo;


public String marca;
// ...
public boolean ligado;

public Carro() {
ligado = false;
}

public void ligar() {


ligado = true;
System.out.println("O veículo ligou!");
}

public void desligar() {


ligado = false;
System.out.println("O veículo desligou!");
}
}

Veja que, além de adicionarmos um atributo chamado ligado do tipo booleano (truepara


ligado ou false para desligado), nós temos ainda o trecho de código abaixo:

public Carro() {
ligado = false;
}

Esse trecho de código é um construtor. O construtor é invocado quando inicializamos


um objeto a partir de uma classe. Nós, de maneira geral, invocamos o construtor quando
chamamos a criação da instância com a palavra-chave new. Sendo assim, quando temos
o código abaixo…

Carro gol = new Carro();

… nós estamos justamente chamando este método construtor. Ao reescrevermos este


método, estamos impondo uma “personalização” na inicialização dos objetos a partir da
classe Carro: todo carro será criado já possuindo o indicador ligado como false, ou seja,
o carro já começa como desligado por padrão.
Agora, poderíamos definir, por exemplo, um método chamado acelerar() em
nosso Carro. Nós podemos verificar o atributo ligado para “controlar” melhor este
método: um carro, obviamente, só pode ser acelerado caso esteja ligado.
Nosso código de exemplo ficaria da seguinte maneira:

public class Carro {

public String modelo;


public String marca;
// ...
public boolean ligado;

public Carro() {
ligado = false;
}
public void ligar() {
ligado = true;
System.out.println("O veículo ligou!");
}

public void desligar() {


ligado = false;
System.out.println("O veículo desligou!");
}

public void acelerar() {


if (!ligado){
throw new Exception("O carro não pode ser acelerado,
pois ele está desligado."); // Erro: o carro está desligado!
}
System.out.println("O carro foi acelerado");
}
}

Assim, poderíamos ter a nossa classe Carro sendo utilizada da seguinte maneira:

Carro gol = new Carro();


System.out.println(gol.ligado); // Vai imprimir "false" por causa do
construtor personalizado
gol.modelo = "Gol";
gol.marca = "Volkswagen";
// gol.acelerar(); Se essa linha for descomentada, o código gerará um erro,
pois estamos tentando acelerar um carro desligado
gol.ligar();
System.out.println(gol.ligado); // Vai imprimir "true", pois o método ligar()
foi chamado
gol.acelerar();
gol.desligar();
System.out.println(gol.ligado); // Vai imprimir "false", pois o método
desligar() foi chamado

Aparentemente, nossa classe Carro está funcionando corretamente. Mas, temos um


problema: o atributo ligado é acessível para todo mundo, da mesma maneira que os
atributos modelo e marca por exemplo. Isso quer dizer que nós podemos “deturpar” o
comportamento da classe Carro… Nós poderíamos, por exemplo, alterar manualmente o
conteúdo do atributo ligado antes de chamarmos o método ligar(), permitindo acelerar
um carro que estivesse em tese desligado:

Carro gol = new Carro();


System.out.println(gol.ligado); // Vai imprimir "false" por causa do
construtor personalizado
gol.ligado = true; // Atributo que define se o carro está ligado ou não
alterado "na mão"
gol.acelerar(); // Agora a linha não causará erro, mesmo que o método ligar()
não tenha sido chamado

Isso certamente é uma situação problemática, pois agora nosso carro dá uma brecha para
funcionar de maneira diferente de como ele foi planejado.

Para corrigirmos isso, precisamos recorrer a um pilar da orientação a objetos:


o encapsulamento. O encapsulamento visa esconder atributos e métodos de nossas
classes que não deveriam ser acessados por outras estruturas. É exatamente o que
precisamos: o atributo ligado deveria ser acessível em tese só pela própria classe Carro,
o que permitiria somente aos métodos ligar() e desligar() alterarem o indicador de
funcionamento do carro da maneira correta. Isso evitaria que nós acessássemos o
atributo do lado de fora, causando a falha no código que estamos discutindo aqui.
O encapsulamento nas linguagens orientadas a objetos é definido através de algo
que chamamos de atributo de visibilidade. Estes atributos de visibilidade estabelecem
justamente o quão acessível nossos atributos e métodos são com relação às demais
estruturas do nosos código. De maneira geral, temos três atributos de visibilidade
básicos e comuns às linguagens orientadas a objeto em geral:

 public: a estrutura é visível a partir de qualquer lugar no código, inclusive em


outras classes fora a classe que define o atributo ou método em si;
 private: a estrutura é visível somente pela classe que define a estrutura em si.
Estruturas externas, como outras classes, não conseguem acessar o método ou
atributo que esteja marcado com este atributo de visibilidade;
 protected: a estrutura é visível somente na classe-mãe e nas classes-filhas.

Se considerarmos a nossa classe Carro, podemos ver que o problema relatado


acontece porque o atributo ligado está definido como public, o tornando acessível em
qualquer lugar. Vimos que esse é o problema, pois o atributo ligado não poderia ser
acessível a partir de qualquer lugar: ele deveria ser acessível somente dentro dos
métodos ligar() e desligar(), ambos dentro da classe Carro. O nosso atributo ligado não
está encapsulado.
Poderíamos o encapsular se o tornássemos private, fazendo com que ele fosse
acessível somente dentro da classe Carro.

public class Carro {

public String modelo;


public String marca;
// ...
private boolean ligado;

public Carro() {
ligado = false;
}

public void ligar() {


ligado = true;
System.out.println("O veículo ligou!");
}

public void desligar() {


ligado = false;
System.out.println("O veículo desligou!");
}

public void acelerar() {


if (!ligado){
throw new Exception("O carro não pode ser acelerado,
pois ele está desligado."); // Erro: o carro está desligado!
}
System.out.println("O carro foi acelerado");
}
}

Assim, o erro que víamos antes não acontecerá mais, pois o atributo ligado agora só é
acessível dentro da própria classe Carro.

Carro gol = new Carro();


gol.ligado = true; // Essa linha causará um erro de compilação, pois o
atributo "ligado" não é mais acessível externamente

Agora, podemos dizer que o atributo ligado da classe Carro está encapsulado.


Se quiséssemos pelo menos ler o valor do atributo ligado externamente (já que alterá-lo
externamente estaria completamente errado), poderíamos criar um método que
devolvesse o valor do atributo ligado.

public class Carro {


public String modelo;
public String marca;
// ...
public boolean ligado;

public Carro() {
ligado = false;
}

public void ligar() {


ligado = true;
System.out.println("O veículo ligou!");
}

public void desligar() {


ligado = false;
System.out.println("O veículo desligou!");
}

public void acelerar() {


if (!ligado){
throw new Exception("O carro não pode ser acelerado,
pois ele está desligado."); // Erro: o carro está desligado!
}
System.out.println("O carro foi acelerado");
}

public boolean estaLigado() {


return ligado;
}
}

Com o método acima, poderíamos pelo menos verificar externamente se o carro está
ligado ou desligado.

Carro gol = new Carro();


System.out.println(gol.estaLigado()); // Vai imprimir "false" por causa do
construtor personalizado
gol.modelo = "Gol";
gol.marca = "Volkswagen";
gol.ligar();
System.out.println(gol.estaLigado()); // Vai imprimir "true", pois o método
ligar() foi chamado
gol.acelerar();
gol.desligar();
System.out.println(gol.estaLigado()); // Vai imprimir "false", pois o método
desligar() foi chamado
Este tipo de método é geralmente chamado de método de acesso, já que ele provê um
tipo de acesso indireto a um atributo encapsulado. Com relação ao encapsulamento, nós
temos dois tipos de métodos de acesso basicamente:

 get: métodos que permitem ver o valor de um atributo;


 set: métodos que permitem alterar o valor de um atributo.

Nós poderíamos falar que o método estaLigado() é um método get, pois ele permite a


nós lermos algo que está encapsulado dentro da classe Carro.
É uma prática recorrente em linguagens orientadas a objeto (principalmente no Java)
envolver todos os atributos com métodos de acesso do tipo get e set, evitando o acesso
direto aos atributos. Apesar de ser uma prática comum, é importante dizer que só o fato
de utilizarmos métodos de acesso não garante o encapsulamento de nenhuma estrutura.
O que garante este encapsulamento é a definição de visibilidade correta de cada um dos
atributos e métodos e o estabelecimento dos métodos de acesso de maneira correta. Por
exemplo: não criamos um método set para o atributo ligado, pois ele só pode ser
alterado pela própria classe.
Se fôssemos aplicar esta regra a nossa classe Carro, nós teríamos o seguinte
código:

public class Carro {

private String modelo;


private String marca;
// ...
public boolean ligado;

public void setModelo(String modelo) {


this.modelo = modelo;
}

public String getModelo() {


return modelo;
}

public void setMarca(String marca) {


this.marca = marca;
}

public String getMarca() {


return marca;
}
public Carro() {
ligado = false;
}

public void ligar() {


ligado = true;
System.out.println("O veículo ligou!");
}

public void desligar() {


ligado = false;
System.out.println("O veículo desligou!");
}

public void acelerar() {


if (!ligado){
throw new Exception("O carro não pode ser acelerado,
pois ele está desligado."); // Erro: o carro está desligado!
}
System.out.println("O carro foi acelerado");
}

public boolean estaLigado() {


return ligado;
}
}

Herança
O reaproveitamento de código e a possibilidade de se evitar código duplicado
são objetivos das linguagens orientadas a objetos. Vamos imaginar que agora nós
precisamos criar uma classe para definir um outro tipo de veículo, como uma bicicleta.
Uma bicicleta possui atributos em comum com um carro: ambos possuem marca e
modelo, por exemplo. Além disso, ambos não deixam de ser um tipo de veículo.

Se fôssemos escrever o código para definição da classe Bicicleta sem considerar


a classe Carro, nós teríamos o seguinte código:

public class Bicicleta {

private String modelo;


private String marca;
public void setModelo(String modelo) {
this.modelo = modelo;
}

public String getModelo() {


return modelo;
}

public void setMarca(String marca) {


this.marca = marca;
}

public String getMarca() {


return marca;
}

public void acelerar() {


System.out.println("A bicicleta foi acelerada.");
}
}

Acabamos duplicando todo o código relativo aos atributos marca e modelo nas


classes Carro e Bicicleta, o que pode ser bem ruim em termos de manutenibilidade do
código a longo prazo. Mas, nós temos uma maneira de evitar essa duplicidade: nós
podemos utilizar o conceito de herança.
Nesse exemplo, se fôssemos aplicar o conceito de herança, nós teríamos três
classes:

 A classe Carro, com tudo que um carro possui de características e ações;


 A classe Bicicleta, com tudo que uma bicicleta possui de características e ações;
 Uma nova classe chamada Veiculo, com tudo que existe de comum entre carros e
bicicletas.

No exemplo acima, para que as classes Carro e Bicicleta conseguissem usufruir das


estruturas comuns estabelecidas na classe Veiculo, elas precisariam herdar a
classe Veiculo.
Poderíamos representar esta relação entre as classes Veiculo, Carro e Bicicleta com a
UML da seguinte maneira:
Também poderíamos definir as classes Veiculo, Carro e Bicicleta da seguinte maneira:

public class Veiculo {

private String modelo;


private String marca;

public void setModelo(String modelo) {


this.modelo = modelo;
}

public String getModelo() {


return modelo;
}

public void setMarca(String marca) {


this.marca = marca;
}

public String getMarca() {


return marca;
}

public void acelerar() {


// ???
}

public class Carro extends Veiculo {

public boolean ligado;


public Carro() {
ligado = false;
}

public void ligar() {


ligado = true;
System.out.println("O veículo ligou!");
}

public void desligar() {


ligado = false;
System.out.println("O veículo desligou!");
}

public void acelerar() {


if (!ligado){
throw new Exception("O carro não pode ser acelerado,
pois ele está desligado."); // Erro: o carro está desligado!
}
System.out.println("O carro foi acelerado");
}

public boolean estaLigado() {


return ligado;
}
}

public class Bicicleta extends Veiculo {

public void acelerar() {


System.out.println("A bicicleta acelerou!");
}

Veja que tudo que é comum entre as classes Carro e Bicicleta foi para a classe Veiculo.


As classes Carro e Bicicleta estão agora herdando a classe Veiculo para reaproveitar
estas estruturas comuns, além de que um carro e uma bicicleta são tipos de veículos. É
importante que, quando formos empregar a herança, exista essa relação de “ser” ou
“estar” entre as classes.
Nesse caso, nós podemos fazer com que Carro herde Veiculo porque um carro é um
veículo; assim como também podemos fazer com que Bicicleta herde Veiculo, pois uma
bicicleta é um tipo de veículo. Com a herança, nós evitamos a duplicidade de código e
facilitamos a manutenção, além de manter a coerência, desde que a regra do “ser/estar”
seja devidamente implementada.
Quando temos a herança sendo utilizada, as classes podem assumir um dos dois papéis:
 Classe-mãe, classe-base ou super-classe: é a classe que serve de base para as
demais classes. Em nosso exemplo, a classe Veiculo é uma super-classe;
 Classe-filha ou sub-classe: é a classe que herda outra determinada classe. No nosso
exemplo, as classes Carro e Bicicleta são sub-classes.

Abstração
Quando estamos lidando com a orientação a objetos, é muito comum que nós
sempre tentemos escrever código baseado em abstrações, pois isso traz flexibilidade ao
código.

No exemplo anterior, nós esbarramos em um problema de abstração: todo veículo é


capaz de acelerar, independente de ser um carro ou bicicleta. Sendo assim, nós
não podemos remover o método acelerar() da nossa classe Veiculo, já que todo veículo
tem esse comportamento. Mas, a própria classe Veiculo não “sabe” como acelerar.
Quem sabe como acelerar é a classe Carro(que sabe como um carro acelera) e a
classe Bicicleta (que sabe como uma bicicleta acelera). Para resolver esta situação,
poderíamos tornar o método acelerar() abstrato: isso vai desobrigar a classe Veiculo a
definir uma implementação para este método (já que a classe Veiculo não sabe como
acelerar), mas obriga as classes-filha (no caso, as classes Carro e Bicicleta) a definirem
seus comportamentos de aceleração.
Além disso, se pararmos para analisar, não faz sentido nós instanciarmos objetos a partir
da classe Veiculo, já que ela serve somente como uma super-classe em nosso cenário.
Nós poderíamos instanciar objetos a partir das classes Carro e Bicicleta, mas não a
partir da classe Veiculo. Como queremos evitar que objetos sejam instanciados a partir
da classe Veiculo, já que ela deve ser somente uma classe-base, também podemos
definir a classe Veiculo como sendo uma classe abstrata.
O código ficaria da seguinte maneira:

// Classe abstrata (não pode ser instanciada)


public abstract class Veiculo {

private String modelo;


private String marca;

public void setModelo(String modelo) {


this.modelo = modelo;
}

public String getModelo() {


return modelo;
}

public void setMarca(String marca) {


this.marca = marca;
}

public String getMarca() {


return marca;
}

// Método abstrato: a classe Veiculo sabe que ela tem que acelerar,
mas não sabe como fazer isso.
// A responsabilidade passa a ser das classes-filha
public abstract void acelerar();

public class Carro extends Veiculo {

public boolean ligado;

public Carro() {
ligado = false;
}

public void ligar() {


ligado = true;
System.out.println("O veículo ligou!");
}

public void desligar() {


ligado = false;
System.out.println("O veículo desligou!");
}

// Aqui, a classe Carro define como ela deve exercer o ato de


acelerar
@Override
public void acelerar() {
if (!ligado){
throw new Exception("O carro não pode ser acelerado,
pois ele está desligado."); // Erro: o carro está desligado!
}
System.out.println("O carro foi acelerado");
}

public boolean estaLigado() {


return ligado;
}
}
public class Bicicleta extends Veiculo {

// Aqui, a classe Bicicleta define como ela deve exercer o ato de


acelerar
@Override
public void acelerar() {
System.out.println("A bicicleta acelerou!");
}

Com a utilização da abstração nesse caso, sabemos que qualquer novo tipo de veículo
que for criado a partir da classe Veiculo terá que obrigatoriamente implementar o
comportamento de aceleração. Isso faz muito sentido, já que todo veículo tem que ser
capaz de acelerar.

NOTA: Em POO, a abstração é o processo de esconder os detalhes de implementação


de uma aplicação.

Um exemplo claro do conceito de abstração seria o funcionamento de um carro. Quando


acionamos ele para ligar, não precisamos saber quais passos ele faz para colocar o
motor em funcionamento. Quando acionamos o freio, não precisamos saber todos os
mecanismos que são acionados para fazer o carro frear. Apenas sabemos o que cada
objeto ou função do carro produz como resultado.

Polimorfismo
Linguagens orientadas a objetos ainda prevêem o suporte para criação de
estruturas polimórficas. Estruturas polimórficas são estruturas que conseguem mudar
seu comportamento interno em determinadas circunstâncias. Essa variação
comportamental pode acontecer por algumas formas, como através da sobescrita de
métodos e através do LSP (Liskov Substitution Principle).
No exemplo anterior, nós temos um exemplo de polimorfismo através da sobrescrita de
métodos: nós temos a classe Veiculo, que possui o método abstrato acelerar(). O método
é abstrato porque a classe Veiculo só sabe que tem que conter este comportamento, mas
não sabe como esse comportamento deve ocorrer.
Mas, as classes Carro e Bicicleta foram obrigadas a implementar o
método acelerar() por herdarem a classe Veiculo. Cada uma dessas classes implementou
o método acelerar() da maneira mais adequada para cada um dos tipos de veículos.
Aqui, já temos um exemplo de polimorfismo: as classes Carro e Bicicleta são
polimórficas, pois possuem um ancestral comum (a classe Veiculo) que as obriga a
implementar o método acelerar(), mas cada uma delas implementa o mesmo método da
maneira mais correta para cada tipo de veículo.
Essa mudança de implementação não exigiu a mudança do código da
classe Veiculo, além de que a implementação dos métodos acelerar() em cada uma das
classes é isolada: a implementação do método acelerar() na classe Carro não afeta a
implementação do mesmo método na classe Bicicleta, e vice-versa.
Outro exemplo de polimorfismo seria através da aplicação do Princípio da Substituição
de Liskov (também conhecido como LSP - Liskov Substitution Principle). O LSP é
parte de um conjunto de cinco práticas de codificação conhecido como SOLID. Estes
princípios visam a produção de código com alta qualidade e alinhado com os princípios
das linguagens orientadas a objeto.
Para entender o LSP, considere o código abaixo:

Carro veiculo = new Carro();


// ...
veiculo.acelerar(); // Vai escrever "O carro foi acelerado"
// ...

O código não aparenta nada de diferente: temos um objeto chamado veiculo do


tipo Carro. Mas, pelo fato de Carro herdar a classe Veiculo, nós podemos deduzir que
um objeto do tipo Carro também pode ser considerado como sendo do tipo Veiculo;
afinal, todo Carro agora é também um Veiculo por causa da herança. Sendo assim,
podemos escrever o código acima da seguinte maneira:

Veiculo veiculo = new Carro();


// ...
veiculo.acelerar(); // Vai escrever "O carro foi acelerado"
// ...

Nós podemos definir o objeto veiculo como sendo do tipo Veiculo, mas o criar com


base em um Carro. Nesse momento, o objeto veiculo vai se comportar como um Carro.
No caso acima, dizemos que Veiculo é a abstração, enquanto Carro é a concretização.
Se quiséssemos trocar o tipo do nosso veículo para uma bicicleta, bastaria trocar a
concretização.
Todo o código abaixo continuaria funcionando normalmente, já que
uma Bicicleta também é um Veiculo. Isso torna nosso código muito mais flexível (se
quisermos alterar o comportamento do nosso código, basta trocar as
concretizaçãoes) e a prova de falhas de escrita de código (já que a abstração vai
garantir que o código que vier logo abaixo da definição da concretização vai
continuar funcionando de maneira transparente).

Veiculo veiculo = new Bicicleta();


// ...
veiculo.acelerar(); // Vai escrever "A bicicleta foi acelerada"
// ...

Neste exemplo, além de todas as vantagens que podemos notar no que diz
respeito à qualidade e manutenibilidade do código, podemos dizer que o
objeto veiculo é um objeto polimórfico, pois a concretização está sendo capaz de alterar
seu comportamento. Porém, todo o código subsequente não é afetado por essa troca.

Você também pode gostar