Você está na página 1de 11

Encapsulamento

Nós vimos no conteúdo teórico e nas aulas práticas como podemos criar
nossas classes no código que estamos desenvolvendo. Vimos como criamos
as nossas classes e, também vimos como representamos os atributos e os
métodos que uma classe pode conter.

Lembra-se de que, na orientação a objetos, vários objetos podem conversar


entre si através de “mensagens”? Essas mensagens são constituídas
basicamente a chamadas de atributos e métodos entre diferentes objetos que
podem agir em conjunto.

Agora, um outro ponto importante da orientação a objetos é termos controle


justamente sobre essas trocas de mensagem que podem ocorrer entre objetos.
Muitas vezes, não é interessante expormos todas os atributos e métodos de
uma classe para o mundo exterior, os expondo aos efeitos de mensagens
trocadas com outros objetos da aplicação. Muitas vezes, é necessário nós
escondermos certos atributos e métodos dos efeitos que as trocas de
mensagem entre objetos podem ocasionar.

O ato de ocultarmos estes métodos e atributos é o que chamamos


de encapsulamento.

Por que devo me preocupar com encapsulamento?

Vamos a um exemplo clássico da literatura: o exemplo da conta-corrente.

Se nosso software é um software bancário, nós fatalmente teremos a classe


conta-corrente em nossa aplicação. Uma conta-corrente pode ter o nome do
titular e o saldo da conta como atributos, além de ter os métodos sacar e
depositar.

Nós enfrentamos um problema em nossa classe conta-corrente neste caso:


nós temos que ter uma maneira de não permitir que o nosso atributo saldo seja
afetado por trocas de mensagens externas com outros objetos em nossa
aplicação. Se nós não escondermos o atributo saldo da classe conta-corrente
de ações externas, nós podemos ter um código errôneo em nossa aplicação
que altera de maneira incorreta o saldo das contas-correntes gerenciadas pela
aplicação que estamos escrevendo. Em nossa classe conta-corrente, a única
maneira que podemos ter de alterar o atributo saldo é através de chamadas
aos métodos sacar e depositar. Mas, nunca que o saldo poderia ser alterado de
maneira direta, sem passar por estes métodos. Desta maneira, nós precisamos
encapsular o atributo saldo da classe conta-corrente, prevenindo que o atributo
seja acessado de maneira direta ou mesmo modificado de maneira direta.
Atributos de visibilidade

No exemplo anterior, nós podemos esconder o atributo saldo da classe conta-


corrente modificando o seu atributo de visibilidade. Os atributos de visibilidade
definem o nível de acesso dos atributos e métodos de uma classe com relação
à outras classes e objetos.

Nós temos de maneira geral três atributos de visibilidade nas linguagens


orientadas a objetos. Mas, nesta altura, vamos abordar somente dois destes
três atributos:

 Public. Atributos e métodos declarados como públicos podem ser chamados,


acessados e modificados por objetos da própria classe e também por objetos
externos, sendo da mesma classe ou não;
 Private. Atributos e métodos declarados como privados não podem ser
chamados, acessados ou modificados por objetos externos, sendo eles da
mesma classe ou não. Somente o próprio objeto pode modificar os atributos
que são privados, assim como somente o próprio objeto pode invocar os
métodos que são declarados como privados.

No caso do nosso atributo saldo da classe conta-corrente, se quiséssemos o


encapsular, o correto seria nós o declararmos como sendo um atributo privado.
Já os métodos sacar e depositar deveriam ser públicos, para que os demais
objetos pudessem invocar estes métodos.

Métodos Acessores

Várias bibliotecas e frameworks exigem que as classes implementem os


métodos chamados acessores

De maneira geral, é considerada uma boa prática não expor os atributos de


uma classe de maneira direta. Ou seja: não é uma boa prática deixar os
atributos de uma classe como sendo públicos, para que possam ser acessados
de maneira direta. Segundo esta prática, o correto é manter os atributos como
privados.

Porém, como vimos, se tornarmos os atributos todos privados, eles não


poderão ser modificados nem lidos por outros objetos de nossa aplicação. Nós
precisamos ter uma maneira de expor estes atributos, porém, sem expor os
atributos em si de maneira direta. E é aí que entram os métodos acessores.

Nós temos basicamente dois tipos de métodos acessores:

 Método acessor get: é um método responsável por retornar o valor de um


determinado atributo;
 Método acessor set: é um método responsável por alterar o valor de um
determinado atributo.
Seguindo este pensamento, cada atributo terá dois métodos acessores: um
método get para que o valor possa ser lido; e um método set para que o valor
possa ser alterado. Porém, não é uma regra que todo atributo deverá de fato
conter os dois métodos acessores: você pode ter um atributo somente com um
método get associado (o que caracterizaria um atributo somente-leitura
externamente) ou até mesmo somente com um método set associado (o que
faria que o atributo só pudesse ser modificado externamente).

