quarta-feira, 3 de novembro de 2010

Um diálogo sobre orientação a objetos

Sempre que leciono Programação Orientada a Objetos, quando estou falando de encapsulamento, eu mostro uma pequena história para desmistificar a lenga-lenga de que "encapsulamento é ter atributos privados". Fiz, para publicar no blog, uma versão em forma de diálogo. Tentei minimizar o tom professoral, mas o texto não nega a origem. Segue.


- Aí cara, a galera criou um fork de orientação a objetos, e uma das coisas que ficou como exercício foi criar um programa orientado a objetos que calcule o fatorial de um número, mas sem utilizar estruturas de repetição. Essa é fácil, implementei rapidinho! Vê aí se ficou legal:

- Este programa calcula certinho o fatorial, mas não faz o que foi pedido, porque isso aí é tudo menos orientado a objetos.

- Mas como não? Eu utilizo uma classe e um método! Logo, estou programando orientado a objetos, não?

- Nada disso! O fato de utilizar abstrações e terminologia de OO como classes e métodos não torna automaticamente um programa orientado a objetos. É muito comum ver programas completamente procedurais escritos em linguagens orientadas a objetos. Logo de cara, pense comigo: há sentido em um objeto fatorial no contexto deste programa? Você criou um método chamado fatorial e uma classe Fatorial. Não tem uma coisa errada aí? Afinal, fatorial é uma classe ou um método?

- Bom, que fatorial é um método está claro. Agora eu já não estou mais tão seguro em relação a ser uma classe.

- Então, se fatorial é um método, é só pensar qual é o objeto que tem a responsabilidade de oferecer esse método. Objetos pilha tem métodos push e pop, que inserem e retiram um elemento de si mesma. Objetos conta bancária oferecem saques e depósitos em si próprios. Pensando assim, qual objeto deve oferecer o cálculo do fatorial?

- Ahhhhh!!! O número é que deve oferecer um fatorial! É tão óbvio! Então fica assim:

- Mas isso continua do mesmo jeito. Como você disse, procedural. Foi só uma mudança estética, certo?

- Mais ou menos. Nomes são importantes e nos ajudam a pensar mais claramente a respeito dos problemas. Pensa aí, como é que a gente pode melhorar mais isso aí? Se eu chamar um new Numero(), o que este meu objeto vai representar? Qual é o seu significado? Não tá faltando nada aí não?

- Ah, claro! Qual é o número do meu Numero... Um atributo!

- Certo! Manda ver então!

- Realmente, está ficando cada vez mais com cara de código OO. Mas ainda está estranho, pois eu posso fazer algo assim:

Numero numero = new Numero(3);
numero.fatorial(5);

- Não faz o menor sentido eu pedir a 3 para calcular o fatorial de 5.

- E o que isto significa?

- Já estou entendendo: o raio do método ainda é procedural, e ele é que está bagunçando a história toda...

- Esse método é procedural. Se ele fosse escrito em C, seria exatamente igual (tirando o public, claro!). Na programação procedural, rotinas processam dados. É exatamente isto que o nosso método fatorial faz. Na OO, a comunicação se dá por troca de mensagens, não por fluxos de dados. Ou seja: quando você pede a 3 para calcular um fatorial, deve ser o fatorial de quem?

- De 3, claro.

- Então...

- Rala peito, parâmetro!

- Agora ferrou! Como fazer a parada funcionar?

- Você ainda está pensando de modo procedural. Ali dentro do método, você quer o fatorial de quem?

- De valor - 1.

- Então por que você está pedindo o fatorial de valor novamente? Lembre-se que ali você está chamando fatorial sobre o próprio objeto, this. Se você quer o fatorial de valor - 1, não deveria chamar o fatorial sobre o objeto que representa valor - 1?

- Peraí, agora pegou aqui! Deixa eu pensar... Tá, tudo bem, eu entendi a sua ideia, mas não sei como implementar.

- Como é que você obtém um objeto Numero que seja valor - 1?

- Fazendo new Numero(valor - 1).

- Então está pronto!

- Ahhh claro!!! Lá vai!

- Depois que você vê pronto é muito, muito simples!! Agora sim!

- A gente pode parar por aqui se você quiser.

- Como assim se eu quiser. Tem mais?

- Imagine que eu estou usando seu código em uma aplicação, e começo a criar códigos como:

long resultado = numero.fatorial();

- Aí um belo dia eu faço o seguinte código:

