Novatos > Orientação a objetos > Parte 4
O objetivo deste artigo é continuar a seqüência de artigos sobre Orientação a Objetos e Delphi, adentrando-se nos conceitos relacionados ao encapsulamento.
Habilidade em programação com delphi em qualquer versão, conhecimento de Object Pascal, cabeça aberta, e ter lido o os outros artigos publicados anteriormente na home page do canal Delphi, partes I, II e III. Sugerimos ainda uma lida ao nosso artigo publicado sobre exceções, aos programadores que não conhecem o assunto.
Uma vez que lemos os três primeiros artigos já publicados, então já sabemos do que se tratam objetos, instâncias, classes concretas e abstratas, atributos, métodos estáticos e dinâmicos, construtores, destruidores, herança, polimorfismo, enfim, um monte de coisas. Mas ainda não sabemos o que é encapsulamento – o terceiro e último dos pilares da OO. E é dele que trataremos aqui.
Os objetos, nos sistemas de verdade – ao contrário dos exemplos didáticos que citamos até agora – costumam ser numerosos, relativamente grandes, e repletos de atributos e métodos. Isto pode torná-lo de difícil compreensão caso não haja um bom critério de privacidade para as partes do objeto consideradas internas. E é isto que propõe o que estamos a tratar aqui sob o título de encapsulamento.
O encapsulamento em resumo é a maneira de fazer com que um objeto exponha apenas o que é necessário ser exposto, mantendo como próprio e privado tudo aquilo que não seja de interesse de outros objetos que o vejam por fora. Desta maneira poderemos separar "os dois mundos", a que nos referimos no título – o mundo exterior ao objeto e seu mundo privado.
Mantendo uma vida particular
Imagine um aparelho de TV. Ele normalmente é constituído de um painel, um visor, e alguns circuitos internos. O painel é o seu controle, acessível ao seu usuário através de alguns botões. De igual maneira, o visor procura exibir, diretamente para o telespectador, as imagens que lhe são requeridas. Estas duas partes são obviamente necessárias ao acesso manual, satisfazendo muito bem o seu propósito.
Entretanto, determinados circuitos e controles não estão disponíveis no painel ou na caixa deste equipamento. Eles estão isolados dentro do aparelho, a fim de evitar o acesso manual, que poderia ser perigoso tanto para quem tentasse acessá-lo, quanto poderia comprometer o bom funcionamento do aparelho de TV.
Por isso é importante que os objetos tenham alguns de seus elementos privados, dentre aqueles que são declarados nos seus atributos e métodos. Com esta privacidade será possível manter a compreensão, com um código mais limpo e mais legível. Além disso, o acesso a determinados atributos ou métodos perigosos dos objetos será restrito.
O objetivo do encapsulamento é tratar o objeto como uma caixa preta, que faz certas coisas, e disponibiliza para que os seus programadores – que neste caso, devem ser tratados mais como "usuários" – saibam o que ele faz. Como o objeto faz, não seria de todo interessante saber. E isto é um ponto fundamental para se reduzir a complexidade do sistema à visão do programador.
Pois eis que o exemplo do aparelho de TV é uma boa analogia quanto ao encapsulamento dos objetos. Além do acesso total não ser muito conveniente, os usuários não costumam estar devidamente qualificados para acessar as centenas de circuitos dentro do aparelho. Da mesma forma, os outros objetos de um programa certamente não estão qualificados ao acesso total de todos os objetos.
Classificando os membros por visibilidade
Os membros de uma classe são classificados por três escopos principais de visibilidade: públicos (public), privados (private) e protegidos (protected). No Delphi especificamente surge um tipo chamado published, que é uma variante do público, mas que não está relacionado especificamente com a OO, e sim com os componentes da VCL. Não trataremos de published neste artigo.
Os atributos e métodos públicos são aqueles que estão disponíveis para tudo e todos acessarem, desde que tenham visibilidade do objeto. Eles são visíveis tanto externa quanto internamente ao objeto, e visam disponibilizar a comunicação do objeto com os demais elementos do modelo. Membros são públicos quando declarados numa seção public da classe a que pertence.
Os membros privados por sua vez são aqueles que somente o objeto deve saber, não devendo estar disponíveis para os objetos externos. Eles devem ser declarados na seção private da classe. As seções podem ser declaradas em qualquer ordem, desde que dispostas sempre antes dos membros relativos à sua declaração. Vejamos um exemplo:
FMotor: TMotor;
procedure AlimentarBombaDeCombustivel;
procedure LiberarValvulaDeInjecao;
procedure InjetarCombustivel;
procedure AcionarPastilhas;
procedure DispararArranque;
Pneus: array [1..5] of TPneu;
procedure Acelerar;
procedure Freiar;
DispararArranque;
InjetarCombustivel;
procedure TCarro.Acelerar;
procedure TCarro.Freiar;
AcionarPastilhas;
procedure TCarro.AlimentarBombaDeCombustivel;
{aqui deverá ser implementado o código referente à alimentação da bomba de
combustível}
procedure TCarro.InjetarCombustivel;
AlimentarBombaDeCombustivel;
LiberarValvulaDeInjecao;
procedure TCarro.AcionarPastilhas;
{aqui deverá ser implementado o código referente ao acionamento das pastilhas
de freio}
procedure TCarro.DispararArranque;
{aqui deverá ser implementado o código referente ao disparo do motor de
arranque}
O que fizemos no exemplo? Bom, declaramos alguns métodos e atributos privados e outros públicos, de modo que o que está privado – dentro da cláusula private – não estará acessível para uso por qualquer outro elemento do sistema, senão dentro do próprio código da classe – diferentemente do que está declarado no public, que pode ser usado por qualquer um que consiga enxergar o objeto. Vale ressaltar que por convenção declaramos os atributos privados começando-os pela letra "F" – do inglês Field (campo).
Note que existe uma razão para isso. Se o leitor perceber bem, todos os métodos declarados públicos estão mais relacionados a funcionalidades externas do objeto, ou seja, algo que o usuário deve saber: "o que" deve pode feito. Lá estão os métodos Ligar, Freiar e Acelerar do carro. Qual motorista não deveria fazer estas três coisas?
Em contrapartida, nada do que foi declarado em private deve ou precisa ser visto externamente ao carro. Somente ele deve saber que precisa injetar combustível ao acelerar, ou acionar as pastilhas de freio ao se freiar. Isto é "como" deve ser feito, e não há por que isto interessar ao mundo de fora.
Um terceiro tipo de visibilidade dos membros de uma classe é o protegido. Os membros protegidos, declarados na seção protected, são privados ao objeto tanto quanto os declarados na seção private, exceto por um detalhe. Eles são visíveis a todas as classes descendentes, como se fossem membros privados delas.
Assim, seria interessante que instâncias das classes TCarroPasseio e TCarroEsporte, descendentes de TCarro, pudessem acessar alguns de seus elementos privados, uma vez que elas também pertencem a esta classe. Agora vamos reformular o cabeçalho da nossa classe:
A razão desta mudança é que o desenvolvedor desta classe imaginou o quanto seria útil disponibilizar os métodos InjetarCombustivel e DispararArranque para as classes descendentes dela, e o fez ao declará-los como protegidos. Note que os métodos declarados em private realmente não precisariam serem disponibilizados, a não ser que isto fosse julgado importante.
Propriedades: atributos a uma visão externa
A orientação a objetos diz, em seu conceito geral, que todo e qualquer atributo deve ser acessado apenas através de métodos públicos. Daí, dispor atributos publicamente não seria uma boa prática de programação.
O objetivo desta restrição é garantir a consistência do estado do objeto. Convenhamos que qualquer objeto com um bom encapsulamento saberá, através destes métodos, exatamente a melhor forma de manter a leitura ou escrita de seus dados. Estes métodos - popularmente conhecidos como gets e sets – pertencem a uma filosofia bem respeitada por programadores Java em geral.
A Borland, por sua vez, adotou uma medida alternativa e interessante de promover a segurança de seus atributos: o uso de propriedades. Esta medida se trata, na verdade, de encapsular os atributos sob métodos gets e sets. Isto dará ao desenvolvedor-usuário desta classe a "sensação" de que ele está manipulando os atributos internos do objeto diretamente, sem que de fato esteja. Vejamos:
FAtributo: Integer;
FInicializado: Boolean;
procedure SetAtributo(const Value: Integer);
function GetAtributo: Integer;
property Atributo: Integer read GetAtributo write SetAtributo;
MeuObjeto: TMinhaClasse;
procedure TMinhaClasse.SetAtributo(const Value: Integer);
if (Value < 0) or (Value > 100) then
raise ELimiteException.Create('Este valor está fora dos limites' +
' estabelecidos para este objeto');
FAtributo := Value;
function TMinhaClasse.GetAtributo: Integer;
if not FInicializado then
raise EEstadoException.Create('Este objeto ainda não' +
' foi inicializado');
Result := FAtributo;
if MeuObjeto.Atributo <= 20 then
ShowMessage('Este valor está muito baixo. Vamos aumentá-lo.');
MeuObjeto.Atributo := 35;
No código acima, note que declaramos um atributo privado qualquer, FAtributo. Este atributo é algo de íntimo do objeto, e deve respeitar a algumas regras internas da classe, que nem todos os que acessam a classe necessariamente devem saber. Mas querendo ou não, terão que respeitar. Propriedades são apenas uma forma de abstração do código, e o compilador, na realidade, entende como se você estivesse chamando os métodos GetAtributo e SetAtributo diretamente.
Nota-se que nossa classe garante a integridade de leitura e escrita do atributo FAtributo, através do uso de exceções. O acesso dele está sendo feito pela propriedade Atributo, que é entendida pelo compilador como o acesso aos métodos GetAtributo e SetAtributo. No primeiro, garante-se que nenhuma instância desta classe terá FAtributo lido, sem que Finicializado esteja com o valor True. No segundo, garante-se que nunca lhe será atribuído um valor fora do limite de 0 a 100.
Dica: A IDE do Delphi possui um atalho para automaticamente criar os métodos Gets e Sets, declarando-os na propriedade, e criando o seu corpo automaticamente na seção implementation. Para isto, basta declarar a propriedade com o tipo, e pressionar [control + shift + C].
É de se observar ainda que as propriedades não necessariamente precisam representar um atributo-imagem pelo lado de fora, uma vez que se tratam puramente de métodos de entrada e saída. Elas podem ser apenas uma abstração de atributos, que podem ser físicos ou não. O importante é que o programador-usuário entenda que está acessando uma propriedade do objeto. Assim criaremos propriedade virtual, e o programador usuário da classe simplesmente acredita que está acionando um atributo. Exemplo:
procedure TDatabase.SetActive(const Value: Boolean);
function TDatabase.GetActive: Boolean;
Result := IsConnected;
A sintaxe da declaração dos membros de leitura e escrita nas propriedades admite tanto métodos quanto atributos de mesmo tipo da propriedade. Não serão estudados mais detalhes sobre propriedades aqui, porque elas não pertencem à filosofia do paradigma OO. Para ver este assunto mais detalhadamente, nós recomendamos consultar a ajuda do Delphi.
Um outro conceito relevante quanto à visibilidade de métodos é a sobrecarga. Sobrecarga – antes que o leitor pergunte – é a declaração de dois métodos diferentes numa mesma classe, com o mesmo nome. O que deve diferir os métodos são os seus parâmetros.
Se existe uma funcionalidade na sua classe que executa coisas diferentes em dois métodos com parâmetros diferentes, mas que tenham um mesmo propósito, então aparentemente não há impecílios para que o leitor dê o mesmo nome aos métodos. Basta sobrecarregá-los. Para isso, usa-se a diretiva overload. A sobrecarga pode inclusive facilitar a vida de quem irá usar a sua classe.
TColecao = class
procedure Adiciona(const Item: TItem); overload;
procedure Adiciona(const Colecao: TColecao); overload;
A partir do código de exemplo acima, podemos usar a chamada de método Adiciona tanto para adicionar itens quanto para adicionar Coleções. Nota-se que o procedimento executado será diferente em cada um dos métodos. A maneira com que se adiciona um TItem é diferente de como se adicionar um TColecao. Mas, como mais um caso de fins versus meios, o propósito é o mesmo.
Visibilidade entre classes distintas
A rigor, nenhuma classe enxerga os atributos privados de outra. Assim diz a regra da boa vizinhança entre as classes, na OO. Sabe-se que o ideal é que cada classe conheça o menos possível dos elementos internos da outra na OO, mas existe um porém. Um não, dois, pois existem situações em que pode ser necessário se extender a visibilidade entre as classes.
O primeiro caso refere-se aos membros protegidos. Ora convenhamos que se um método é declarado protegido, isto significa que, muito embora não seja desejável que ele seja visto por fora, é interessante que as subclasses façam uso dele. Portanto, o método Tcontrol.Click, embora não seja público, é necessário que seja acessível tanto por um TEdit, quanto por um TButton ou por qualquer subclasse descendente de TControl.
O segundo porém tem a ver com classes amigas. A "amizade" entre as classes é algo conhecido no Ansi C++. Ela se caracteriza por estabelecer, através da palavra reservada Friendly, uma espécie de relacionamento entre duas ou mais classes, de modo que garanta a visibilidade de algumas de suas partes privadas entre elas.
No Delphi, esta cumplicidade entre as classes não é declarada por meio de palavras reservadas, mas ela existe. Duas classes passam a ter visibilidade mútua de seus atributos privados quando são declaradas na mesma unit. Daí, se for interessante compartilhar atributos privados entre duas ou mais classes, as declare sempre dentro de um mesmo arquivo ".pas".
Entretanto, o acúmulo de classes diversas numa mesma unidade de código pode acabar comprometendo a segurança de seus objetos. Daí uma boa prática de programação que propõe dar ênfase ao encapsulamento é procurar declarar as classes no máximo de arquivos fontes separados o possível. Se no seu modelo não existe um motivo bem forte para que uma classe enxergue os elementos privados de outra, então as separe.
Público, privado ou protegido: quando devo declarar?
O discernimento do grau de visibilidade é algo trivial para um programador OO experiente. Mas ainda não é para quem não está habituado com o encapsulamento. O critério de quando declarar um atributo como público ou como privado é adquirido com a prática. Mas sempre sugerimos tentar discernir, dentre as funcionalidades e propriedades dos objetos, o que pode ser separado entre o que fazer dos detalhes de como se fazer. Os primeiros são forte candidatos a serem privados ou protegidos.
Um critério alternativo adotado por alguns desenvolvedores para definir o encapsulamento dos objetos, segue a frase "tudo é privado até que se prove o contrário". Assim, declaram-se todos os métodos como privados, até que uma subclasse realmente precise acessá-los, para que se tornem protegidos – ou até que realmente seja preciso acessá-los publicamente, e se tornarem públicos. Esta filosofia, apesar de ser muito questionada, garante um bom nível de segurança do objeto.
Quanto aos atributos, recomendamos fortemente que nunca sejam declarados públicos, sempre procurando usar propriedades para a sua leitura e escrita externas. Esta é uma boa prática de OO.
Novamente volto a dizer que não pretendemos, com este artigo, ser citado na bibliografia da monografia de ninguém. Pretendemos somente clarear a idéia sobre Orientação a Objetos – especialmente sobre o contexto de encapsulamento e outros pontos relacionados com a visibilidade – aos iniciantes. Na medida do possível, tentamos fazer com que o mito de bicho de sete cabeças a que a OO tanto sofre preconceito seja quebrado.
O encapsulamento é uma maneira bastante eficiente de diminuir a complexidade dos sistemas orientados a objeto, e garantir a sua segurança e consistência, principalmente quanto ao uso indevido de atributos e métodos dos objetos.
Outros tópicos relacionados à Orientação a Objetos ainda estão por vir, dentro de uma mesma seqüência. Pretendemos – em contradição com o que tínhamos planejado – publicar ainda mais três artigos sobre o assunto, abordando tópicos essenciais da OO, sempre aplicando-os em código.