Encapsulamento, atributos de visibilidade e UML

A UML também prevê a definição de atributos de visibilidade. A representação


dos atributos de visibilidade na UML são, respectivamente:

 +: público;
 -: privado.

Lembra-se do nosso diagrama da classe Gato?

Perceba que na frente dos nossos atributos, temos a representação de que


eles são privados (-), enquanto os métodos possuem a representação de que
são públicos (+).

Herança

Na programação Orientada a Objetos, herança é o mecanismo pelo qual uma


classe obtém as características e métodos de outra para expandi-la ou
especializá-la de alguma forma. Ou seja, uma classe pode “herdar”
características, métodos e atributos de outras classes. Da mesma maneira,
uma classe transmite suas características para outras classes, tornando-as
suas herdeiras.

Sob o ponto de vista prático da orientação a objetos, a herança constitui um


mecanismo muito inteligente de aproveitar código. É através da herança que os
objetos podem compartilhar métodos e atributos. Assim, é possível criar uma
nova classe fazendo com que herde os métodos e atributos de outra classe,
tornando-a uma classe “filha” da classe que a gerou. A grande vantagem,
nesse caso, é a reutilização de todo o código já implementado na classe-pai,
restando apenas implementar os métodos e atributos que a diferenciem da
classe-pai.

Superclasses e subclasses

Quando aplicamos a herança na modelagem de nossas classes, nós passamos


a ter dois elementos envolvidos:

 Nós passamos a ter a superclasse ou classe-pai. A superclasse é a classe que


serve de base para que outras classes a herdem;
 Nós passamos a ter também a subclasse ou classe-filha. A subclasse é a
classe que herda atributos e métodos de uma superclasse.

Vamos ao seguinte exemplo: vamos imaginar que nós temos uma classe
Mamifero, que contém atributos comuns para todos os mamíferos, como por
exemplo: espécie e tempo de gestação, além do método mamar.

Agora, nós também podemos ter uma classe Cachorro. Perceba que a classe
Cachorro tem todos os atributos e métodos que a classe Mamífero tem, além
de possui atributos próprios (por exemplo: cor do pelo) e métodos próprios
(como por exemplo, latir).

Não faz sentido nós repetirmos o código que existe dentro da classe Mamifero
dentro da classe Cachorro. Na verdade, de maneira geral, repetição de código
não é um bom sinal dentro da aplicação, além de dificultar e muito a
manutenção da aplicação no futuro.

Como nós podemos resolver esta situação sem a repetição de código? Nós
podemos fazer com que a classe Cachorro herde a classe Mamífero. Dessa
maneira, a classe Cachorro conterá tudo que a classe Mamífero possui, só que
sem a repetição de código que iria ocorrer. Neste caso, a classe Mamífero
seria a nossa superclasse, enquanto a classe Cachorro seria a nossa
subclasse.

A herança é muito bem-vinda quando nós temos a relação de “ser” entre duas classes.
O exemplo acima ilustra este ponto perfeitamente: todo cachorro, no fundo, é um
mamífero também. Sendo assim, faz bastante sentido que a classe Cachorro herde a
classe Mamífero.

Por que a herança constitui um assunto tão


polêmico?
A herança facilita e muito a vida do desenvolvedor no que diz respeito à
manutenção do código. O problema é quando ela é aplicada de maneira
indiscriminada, fato recorrente entre os desenvolvedores. Muitas vezes, as
classes não apresentam aquela relação de “ser” e o desenvolvedor, de maneira
incorreta, aplica a herança entre as duas classes em questão. Essa associação
incorreta entre as duas classes certamente refletirá em problemas no futuro, já
que qualquer modificação feita na superclasse irá refletir diretamente na
subclasse. Se as duas classes na verdade não possuem nenhum tipo de
relação, pode ser que as modificações introduzidas na superclasse afetem de
maneira negativa as subclasses associadas.

Outro ponto é que os desenvolvedores, muitas vezes, se esquecem de outros


artifícios que a orientação a objetos provê para resolução de alguns problemas,
como as interfaces e as sobrescritas e sobrecargas de métodos. Muitas vezes,
o que ocorre é que, nestes casos, a herança é aplicada de maneira incorreta,
dificultando a manutenção do código (justamente o contrário do que deveria
ocorrer).

Por fim, por muitas vezes, os desenvolvedores acabam criando o que


