Linguagens e Arquiteturas
Blog de Ronaldo Moreira sobre Linguagens de Programação, Arquiteturas e Tecnologia

POO sem O

Março 3rd, 2008 de Ronaldo Moreira

O título quer dizer “Programação Orientada a Objetos sem Objetos”. É um exercício muito interessante e elucidativo para entendermos os princípios da abstração OO. Como essa abordagem nos aproxima da natureza dos problemas, e de que trabalho nós estamos sendo poupados usando uma linguagem com suporte a objetos e seus conceitos.

O texto pode parecer meio básico a principio para quem já conhece as estruturas citadas. Peço ainda licença aos adeptos de C, que é uma linguagem conhecida pelo extenso uso de ponteiros, mas vou usar aqui exemplos em Pascal por ter uma sintaxe mais didática.

Registros e Classes
Registros (Records em Pascal ou Structs em C) são estruturas de dados compostas de outros dados, a base histórica para entendermos o conceito de Classe, do ponto de vista prático.
Quando escrevemos um registro, estamos modelando um conceito sobre alguma coisa. Inserimos nesse registro algumas características distintas do conceito, na forma de variáveis tipadas.
No exemplo, definimos um registro simples, um modelo de algo que seja capaz de guardar o nome e a data de nascimento de um indivíduo. Não guardamos os dados diretamente no registro, ele não serve para isso. Se fosse assim, teríamos de escrever um registro para todos os indivíduos que quiséssemos guardar. Ao invés disso, usamos variáveis.
As variáveis P1 e P2 representam indivíduos que têm seus próprios nomes e datas de nascimento, segundo o conceito de Pessoa. Com isso, podemos agora guardar dados de duas pessoas. Para um milhão de indivíduos, podemos definir um milhão de variáveis, ou um vetor com um milhão de elementos (ex.: PX).
Variáveis ocupam memória desde o momento da carga do programa. Variáveis estruturadas ocupam o equivalente à soma das suas partes. Ao invés de projetar um vetor de um milhão, talvez seja melhor começar por um elemento e alocar espaço à medida do necessário.

Ponteiros e Referências
Assim como registros estão para classes, os ponteiros estão para referências para as instancias das classes, ou objetos.
Um ponteiro é um tipo de dado que contém um endereço de memória, um número que indica uma posição na memória da máquina. Quando esse endereço coincide com o de algum dado do programa, podemos chamá-lo de referência para o dado. Um dado pode ter inúmeras referências apontando para ele.
Podemos dizer que um ponteiro não possui dados, na verdade ele aponta para algo que os tenha. Um ponteiro puro ou não-tipado (Pointer ou Void, ex.: P3) não “conhece” a natureza do dado a que ele se refere. Para manipular o dado através do ponteiro, é necessário fazer o casting para um tipo conhecido como um inteiro, uma string ou um registro, ou “tipar” o ponteiro (ex.: P4, P5).
Ponteiros podem referir dados já existentes, mas também é possível usá-los como variáveis de alocação dinâmica (ex.: P5).

Neste ponto podemos fazer algumas analogias ao modelo OO. O New seria a chamada ao construtor da classe Pessoa, P5 a referência para o objeto criado, e o Dispose a chamada ao destruidor do objeto.

Herança
Um candidato a extensão do nosso conceito de Pessoa pode ser o conceito de Funcionário, por exemplo. Para simplificar nossos exemplos, adicionamos apenas o atributo Salário. A implementação com registros usa composição, com o registro Pessoa sendo uma variável de Funcionário (AsPessoa). Ao criar uma nova instancia de Funcionário, alocamos por tabela uma variável do tipo Pessoa. O endereço de AsPessoa coincide com o da própria instancia de Funcionário, porque AsPessoa é a primeira variável do registro.
No exemplo, podemos fazer P5 apontar para F1 e manipulá-lo como uma Pessoa, pois há realmente uma instancia de Pessoa lá.

Dessa forma, Funcionário é uma especialização de Pessoa, e Pessoa é uma abstração ou super classe de Funcionário. A variável AsPessoa pode ser usada como referência à super classe (inherited em Delphi ou super em Java).

Procedimentos e Métodos
Métodos podem ser simulados em registros através de ponteiros para procedimentos ou funções. Ponteiros deste tipo podem ser invocados com passagem de parâmetros e dado de retorno.
Acrescentamos um ponteiro do tipo PReajustador ao registro Funcionário. Esse ponteiro pode referenciar qualquer procedimento que possua a mesma assinatura. Diferentes implementações da rotina de reajuste representam um mecanismo de Polimorfismo. O usuário do registro Funcionário chama sempre o mesmo procedimento: F.Reajusta(F, 1.10), mas o resultado depende da implementação escolhida. O primeiro parâmetro deve ser sempre a instancia objeto da ação (Self em Delphi, ou this em Java, C++).

