Este artigo é uma anotação de uma palestra que fiz recentemente na Software Summit Passion em Gotemburgo, Suécia.
Escrever uma especificação em uma linguagem de programação completa como Python possui vantagens e desvantagens. Em relação às desvantagens, o Python não é uma linguagem declarativa, portanto, qualquer tentativa de torná-lo declarativo (além de apenas listar os tipos nativos de dados) exigirá algum tipo de personalização e/ou ferramentas para trabalhar. Como vantagem, tendo uma declaração na linguagem na qual você escreve seus servidores, você pode usar a própria especificação, em vez de um derivado gerado dela, e escrever geradores personalizados para outras linguagens é simples, desde que você possa fazer introspecção em Python atravessar a sua especificação, e a lógica templating de sua escolha para gerar o código-fonte – o que torna possível, por exemplo, atingir um terminal J2ME que só não vai aceitar as soluções existentes, e onde colocar um arquivo jar 150K para implementação do protocolo não é uma alternativa.
Para mim, essa jornada começou por volta de 2006, quando comecei a perder o controle sobre documentação de protocolo e versões de protocolo para o protocolo usado entre terminais e servidores no gerenciamento da solução Visual Units Logistics. Depois de procurar e descartar várias ferramentas existentes, e após ser inspirado pelo fato de que nós geralmente configuramos o JavaScript no JavaScript, comecei a esboçar (como tinta sobre papel) sobre como seria uma especificação de protocolo em Python. Esta é uma transcrição do que eu criei na época:
imei = long
log_message = string
timestamp = long
voltage = float
log = Message(imei, timestamp,
log_message, voltage)
protocol = Protocol(log, ...)
protocol.parse(data)
Com isso como uma meta, eu criei a primeira versão de uma implementação de protocolo. Ela se parecia com a versão alvo, mas sofreu com uma abundância de repetição:
#protocol.py
LOG = 0x023
ALIVE = 0x021
message = Token('message', 'String', 'X')
timestamp = Token('timestamp', 'long', 'q')
signal = Token('signal', 'short', 'h')
voltage = Token('voltage', 'short', 'h')
msg_log = Message('LOG', LOG, timestamp, signal, voltage)
msg_alive = Message('ALIVE', ALIVE, timestamp)
protocol = Protocol(version=1.0, messages=[msg_log,msg_alive])
#usage
from protocol import protocol
parsed_data = protocol.parse(data)
open('Protocol.java’,'w').write(protocol.java_protocol())
A implementação em torno disso é simples, a classe Token sabe como analisar uma parte de uma mensagem, a classe Message sabe quais Tokens usar (e em que ordem), e a classe Protocol seleciona a instância de Mensagem correta utilizando um mapeamento de marcadores de bytes nas instâncias de Mensagens.
No entanto, nenhum apoio é dado para lidar com várias versões do protocolo e a quantidade de duplicação de nomes torna isso muito complexo – por isso me propus a criar uma versão melhor.
Algumas coisas complicaram a criação de uma versão melhor. O pior problema de todos provou ser somente eu. Nessa época, eu tinha usado Python por alguns anos, e comecei a me interessar pelas ferramentas mais sofisticadas disponíveis. Eu tinha acabado de aprender sozinho sobre metaclasses, e pensei que fossem um aplicativo engenhoso para orientação a objetos – e porque tinha encontrado um martelo novinho em folha, eu estava louco para encontrar um prego.
Infelizmente, eu não tinha uma necessidade urgente para a utilização de metaclasses, então eu inventei uma – eu queria evitar algumas tarefas na especificação de protocolo, então usei metaclasses para arrancar o método (constructor) init e substituí-lo por uma versão que registrou a instância em um mapa global e depois chamei o método init original. Isso foi errado em pelo menos três formas – uma vez que não era genérico, poderia ter sido feito diretamente no método init, se tivesse sido geral, teria sido um trabalho para um decorator, e é realmente uma ótima maneira de ofuscar o código:
__MSG_MAPPING__ = {}
def msg_initizer(cls, old_init):
def new_init(self, name, marker, *args):
__MSG_MAPPING__get(cls, {})[name] = self
__MSG_MAPPING__[cls][struct.pack("!B", marker)] = self
old_init(self, name, marker, *args)
return new_init
class RegisterMeta(type):
def __new__(cls, name, bases, attrs):
attrs['__init__'] = msg_initizer(cls,
attrs['__init__'])
return super(RegisterMeta, cls).__new__(cls,
name, bases, attrs)
class Message(object):
__metaclass__ = RegisterMeta
A propósito, esse é o tipo de código do qual eu não tenho orgulho. A pior parte? Eu nem sequer removi a duplicação, embora a tenha diminuído um pouco – e o registro global de mensagens durante o carregamento de um protocolo realmente bagunçou qualquer tentativa de suporte para versão múltipla. Esse não foi o único problema; eu também extrapolei e queria suporte especificando a sintaxe do protocolo, usando uma classe Flow que definiu a ordenação coletiva das mensagens. Isso poderia ter sido uma boa ideia se tivéssemos realmente tido esses requisitos em nossos protocolos; já que eles são “autenticar, fazer qualquer coisa”, adicionar suporte para isso apenas expandiu a base de código e fez a especificação de protocolo mais complexa para pouquíssimo ganho (especialmente pelo fato de que autenticamos de diferentes maneiras, dependendo do cliente). Adicionando insulto à injúria, isso é ainda mais detalhado do que a primeira tentativa.
#In protocol.py
imei = Token('imei', 'long')
message = Token('message', 'String')
timestamp = Token('timestamp', 'long')
signal = Token('signal', 'short')
voltage = Token('voltage', 'short')
auth = Token('auth', 'String')
Markers({'LOG': 0x023,
'ALIVE': 0x021,
'AUTH': 0x028})
Message('LOG', imei, timestamp, signal, voltage)
Message('ALIVE', imei, timestamp)
Message('AUTH', imei, timestamp, auth)
Flow([('AUTH'), ('LOG', 'ALIVE')])
#Usage
protocol = Protocol(version=2.0)
parsed_data = protocol.parse(data) #error if not auth parsed
Toda essa tentativa se tornou um exemplo de alerta – ela mostra o perigo de encontrar novas tecnologias interessantes e aplicá-las antes de compreendê-las totalmente, e mostra o perigo de engenharia e fluência de recurso. Felizmente, quando eu dei uma boa olhada no que havia criado, até mesmo no que eu fiz uns anos atrás, pude ver que isso era uma abominação, que depois foi retirada calmamente e deixada de lado, sem nem chegar aos testes de integração.
Finalmente, e progressivamente, decidi aplicar uma quantidade cuidadosa de magia de biblioteca padrão para fazer as especificações mais concisas e remover as coisas de que nós não precisávamos. Isso fez com que a especificação parecesse com algo assim:
#In protocol_4.2.py:
#Tokens
t('message', string)
t('timestamp', i64)
t('signal', i16)
t('voltage', i16)
#Messages
LOG = ('A log message containing some debug info',
0x023, timestamp, message, signal, voltage)
ALIVE = ('A message to signal that the terminal alive',
0x021, timestamp)
#Usage
protocols = load_protocols('protocols')
parsed = protocols[4.2].parse(data)
protocols[4.2].write_java() #Writes to Protocol42.java
Ao mesmo tempo, era ainda resumida, mas essa versão realmente não teve sucesso, e a versão de produção é muito semelhante a esta. A duplicação de nomes é evitada usando duas técnicas diferentes – os tokens são definidos ao chamar um método t que cria a instância Token e a introduz de volta para a chamada namespace usando o nome fornecido:
#In types.py
from inspect import currentframe
def t(name, data_type):
"""Inserts name = (name, data_type) in locals()
of calling scope"""
currentframe().f_back.f_locals[name] = (name, data_type)
Para alguns, isso pode parecer uma blasfêmia, mas considere que a aplicação é extremamente simples no conceito, ela faz o trabalho ser feito e é fácil de explicar. Outra mudança é que as mensagens são criadas somente usando inspect para extrair membros do módulo que se parece com mensagens – todos os nomes em letras maiúsculas e uma tupla. Vale notar que pode ser que tenha havido inicialmente um erro de manipulação, mas eu o removi para fazer a análise de falha, em vez de aceitar uma especificação que poderia ou não ter contido erros.
Finalmente, a documentação java de origem e de html é criada percorrendo a instância de protocolo, e alimentando as informações em modelos simples – os experimentos foram feitos usando linguagem de programação literal usando REST para criar documentação, mas que no fim tendem a ofuscar, e não o contrário. Esse pode ser um efeito de implementação ingênua, ou esse problema não se presta bem à programação literal, mas, de qualquer forma, não valeu a pena nesse caso.
Há um trabalho e uma versão ligeiramente generalizada disponíveis no bitbucket, sobre os quais você poderia gostar de saber mais (e mais detalhes sobre a magia usada de Python).
?
Texto original disponível em http://blaag.haard.se/Protocol-specifications-written-in-Python/