chamamos de “árvores de herança”. As árvores de herança se caracterizam
por um conjunto de classes que estão unidas por uma cadeia de herança. Nós
estamos falando, por exemplo, de uma classe D, que herda uma classe C, que
herda uma classe B, que herda uma classe A. Tome muito cuidado com
árvores com essa extensão, pois a manutenção do código destas classes é
extremamente complexa, já que qualquer um dos membros da árvore que for
alterado irá repassar suas alterações para todos os seus filhos. De maneira
geral, dois ou três níveis em uma árvore de herança são suficientes para a
resolução dos problemas de arquitetura de software

Quando devo, de fato, utilizar a herança?


A herança é muito bem-vinda quando nós temos a relação de “ser” entre duas
classes. O exemplo acima ilustra este ponto perfeitamente: todo cachorro, no
fundo, é um mamífero também. Sendo assim, faz bastante sentido que a classe
Cachorro herde a classe Mamífero.

O atributo de visibilidade “protected”


Em algumas situações, nós precisamos encapsular um atributo ou método para
que este não seja acessível de maneira direta por outros objetos, mas nós
precisamos que este atributo ou método seja visível nas superclasses e
também nas subclasses. Veja: nós não podemos tornar este atributo ou
método público, porque senão ele seria acessível em qualquer objeto da nossa
aplicação. Também não podemos o tornar privado, porque senão somente a
superclasse teria acesso. Nós precisamos de um meio termo entre o public e
o private.

Nestes casos em específico, podemos utilizar o atributo de


visibilidade protected. Membros protegidos são atributos ou métodos que só
são acessíveis nas superclasses e em suas respectivas subclasses. As vídeo-
aulas a seguir irão abordar situações onde precisaremos deste atributo de
visibilidade.
Herança e UML

No exemplo entre a classe Mamífero e a classe Cachorro, nós poderíamos


representar essa relação com o diagrama UML abaixo:

Perceba no diagrama acima que a representação da herança é a seta que liga


as classes Mamífero e Cachorro. No caso, de acordo com o sentido da seta,
nós sabemos que a classe Cachorro herda a classe Mamífero.

Nós temos ainda um outro nome que podemos dar para as classes Mamífero e
Cachorro. A classe Mamífero, que é nossa superclasse, também é chamada
de abstração; enquanto a classe Cachorro, que é a subclasse, também é
conhecida como concretização.

Herança simples ou herança múltipla?