O endereço inicial do ponteiro Reajusta é dado pelo Construtor do Funcionário. O construtor é uma função que aloca um nova instancia de Funcionário e inicializa as suas variáveis, inclusive os ponteiros para procedimentos. Com isso, já dá para imaginar como faríamos um Override no método Reajusta. Escrevemos um descendente de Funcionário, do mesmo jeito que fizemos com Pessoa, aí criamos um construtor para ele que, após invocar o construtor antigo, redireciona o ponteiro Reajusta para outro procedimento.

O conceito de Encapsulamento já pode ser encontrado aqui, ao passo que o usuário não influencia diretamente na forma de cálculo, apenas usa os métodos que a instancia oferece.

Questões de Visibilidade são um pouco mais difíceis de engolir. Muitas implementações de POO-O precedem os nomes de variáveis e procedimentos com um ou dois underscores (“_”). Essa simples convenção sinaliza membros protegidos ou privados e inibe usuários educados de mexer ali.
Há outras formas de definir a visibilidade dos membros de forma mais restritiva. Internamente ao pacote, usa-se um registro com todos os campos nomeados normalmente. Externamente, usuários recebem uma versão do mesmo registro com os campos escondidos em outras estruturas, comunmente vetores de bytes de tamanho equivalente. Estes vetores recebem nomes sugestivos, como Filler1, Filler2, Undefined, Reserved etc.

Acredito mesmo que tudo que pode ser feito com objetos, possa também de alguma forma ser escrito usando registros e ponteiros. O problema é que dá um trabalho hercúleo fazer isso e, no final, vamos olhar para o código e enxergar um monstro obtuso escondendo o real sentido do programa. Eliminar esses “monstros” é o mérito indiscutível da abordagem por objetos.

Observando códigos-fonte antigos escritos em C sem OO, constatamos o uso extensivo de técnicas POO-O. Várias centenas de registros com suas variáveis e procedimentos imitando a orientação a objetos podem ser conferidos nos fontes do Interbase 6.
Vale a pena conferir esse trabalho.

Enviado em Paradigmas | Sem comentários »

Hello World! Em várias línguas

Fevereiro 8th, 2008 de Ronaldo Moreira

Tive a idéia de escrever um pouco sobre aquela primeira experiência que temos ao encontrar uma nova tecnologia, o “Hello World!”. Aí, pesquisei no Google um título exatamente igual ao deste artigo, e não é que apareceram quase 200 links! É difícil ser original nos tempos de globalização. Mas, vamos lá.

A Wikipédia define “Hello World” em informática como um programa escrito da maneira mais simples possível e com uma única funcionalidade: colocar uma frase de boas vindas em algum dispositivo, geralmente uma tela, mas também pode ser uma impressora, um arquivo em disco etc. É utilizado em tutoriais para introduzir conceitos de uma nova linguagem ou arquitetura. Na verdade, qualquer primeira aproximação de uma tecnologia aonde há um estímulo e a expectativa por uma resposta pode ser considerado um HW, como por exemplo, fazer acender um LED através da porta paralela do micro.
Programas de boas vindas também são úteis para “testes de sanidade”, para saber se o ambiente de desenvolvimento está corretamente instalado e configurado. É sempre bom verificar se o basicão funciona antes de sair na porrada com o micro quando o seu super ERP não roda.

Minha intenção aqui era descrever a implementação desses HW em algumas linguagens, para tentar medir a receptividade de cada uma em relação a novos programadores. Constatei que isso já tinha sido feito e o resultado ficou bem legal.
Encontrei um site com um ranking de implementações em quase 50 linguagens de programação.
A comparação é feita em termos de tamanho dos programas, consumo de CPU e consumo de memória. Muito interessante.

Há outro site ainda mais interessante chamado “The Hello World Collection“, sem ranking, mas acho que deve ter todas as linguagens do mundo lá, de A à Z. Inclui até um Hello feito na Máquina de Turing. Vale à pena dar uma conferida, especialmente no código-fonte dos programinhas. Rende umas boas gargalhadas.

Alguém aí já ouviu falar de uma linguagem chamada “Argh!”?
Meu Deus!

Enviado em Linguagens | Sem comentários »

Déjà vu

Fevereiro 7th, 2008 de Ronaldo Moreira