Numero numero = new Numero(50);
long resultado = numero.fatorial();

- E o resultado que obtenho é obviamente errado: -3258495067890909184. Estourou o tamanho do tipo long.

- Poderia usar double?

- Até poderia, mas já começa a virar gambiarra, concorda? Fatorial, matematicamente, é uma função aplicada sobre um número natural, que retorna um número natural. Meter números reais na história não vai cheirar nada bem. De qualquer modo, a gente pode pensar com razoável grau de segurança que ninguém vai querer um fatorial de 50, e depois, se alguém pedir, a gente troca. Um dos preceitos da OO é o encapsulamento, sob o qual mudanças na implementação interna de uma classe não alteram o mundo exterior a ela. Como acreditamos que nosso programa está orientado a objetos direitinho... Acreditamos, certo?

- Sim, eu acho que agora está ok, o atributo privado, só o método público lá fazendo só o que deve fazer... Eu acho que está ok.

- Podemos liberar esta classe para produção então?

- Manda ver!!

- Agora imagine que você escreveu este código e todos os 200 desenvolvedores da empresa estão utilizando esta classe em 30 aplicações.

- Que responsa!

- Pois é! Aí chega uma demanda dos usuários de algumas das aplicações por fatoriais de números na casa dos 200.

- A gente pode usar, sei lá, BigInteger para representar o número.

- Você está confiando no encapsulamento da classe pra fazer isso? Tenta lá.

- Legal, só que isto vai quebrar TODAS as milhares de chamadas ao método fatorial (estamos imaginando aqui que fatorial é uma funcionalidade muito popular, ok?). Tudo bem, aqueles que querem fatoriais gigantescos nem vão reclamar tanto, mas e os que não têm nada a ver com isso? E ainda são obrigados a importar mais uma classe! E imagine que coisa fede ainda mais se criarmos um método getValor(). A solução seria criar um método Frankenstein, tipo bigFatorial e manter o fatorial normal? Manter duas classes diferentes? E cadê o desgraçado fidumaégua do encapsulamento que não te salvou? Era tudo mentira isso de "mudanças na implementação não alteram o mundo externo"?

- É aquele papo, de uma "mudancinha à toa" pro cliente ser um pesadelo de manutenção pros desenvolvedores.

- Isso! E muitas vezes (como no nosso caso atual) isto é fruto de mau design.

- E como resolver isso? Eu não tenho idéia.

- Na verdade há um problema conceitual na nossa classe. Ou, dizendo de outro modo, não está suficientemente OO.

- Como assim?

- Primeiro, há um vazamento no encapsulamento: o tipo de retorno de fatorial. Ele espelha exatamente a implementação interna, o modo como a classe Numero armazena o atributo valor. Se houver futuramente uma alternativa a BigInteger e quisermos eliminá-lo, estaremos escolhendo novamente entre quebrar os clientes ou fazer gambiarras de duplicar métodos. Ter simplesmente "atributos privados" não é o bastante. Linguagens como Python não têm suporte a membros privados e não deixam de ter encapsulamento por isto. Encapsulamento é um conceito, não uma palavra-chave como muita coisa que se lê por aí faz supor.

- Certo.

- Este vazamento de encapsulamento é causado pelo problema conceitual de que falei. Quando criamos uma classe, a única representação daquele conceito no nosso modelo passa a ser a classe. Nós criamos uma classe para números, mas representamos números de duas formas: objetos Numero e objetos BigInteger. Deveríamos usar apenas uma. Pense: o que um fatorial retorna?

- Um número.

- E o que é um número no mosso modelo?

- Entendi!! Um objeto Numero!

(começa a escrever o código)

- Mas se eu quiser realmente encapsular por completo o tipo utilizado eu vou ter que...

- Reescrever todos os operadores? Sim, não tem outro jeito neste caso.

- E então?

- A classe Numero agora é um número de pleno direito dentro do software. Agora qualquer coisa que ela faz retorna outro objeto Numero. Tudo está realmente encapsulado.

- Mas continua um problema, aquilo que você falou sobre ter duas representações. Internamente só vamos ter uma, mas o cliente da classe vai continuar vendo duas: o nosso objeto Numero e os números (int, long, etc) que a linguagem Java oferece. Além disso, ter que reescrever todos os operadores dá um trabalhão. Ali eu só reescrevi os que foram necessários, em um caso real todos os outros teriam que ser reescritos também.

