Todos os exemplos nesse post são simples de implementar. Não estou interessado na dificuldade da implementação, somente na arquitetura. Todo o código se encontra em https://github.com/renzon/gaebusiness-explanation. Dividi o post em passos e vc pode conferir o código inteiro baixando desse repositório e rodando o comando:
"git checkout n", onde n é o número do passo.
Passo 1
O que eu mais gosto em desenvolvimento de software é pensar na arquitetura, em como organizar seu processo de desenvolvimento e projeto para obter uma boa produtidade de sua equipe, ao mesmo tempo em que produz software com qualidade.Quando estava desenvolvendo o Pic Pro (Digital do Vale), estudei e implementei o Domain Driven Development (DDD), mas da maneira errada. Eu criava meus módulos baseado em entidades de domínio centrais e dentro desses eu usava um MVC. Até certo ponto do projeto, essa se mostrou uma boa organização.
Mas como sempre, satisfação com arquitetura não dura muito. Eu colocava todo meu negócio nos meus handlers web, o que no DJango chamam de Views. Isso se tornava um problema, na medida em que a lógica de web se misturava com a lógica de negócio. Com isso, ficava difícil reutilizar regras de negócio.
Como exemplo, vamos construir um código para salvar um usuário:
def index(_write_tmpl, name=None): url = router.to_path(index) users = User.query_all().fetch() if name: user = User(name=name) user.put() users.insert(0, user) values = {'form_url': url, 'users': users} _write_tmpl('templates/form.html', values)
Observando a função você pode perceber que da linha 4 à 6 está a lógica de salvamento do usuário, misturada à de obtenção de valores para renderização do template HTML. Isso se torna um problema quando você quer salvar o usuário de outra forma, por exemplo, com uma chamada AJAX:
def save_user(_resp, name): user = User(name=name) user.put() js = json.dumps(user.to_dict()) _resp.write(js)
Então você nota que está sendo utilizada e bela técnica de reutilização de código "Ctrl+C Ctrl+V" nas linhas 2 e 3. O problema disso é que você viola o princípio Don't Repeat Yourself (DRY).
Passo 2
O exemplo simples do que pode acontecer em um projeto grande sendo feito com essa metologia de Programação Orientada à Gambiarra (POG) é o seguinte: com a evolução do projeto, surge a necessidade de logar toda vez que o usuário é salvo. Quero manter o exemplo simples, mas um requisito ainda mais crítico seria decrementar o número de licenças disponíveis do sofware.Utilizando a arte da POG, o desenvolvedor faz uma busca no código e encontra o save_user e, corretamente, implementa a funcionalidade:
def save_user(_resp, name): user = User(name=name) user.put() logging.info("Saving %s" % user) js = json.dumps(user.to_dict()) _resp.write(js)
Aí ele acha que implementou o requisito por completo e manda para produção. Se fosse o caso do número de licenças, a maravilha dessa alteração é que se o usuário fosse salvo pelo formulário HTML em vez do AJAX, a licença não seria contabilizada. Parece brincadeira isso, mas já vi projeto que salvava usuário de umas 3 maneiras diferentes, cada qual com suas regras próprias.
Passo 3
Comecei a estudar sobre como resolver o problema, e gostei muito de um keynote do Uncle Bob em um envento Ruby: Arquitetura: os Anos Perdidos. Apesar de ser uma crítica ao Rails, a carapuça serviu em mim direitinho.
Inspirado pela idéia, passei e implementar uma fachada para minha camada de negócios. Dessa maneira, eu respeitaria o DRY, evitando o problema de duplicação de código. Eu sei que uma fachada deve ser apenas uma interface que delega suas funções a pacotes internos, mas por brevidade, violei o padrão nesse exemplo:
#Fachada def save_user(name): user = User(name=name) user.put() logging.info("Saving %s" % user) return user
#Ajax def save_user(_resp, name): user = facade.save_user(name) js = json.dumps(user.to_dict()) _resp.write(js)
#HTML def index(_write_tmpl, name=None): url = router.to_path(index) users = User.query_all().fetch() if name: user = facade.save_user(name) users.insert(0, user) values = {'form_url': url, 'users': users} _write_tmpl('templates/form.html', values)
Dessa maneira, separo lógica do meu negócio totalmente da apresentação. Não importa se a saída vai ser HTML, XML, JSON ou qualquer outro. Aliás, não precisa nem ser uma aplicação web para que você possa reutilizar suas regras de negócio. Seria possível fazer uso das mesmas regras para implementar um Desktop.
Passo 4
Seguindo nessa linha, suponha um novo requisito onde se quisesse salvar em banco um log com hora, log esse percente a um outro módulo qualquer. E isso somente quando o usuário fosse salvo através do html. Para isso, seguiriamos o mesmo caminho, fazendo uma fachada para meu novo módulo e orquestrando as funções no meu handler:#Fachada de Log def save_user_log(name): log = SaveUserLog(user=name) log.put() return log
#HTLM def index(_write_tmpl, name=None): url = router.to_path(index) users = User.query_all().fetch() if name: user = facade.save_user(name) log_facade.save_user_log(name) users.insert(0, user) values = {'form_url': url, 'users': users} _write_tmpl('templates/form.html', values)
Passo 5
Porém, como já mencionei, satisfação com arquitetura não dura muito. O que acontece nessa abordagem, e com o princípio DRY em geral, é o problema da perfomance. Uma das característica interessantes do Google App Engine é que ele possui várias APIS assíncronas e métodos para otimizar acesso ao banco de dados. Entre eles, salvar entidades de uma só vez no banco é mais eficiente do que salvar uma a uma. Tendo isso em mente, esse esquema simples de fachada é bem reutilizável, mas a performance fica degradada, uma vez que não se tem como salvar todas entidades ao mesmo tempo.
O que eu gostaria era de continuar com essa fachada, mas também queria poder orquestrar chamadas assíncronas, de forma que elas pudessem ocorrer em paralelo, otimizando o tempo de resposta para minhas requisições. Além disso, eu gostaria de poder deixar, quando possível, para salvar minhas entidades todas de uma vez.
Para chegar nesse objetivo, eu criei o GaeBusiness, disponivel via comando "pip install gaebusiness". Nesse framework eu fiz uso intensivo do padrão Template:
class Command(object): def __init__(self, **kwargs): self.errors = {} self.result = None for k, v in kwargs.iteritems(): setattr(self, k, v) def __add__(self, other): return CommandList([self, other]) def add_error(self, key, msg): self.errors[key] = msg def set_up(self): ''' Must set_up data for business. It should fetch data asyncrounously if needed ''' pass def do_business(self, stop_on_error=True): ''' Must do the main business of use case ''' raise NotImplementedError() def commit(self): ''' Must return a Model, or a list of it to be commited on DB ''' return [] def execute(self, stop_on_error=True): self.set_up() self.do_business(stop_on_error) ndb.put_multi(to_model_list(self.commit())) return self
O construtor da classe Command recebe os parâmetros a serem processados. O método set_up faz as chamadas assíncronas. O método do_business executa a regra de negócio e, por fim, o método commit retorna entidades que devem ser salvas no banco de dados ao fim do processo. Já o método execute orquestra toda a operação.
Além dessa classe, também foi criada a classe CommadList, que é uma lista de comandos que implementa o padrão Composite . Assim, é possível tratar um lista de comandos do mesmo jeito que um comando único. Repare também que em Command eu sobrecarreguei o operador de adição para que se possa somar comandos, cujo resultado é um CommandList. Dessa maneira, usando essa arquitetura, temos o projeto refatorado:
#Comando para salvar usuário class SaveUserCmd(Command): def __init__(self, name): Command.__init__(self, name=name) def do_business(self, stop_on_error=True): self.result = User(name=self.name) def commit(self): return self.result
#Fachada do usuario def save_user(name): return SaveUserCmd(name)
#Comando para salvar log lass SaveUserLogCmd(Command): def __init__(self, name): Command.__init__(self, name=name) def do_business(self, stop_on_error=True): if not self.name: self.add_error('name', 'Name is required') else: self.result = SaveUserLog(user=self.name) def commit(self): return self.result
#Fachada do Log def save_user_log(name): return SaveUserLogCmd(name)
#handler ajax refatorado def save_user(_resp, name): user = facade.save_user(name).execute().result js = json.dumps(user.to_dict()) _resp.write(js)
#handler html refatorado def index(_write_tmpl, name=None): url = router.to_path(index) users = User.query_all().fetch() if name: # usando sobrecarga da adicao cmds = log_facade.save_user_log(name) + facade.save_user(name) # Tratando CommandList da mesma for que um Command user = cmds.execute().result if not cmds.errors: users.insert(0, user) values = {'form_url': url, 'users': users} _write_tmpl('templates/form.html', values)
Dessa maneira, eu tenho uma solução Orientada a Objetos para poder fazer polimorfismo com os comandos, definindo as estapas a serem executadas e, principalmente, podendo compor comandos mais complexos a partir de comandos mais simples. É isso que eu queria dizer com o slide sobre módulos da minha apresentação sobre Entrega Contínua, onde faço o paralelo com peças de lego.
Consegui, pela primeira vez, construir um módulo bacana, usando essa estrutura, com o cliente do Passwordless: https://github.com/renzon/pswdclient. Publiquei como um pacote python e só usei a fachada como interface na hora de integrar, como você pode comprovar na linha 276 desse arquivo.
Cabe ressaltar que Command e CommandList não possuem qualquer dependência externa, é apenas código Python puro. Por isso, seria possível reutilizá-lo em qualquer projeto, independente do framework que se use, como DJango ou Flask.
Enfim, o que percebi depois disso tudo é que o código ficou mais reutilizável, mas programar ficou um pouco mais burocrático. Seria bom saber também a sua opinião, já que você teve paciência de ler esse post todo: o que você acha? Deixe seus comentários =D
Abs,
Renzo Nuccitelli
5 comentários:
Tinha visto esse vídeo The Lost Years um tempoa trás também. Bem esclarecedor mesmo.
Acho que quanto a burocracia, é daquelas coisas que não dá pra evitar. Quanto mais fácil de fazer e sem burocracia, menos organizado e reutilizável. Quanto mais organizado, e fica mais chato.
Acho que mantendo uma camada de serviço, independente de como é a estrutura dela, já ajuda bastante.
Olá digo_tech. Tenho a mesma opinião =D
Gostei do que li, Renzo. :-)
Há algum tempo tenho estudado arquitetura do mesmo ponto de vista que você.
Percebi que teoricamente o que o Uncle Bob Martin apregoa é muito legal.
Na prática, o que ele propõe é fazer com que sua aplicação esteja acima da camada de persistência e de delivery. A web é considerada delivery. Ou seja, pelo que entendi, ele propõe que sua aplicação funcione do mesmo jeito, independente de em qual framework ela está desenvolvida.
Isso traz enormes vantagens e aumenta a sobrevida da aplicação, sem dúvida. No entanto, acaba eliminando o benefício que os frameworks trazem, principalmente no que tange validação de dados.
Ainda não tenho opinião formada sobre o assunto. Continuo estudando. ;-)
Grande Vinícus. Pois é, eu gostei do conceito do vídeo do uncle Bob, mas também achei meio exagerado. Quanto a validação, acredito que você esteja falando de algo como DJango forms, estou correto? Acho que vc se encontra no mesmo dilema que também tenho: usar um framework e aproveitar suas vantagens e sofrer com seus problemas, ou construir aplicações do 0. Ou seja, o velho dilema que conversamos na Python Brasil 9: DJango versus Flask. Se for esse o caso e no caso da validação vc estiver falando do DJango forms, vc poderia facilmente encapsular essa validação em um comando. A vantagem disso é que se por acaso vc for integrar módulos e algum deles precisar saber quando determinado form é validado e salvo, bastaria implementar um Observer na Fachada do módulo. Mas assim como você, continuo estudando. Agora estou aprendendo DJango para entender porque o pessoal gosta tanto =D. Mas um vídeo que o Luciano me passou e achei bacana foi o do Martijn (http://pyvideo.org/video/2416/spinning-a-web-framework). Foi legal ver um cara fodão com as mesmas angústias que a gente.
Possas crer Renzo, já fiquei no mesmo dilema usando Flask e as vezes Django, mas é uma ótima solução, e não tem como fugir muito da "burocracia" se quiser algo mais "reutilizável".
Postar um comentário