Vez por outra nos deparamos com algo aparentemente novo, mas que nos dá a sensação de já ter visto antes, um Déjà vu?!
Se você estiver acompanhando a evolução das linguagens, certamente já deve ter ouvido falar em Closures. Closures são blocos de código portáveis, e assemelham-se a funções que tem parâmetros e um dado de retorno. Você pode definir um desses blocos e guardá-lo em uma variável ou passá-lo como argumento para um método. Podemos rodar o código da Closure a qualquer momento através da variável que o referencia.
O conceito de Closures não é exatamente novo. Ele foi proposto nos anos 60 e implementado pela primeira vez nos anos 70 numa linguagem chamada Scheme.
Várias outras linguagens também provêem maneiras de portar código de modo semelhante.
No antigo Pascal é possível definir tipos ponteiros especiais para procedimentos e funções com parâmetros e tipo de retorno. Ponteiros deste tipo podem referenciar qualquer procedimento que tenha a mesma assinatura e invocá-lo naturalmente. Esse é o mesmo mecanismo utilizado nos eventos de Delphi. Em C e C++ essa capacidade ficou conhecida como Callback, largamente utilizado no interfaceamento com bibliotecas de funções. Java inicialmente preferiu implementar esses Callbacks através de interfaces, ou seja, ao invés de passar uma referência a um procedimento como argumento, passa-se uma instância de uma interface que tenha uma assinatura para um propósito específico.
Recentemente, vi os termos “Code-Block” e “Iterator” como características da linguagem Ruby. Ainda não consegui descobrir ainda se essas características nasceram junto com o Ruby ou quando elas teriam sido adicionadas a essa linguagem, mas isso me fez lembrar os tempos em que programava no bom e velho Clipper.

Clipper nasceu meio às avessas como compilador para uma linguagem de manuseio de dados, sofreu muitas críticas, não era orientado a objetos e foi até mesmo questionado como linguagem de programação. Mas, especialmente a partir da versão 5.0 de 1990, adotou características muito interessantes que vejo hoje em dia serem cada vez mais difundidas e utilizadas através de outras linguagens:
* O compilador convertia o código-fonte em p-code (Byte-Code?!) e embutia uma máquina virtual (CLR, JVM?!) nos executáveis monolíticos que produzia;
* Variáveis dinâmicas com tipagem dinâmica (Ruby?!), já foram muito criticadas por gerarem problemas de runtime, como “Tipos incompatíveis” ou “Variável não encontrada”. Mas, tudo isso está em moda novamente, amparado pelos testes unitários;
* Possuia um estágio de pré-compilação que tornava possível resolver símbolos e comandos definidos pelo usuário. Um mecanismo tão poderoso que permitia até criar uma linguagem inteiramente nova dentro do próprio Clipper para propósitos específicos (DSL?!);
* Pedaços de código-fonte na forma funcional podiam ser guardados em blocos chamados Code-Blocks?!.

Essa ultima característica do Clipper, além de homônima, tem exatamente a mesma sintaxe dos Code-Blocks do Ruby. O bloco a seguir quando avaliado, retorna verdadeiro para parâmetros cujo quadrado seja maior que 30:

bloco1 - bloco1

No Clipper, utilizava-se a função “Eval” para rodar um bloco de código e retornar seu valor. Em Ruby, o bloco é um objeto que possui um método “call”. Os Iterators de Ruby são idênticos às funções de vetores do Clipper e, novamente, o paradigma OO é a diferença.
Em Ruby pode-se encontrar um item em um vetor através do método “find” do vetor passando como parâmetro o critério da busca na forma de um Code-Block, ou rodar um código para cada item de um vetor através do método “each”. Há equivalentes em Clipper, como segue:

Exemplo 1 - Busca uma canção pelo nome num vetor de canções em Ruby e em Clipper:

bloco2 - bloco2

bloco3 - bloco3

Exemplo 2 - Exibe cada elemento de um vetor em Ruby e em Clipper:

bloco4 - bloco4

bloco5 - bloco5

É sempre bom estudarmos um pouco de história da computação pra gente não ser pego com cara de bobo, para evitar o efeito “Nossa!”, diante de novos nomes para velhas idéias. Os Déjà vu?!
Recomendo fortemente consultas à Wikipédia, que tem se mostrado uma ferramenta de estudo confiável e imparcial. Parabéns.

Enviado em Linguagens | Sem comentários »

Relaxed Programming

Janeiro 29th, 2008 de Ronaldo Moreira

Não que seja “do meu tempo”, mas sempre que penso em performance lembro-me de histórias sobre os antigos cartões perfurados.

PunchedCard - Cartão Perfurado
Antes de qualquer coisa, este artigo não é saudosista e nem ecológico, eu apenas tento usar os exemplos do passado para comparar e fazer refletir sobre o modo como desenvolvemos software hoje em dia.
Bem, nos tempos do cartão perfurado havia máquinas grandes, porém com limitações severas de recursos como memória, periféricos e poder de processamento, e que perderiam feio frente a qualquer celular atual. Dentre outros vários recursos indispensáveis hoje em dia, essas máquinas não possuíam teclado. Uma forma de fazer o computador entender o que você queria que ele fizesse era através da programação via cartão (nada a ver com os cartões da programação XP).
Grosso modo, imagine-se escrevendo um programa bit a bit furando um cartão com um palitinho de dentes. Imagine que um cartão comporta apenas uma dezena de linhas de programação, e que na verdade para escrever um programa, você leva umas três semanas furando uns cento e vinte cartões, cuja ordenação obviamente é fundamental. Ao terminar de digitar, você leva os cartões para dar entrada um a um no computador. Se no meio do caminho, por infelicidade, o elástico que prendia os cartões arrebenta, todos os cartões se espalham no chão. Não há mais nada a fazer senão voltar para casa e arrumar tudo de novo.