A maioria das linguagens modernas (como o C#, PHP e o próprio Java)


suportam somente herança simples. Isso significa que uma subclasse só pode
possuir uma superclasse associada. Ou seja: uma classe só pode herdar de
uma única classe. Isso ocorre nas linguagens modernas basicamente para
facilitar a manutenção do código, além de prover mais segurança na execução
do código.

Outras linguagens, como o C++, suportam o que chamamos de herança


múltipla. Isso quer dizer que as classes, nessas linguagens, podem ter várias
superclasses associadas.

Nas linguagens de herança simples, é como se uma classe tivesse somente


uma mãe. Agora, nas linguagens de herança múltipla, é como se as classes
pudessem ter várias mães.
Interfaces

Por várias vezes, nós precisamos ter alguma maneira de especificar um


conjunto de métodos que um determinado grupo de classes deverá conter
obrigatoriamente. Nós conseguimos atingir este efeito com a utilização das
interfaces.

O que são interfaces?


Vamos pegar a especificação clássica da literatura sobre as interfaces. Na
orientação a objetos, as interfaces agem como contratos que são firmados
pelas classes.

Um contrato é composto por cláusulas, cláusulas que descrevem determinados


comportamentos que a pessoa que o assina deverá seguir. Caso a pessoa
deixe de cumprir uma ou mais cláusulas do contrato, a pessoa sofre algum tipo
de punição.

Pegando esta situação para a orientação a objetos, a estrutura que representa


o contrato em questão seriam as interfaces. As interfaces especificam as
assinaturas dos métodos que as classes que implementarem a interface em
questão deverão conter. Perceba que os métodos descritos pela interface
agem como se fossem as cláusulas do contrato.

Uma classe, quando implementa uma interface, se vê obrigada a implementar


os métodos descritos pela interface. Se a classe não implementar os métodos
previstos (o que seria o equivalente a uma pessoa não cumprir as cláusulas de
um contrato), o desenvolvedor recebe um erro de compilação, ou seja: o código
não será executado enquanto a classe não implementar todos os métodos
especificados pela interface.

Interfaces e UML
Vamos imaginar a seguinte situação: nós temos uma classe para representar
os animais, a classe Animal. Nós sabemos que, de maneira geral, todos os
animais nascem, crescem, reproduzem-se e morrem. Este é um
comportamento padrão de todos os animais que existem na Terra. Sendo
assim, precisamos ter uma maneira de forçar que as classes que representem
os animais possuam os métodos em questão (nascer, crescer, reproduzir e
morrer). Nós podemos fazer isso criando uma interface, que podemos chamar
de SerVivo, por exemplo. Dessa maneira, a nossa classe
Animal implementaria a interface SerVivo.

Na UML, essa relação entre a interface SerVivo e a classe Animal poderia ser
representada da seguinte maneira:
Perceba que, pelo sentido da seta tracejada (que representa a implementação
da interface), indica que a classe Animal implementa a interface SerVivo e,
obrigatoriamente, a classe Animal terá os métodos nascer, crescer, r

Composição, agregação e cardinalidade

No final das contas, nossas aplicações serão constituídas por várias classes,
que servirão de molde para vários objetos interagirem em nossa aplicação. Nós
sempre teremos objetos interagindo entre si em uma aplicação escrita com
uma linguagem orientada a objetos. O ponto é que estas interações podem ser
classificadas em dois grupos: composições e agregações. Além disso, a
quantidade de objetos envolvidos na interação nos permite descrever a
cardinalidade dessa relação.

Composição e agregação

Com relação ao relacionamento entre os vários objetos de nossa aplicação,


nós podemos ter relações de composição e relações de agregação.

A composição, como o próprio nome diz, ocorre quando uma classe é


composta por objetos de outra classe. Isso quer dizer que uma das classes
envolvidas depende de maneira direta da existência da outra classe para que
ela também possa existir. Neste caso, nós geralmente temos duas definições
para as classes envolvidas na composição: a classe forte, que domina o
relacionamento e que é importante para que a outra classe possa existir; e a
classe fraca ou anêmica, que é a classe que depende da existência da outra
classe para existir. Um exemplo clássico de composição é a relação entre uma
classe que representa uma nota fiscal e a classe que representa os itens da
nota fiscal. A nota fiscal, para existir, depende explicitamente da existência dos
itens da nota fiscal. Sendo assim, temos aqui uma relação de composição: a
nota fiscal é composta por itens da nota fiscal.

Acima, nós temos a composição entre notas fiscais e itens de notas fiscais de
acordo com a UML.

A relação de agregação é um pouco diferente. Ela ocorre quando dois ou mais


objetos se relacionam na intenção de um objeto “complementar” informações
no outro objeto. Os dois objetos não necessariamente dependem um do outro
para existir; mas, quando unidos, apresentam um sentido mais completo e
descritivo sobre uma determinada situação.

Um exemplo clássico é a relação entre uma classe Pessoa e uma outra classe
Endereco. Ambos os objetos podem existir de maneira independente um do
outro (pois não são somente pessoas que possuem endereço). Mas, quando
unimos objetos da classe Pessoa com objetos da classe Endereco, nós
acabamos tendo uma pessoa mais “completa”: uma pessoa com um endereço.
O mesmo vale para o endereço: quando ele é unido com uma pessoa, ele
acaba também “fazendo mais sentido”. Quando dois ou mais objetos têm uma
relação nesse sentido, nós temos uma agregação.
Acima, nós temos a representação UML de uma relação de agregação.

Cardinalidade de relações entre objetos

Quando temos uma relação entre objetos, nós podemos definir a cardinalidade
deste relacionamento. Esta cardinalidade está relacionada à quantidade de
elementos que estão envolvidos na relação, quer seja uma agregação, quer
seja uma composição.

Nós podemos ter, basicamente, os seguintes tipos de cardinalidade no


relacionamento entre objetos:

Cardinalidade Descrição

0/1 .. N (*) Um objeto pode estar associado a zero ou vários objetos

1 .. 1 Um objeto está associado a um único objeto

N (*) .. N (*) Vários objetos podem estar associados a vários objetos

A cardinalidade de uma relação é representada com a notação da tabela acima


em cada uma das extremidades da relação. Vamos ao exemplo da relação
entre notas fiscais e itens das notas fiscais.
Perceba que, nas extremidades da notação da composição neste caso, nós
temos indicadas as cardinalidades em cada extremidade do relacionamento.
No caso acima, nós temos duas indicações:

 Uma nota fiscal pode conter no mínimo 1 e no máximo vários (N ou *) itens de nota
fiscal associados;
 Um item de nota fiscal pode estar associado somente a uma única nota fiscal.

Geralmente, para definirmos a cardinalidade de uma relação entre objetos, nós


adotamos a maior cardinalidade. No caso acima, um dos lados possui uma
cardinalidade única (um item de nota fiscal está atrelado somente a uma nota
fiscal). Mas, o outro lado possui uma cardinalidade múltipla (uma nota fiscal
pode conter um ou mais itens). No caso de cardinalidades múltiplas, nós
assumimos a maior cardinalidade para definir a cardinalidade do
relacionamento. Então, neste caso, nós podemos afirmar que temos uma
relação de cardinalidade 1 para N.

Você também pode gostar