- Bom, aí chegamos aos limites da linguagem Java em termos de orientação a objetos. Se você quiser, podemos parar por aqui.

- Como assim "se você quiser"? Eu não acredito que ainda tem mais!?

- Bom, podemos falar, por exemplo, de Ruby e colocar a orientação a objetos em um nível bem mais alto, tanto de simplicidade quanto de praticidade e poder. Partiu?

(mas isso fica pra outro dia, ainda que, provavelmente, não em outro diálogo...)

14 comentários:

Argentino disse...

Aqui vai um fatorial em erlang

-module(fatorial).
-export([calcular/1]).

calcular(0) ->
1;

calcular(N) ->
N * calcular(N-1).

Tarsis Azevedo disse...

Ja pensou em transformar isso numa paletra?! Na PythOnCampus?!

:D

Rodrigo Manhães disse...

@argentinomota

Espere só a continuação dessa bagaça em Ruby, rapaz... :-P


@tarsisazevedo

É uma ótima ideia! Mas não tem mais lugar pra palestra na grade. Vou pensar em uma versão curta pra lightning talk.

Tarsis Azevedo disse...

Show de bola!

Mas tipo, tu vai fazer tipo um teatro mesmo!?

se precisar de ajuda, #TamoJunto

Anônimo disse...

E qual a vantagem disso?

Magno Machado Paulo disse...

Muito interessante o seu exemplo.... Confesso que pra mim encapsulamento estava muito ligado a ter atributos privados, mas agora pude ver que é muito mais que isso.

Parabens!

Tarsis Azevedo disse...

@Anonimo

A vantagem é PROGRAMAR DIREITO!!!!!!!

E usar todos os recursos que o paradigma OO traz.

Abraços.

Rodrigo Manhães disse...

@Anônimo

Escrever código que, ao mudar a implementação interna, não quebra todos os clientes não é uma vantagem? Será que o texto está tão mal escrito assim?

@Magno

Valeu!! Volte sempre!

Magno Machado Paulo disse...

Eu até entendo o ponto do Anonimo.
Acho que é inegavel que a implementação no final ficou bem mais complexa. Tudo bem que tem a vantagem de realmente ser encapsulada, tudo bem que em outras linguagens (como o Ruby que você citou) dá para fazer melhor ainda, mas ainda assim é mais complexo.

A grande questão é: Será que compensa? Compensa gastar mais tempo desenvolvendo, ter um código mais complexo, implementar mais métodos, para que talvez no futuro eu não precise quebrar o código de ninguem ao usar essa classe?

Até por que, querendo ou não, você criou mais coisas para o usuário da sua classe se preocupar: O Java já tem os tipos int, long, BigInteger, etc, e agora tem mais um, apenas por causa do fatorial.

Eu ia perguntar isso no meu primeiro comentario mas aí me toquei que o fatorial foi só um exemplo. Em situações mais complexas tenho certeza que vale muito a pena se esforçar um pouco mais para fazer um design melhor.

Diante disso, pergunto: Você chegou a usar essa implementação de fatorial em um projeto real? Se sim, compensou?

Mari Reis disse...

Pô, caraca! Dá pra fazer uma história em quadrinhos irada de programação e cheia de gírias! Demorou.... rsrsrs (muito legal o texto e a forma!)

Rodrigo Manhães disse...

@Magno

Como agilista, eu concordo com o que você disse: não se deve programar por especulações e sim atendendo às necessidades atuais.

Porém, aquele é um exemplo didático, apenas, cuja intenção é demonstrar técnicas mas sem querer ser um guia para situações reais. O contexto do post é um contexto de estudo. A quantidade de encapsulamento (ou de qualquer outra coisa) usada realmente vai ser dada pela necessidade concreta. Entenda o post como um exercício mental. Mas se o caso for, digamos, um software matemático, eu penso que o investimento em uma classe Numero (que não teria apenas o fatorial, claro) é muito válido.

Sobre Ruby: em Ruby uma implementação destas fica, ao mesmo tempo, muito mais simples (tão simples quanto a boa e velha implementação procedural) e, ao mesmo tempo, mais conceitualmente correta, em termos de OO, do que a implementação Java vista aqui.

Rodrigo Manhães disse...

@mariounaum

HQ e OO, deve ficar bom isso... hehehe

Valeu!!

Unknown disse...

Muito bom o post! Parabéns e obrigado pela aula!

Rodrigo Manhães disse...

Edgard,

valeu, volte sempre!