Por causa de toda essa dificuldade, havia um cuidado muito maior na confecção dos programas, não havia muitas chances para erro, as soluções tinham que ser simples rápidas e econômicas. Acho que era mais ou menos como a palmatória, você sabia o quanto doía se errasse e por isso tinha que fazer certo da primeira vez. Os programas tinham que compilar e funcionar no cérebro antes de serem submetidos às máquinas.

Aposto que você não desenvolve assim. Você escreve qualquer coisa e aperta uma tecla para rodar, se der erro, você corrige e compila novamente, e repete esse ciclo até que o programa rode ou até que os testes sejam satisfeitos. Algumas pessoas ainda se incomodam com mensagens de advertência ou dicas do compilador, tipo “Ei, você esqueceu uma variavelzinha aqui…”. Então, resolvem o assunto dizendo “Não me incomode com isso”, comentando a linha ou desligando as mensagens.
Você também pode achar que o desempenho do seu programa é suficientemente bom se um processamento levar apenas uma piscada de olhos, ou seja, 50ms. Você já parou para pensar em quantas coisas o computador é capaz de fazer hoje em 50ms, e quantas ele era capaz de fazer a dez anos atrás?
As máquinas obedecem à lei de Moore, dobrando a sua capacidade a cada dois anos, e nós provavelmente nos tornamos programadores duas vezes mais relaxados nesse mesmo período.

Outro dia, rodei um aplicativo feito para MS-DOS que desenvolvi já faz um bom tempo. Fora a beleza e outras firulas das modernas aplicações gráficas, o velho sistema era capaz de fazer tudo os que a nova versão fazia só que com um desempenho impressionante. É que o velho sistema havia sido desenvolvido sob a “palmatória” das limitações de outra época, e encontrou abundância de recursos no novo ambiente. Por que não conseguimos desenvolver sistemas mais rápidos hoje em dia se temos máquinas mais rápidas?

Você pode sentir algo parecido observando a evolução das redes, desde as antigas BBS, por onde trafegavam documentos de texto puro a taxas de alguns Kbps, até aos estonteantes 30 Mbps das linhas de Internet atuais. Hoje é quase inconcebível que um sistema possa rodar dentro de uma rede com velocidade tão baixa quanto a das BBS, mas eles existiram. Certamente, havia muita preocupação quanto ao uso da banda, uso de compressores, protocolos simplificados, maior latência e maior autonomia das partes para diminuir o uso da rede. Hoje, transferimos arquivos imensos como imagens, músicas e filmes, instalamos aplicativos, falamos com pessoas e as vemos através da Internet, e ainda reclamamos da lentidão da rede. Imagine só o que as aplicações BBS fariam com tantos recursos.

Certa vez, lendo a descrição de um projeto, encontrei uma frase memorável, que dizia mais ou menos assim: “… não será necessário utilizar índices para as tabelas, pois a performance do sistema é satisfatória”. Pois é, quanto mais recursos temos mais desperdiçamos.
Consultas SQL são bem cômodas, seu propósito é conduzir o usuário a dizer o que quer, e deixar os detalhes da busca por conta do mecanismo do banco que, em tese, é mais eficiente. Freqüentemente nós sabemos como a consulta deveria ser feita e não entendemos por que o banco demora tanto para realizá-la.

De maneira nenhuma estou dizendo que seja ruim utilizar paradigmas de abstração, como SQL, OOP, AOP, Multi camadas, DSL, VMs etc., se isso nos deixar mais produtivos. Mas acho que todos deveriam conhecer um pouco do que se passa nos bastidores, conhecer um pouco de ASM ou outras linguagens mais próximas da máquina como C (não C++) e Pascal, estruturas de dados, protocolos de rede, implementação de bancos de dados etc. de modo a tecer uma visão crítica sobre as tecnologias que nos são apresentadas e a melhor forma de utilizá-las.

Vez por outra somos convidados a nos submeter a ambientes mais restritivos, como programação para pequenos dispositivos, processamento distribuído. Nesse momento é importante conhecermos as limitações, o que podemos e não podemos fazer, e saber aonde estão os nossos relaxamentos.

Enviado em Paradigmas | 2 comentários »