Back-End

26 nov, 2013

Microarquitetura Python para a camada de negócio: GaeBusiness

Publicidade

Todos os exemplos neste artigo 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 artigo em passos e você 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 produtividade 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 deles 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 a 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 metodologia de Programação Orientada à Gambiarra (POG) é o seguinte: com a evolução do projeto, surge a necessidade de logar toda vez em 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 software.

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 três 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 evento Ruby: Arquitetura: os Anos Perdidos. Apesar de ser uma crítica ao Rails, a carapuça serviu em mim direitinho.

Inspirado pela ideia, passei a 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 neste 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 a lógica do meu negócio totalmente da apresentação. Não importa se a saída vai ser HTML, XML, JSON ou qualquer outra. 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 essa linha, suponha um novo requisito no qual se quisesse salvar em banco um log com hora, log esse pertencente a um outro módulo qualquer. E isso somente quando o usuário fosse salvo através do html. Para isso, seguiríamos 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, disponível 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, 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 etapas 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, em que 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 deste arquivo.

Cabe ressaltar que Command e CommandList não possuem qualquer dependência externa, são apenas código Python puro. Por isso, seria possível reutilizá-los em qualquer projeto, independentemente 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.