Kodumaro :: Aspectos – parte Ⅰ

Released on April 19th, 2016
The shadows behind the code.
Python

Este artigo havia sido publicado originalmente em 18 de fevereiro de 2014 no blog original. Infelizmente o Blogger.com resolveu apagá-lo durante um bug da interface, mas o trouxemos de volta nesta nova encarnação do Kodumaro.

Um paradigma muito útil é a Programação Orientada a Aspectos.

Consiste em separar e encapsular as funcionalidades de um código conforme sua importância.

Nesta primeira parte, abordaremos essa separação de forma simples, deixando o conceito de mixins para a parte II.

Vamos começar com um exemplo: imagine uma view que modifica o estado de um objeto, retornando um hash do novo estado:

ResponseType = Tuple[str, Integral, Mapping]

@app.route('/people/<uuid>/', methods=['PATCH'])
def update_person(uuid: str) -> ResponseType:
    person = db.person.find({'_id': uuid}).first()
    if not person:
        raise Http404

    try:
        data = json.loads(request.data)
    except ValueError:
        return json.dumps({'error': 'invalid request'}), \
               400, \
               {'Content-Type': 'application/json'}

    person.update(data)
    db.person.save(person)

    r = sorted(person.items())
    s = ';'.join('{}:{!r}'.format(k, v) for k, v in r)

    return json.dumps({'etag': md5(s).hexdigest()}), \
           200, \
           {'Content-Type': 'application/json'}

A solução atende, mas é de difícil manutenção. Perceba que a função chamada update_person (atualiza pessoa) faz muito mais do que simplesmente atualizar os dados:

  • Recupera o documento do banco, retornando 404 se não existir;
  • Faz parsing dos dados recebidos, retornando 400 em caso de erro;
  • Efetivamente atualiza o documento;
  • Serializa o objeto para a resposta;
  • Gera um hash da serialização;
  • Responde a requisição com formato conveniente.

Cada um desses passos é um aspecto do processo e pode ser isolado do restante.

Vamos então separar o primeiro aspecto: recuperação do documento.

# Type aliases

ResponseType = Tuple[str, Integral, Mapping]
ViewType = Callable[[str], ResponseType]
ParserType = Callable[[MutableMapping], ResponseType]

# Aspect decorators

def retrieve_person_aspect(view: ParserType) -> ViewType:
    @wraps(view)
    def wrapper(uuid: str) -> ResponseType:
        person = db.person.find({'_id': uuid}).first()
        if not person:
            raise Http404
        return view(person)
    return wrapper

# View(s)

@app.route('/people/<uuid>/', methods=['PATCH'])
@retrieve_person_aspect
def update_person(person: MutableMapping) -> ResponseType:
    try:
        data = json.loads(request.data)
    except ValueError:
        return json.dumps({'error': 'invalid request'}), \
               400, \
               {'Content-Type': 'application/json'}

    person.update(data)
    db.person.save(person)

    r = sorted(person.items())
    s = ';'.join('{}:{!r}'.format(k, v) for k, v in r)

    return json.dumps({'etag': md5(s).hexdigest()}), \
           200, \
           {'Content-Type': 'application/json'}

Agora a recuperação do documento está isolada, podendo inclusive ser usada em outras views. Nossa view já recebe o documento recuperado e não precisa lidar com o fato dele existir ou não.

Porém ainda temos muita coisa misturada. Por exemplo, a obtenção e parsing dos dados recebidos: isso caracteriza outro aspecto do código, que não a atualização do documento.

Podemos portanto, separá-los:

# Type aliases

ResponseType = Tuple[str, Integral, Mapping]
ViewType = Callable[[str], ResponseType]
ParserType = Callable[[MutableMapping], ResponseType]
ETagAddType = Callable[[MutableMapping, Mapping], ResponseType]

# Aspect decorators

def parse_data_aspect(view: ETagAddType) -> ParserType:
    @wraps(view)
    def wrapper(person: MutableMapping) -> ResponseType:
        try:
            data = json.loads(request.data)
        except ValueError:
            return json.dumps({'error': 'invalid request'}), \
                   400, \
                   {'Content-Type': 'application/json'}
        return view(person, data)
    return wrapper

def retrieve_person_aspect(view: ParserType) -> ViewType:
    ...

# View(s)

@app.route('/people/<uuid>/', methods=['PATCH'])
@retrieve_person_aspect
@parse_data_aspect
def update_person(person: MutableMapping, data: Mapping) -> ResponseType:
    person.update(data)
    db.person.save(person)

    r = sorted(person.items())
    s = ';'.join('{}:{!r}'.format(k, v) for k, v in r)

    return json.dumps({'etag': md5(s).hexdigest()}), \
           200, \
           {'Content-Type': 'application/json'}

A função update_person já está muito mais limpa: atualiza o documento, serializa e retorna o hash, mas ainda faz coisas demais. Vamos separar o tratamento do retorno:

# Type aliases

ResponseType = Tuple[str, Integral, Mapping]
ViewType = Callable[[str], ResponseType]
ParserType = Callable[[MutableMapping], ResponseType]
ETagAddType = Callable[[MutableMapping, Mapping], ResponseType]
PersonSerialiserType = Callable[[MutableMapping, Mapping], str]

# Aspect decorators

def respond_etag_aspect(view: PersonSerialiserType) -> ETagAddType:
    @wraps(view)
    def wrapper(person: MutableMapping, data: Mapping) -> ResponseType:
        response = view(person, data)
        return json.dumps({'etag': md5(response).hexdigest()}), \
               200, \
               {'Content-Type': 'application/json'}
    return wrapper

def parse_data_aspect(view: ETagAddType) -> ParserType:
    ...

def retrieve_person_aspect(view: ParserType) -> ViewType:
    ...

# View(s)

@app.route('/people/<uuid>/', methods=['PATCH'])
@retrieve_person_aspect
@parse_data_aspect
@respond_etag_aspect
def update_person(person: MutableMapping, data: Mapping) -> str:
    person.update(data)
    db.person.save(person)

    r = sorted((str(k), repr(v)) for k, v in person.items())
    return ';'.join('{}:{!r}'.format(k, v) for k, v in r)

As coisas estão ficando cada vez mais separadas. A única coisa que a função update_person ainda faz além de atualizar o documento é serializá-lo. Isso também pode ser isolado:

# Type aliases

ResponseType = Tuple[str, Integral, Mapping]
ViewType = Callable[[str], ResponseType]
ParserType = Callable[[MutableMapping], ResponseType]
ETagAddType = Callable[[MutableMapping, Mapping], ResponseType]
PersonSerialiserType = Callable[[MutableMapping, Mapping], str]
PersonUpdaterType = Callable[[MutableMapping, Mapping], MutableMapping]

# Aspect decorators

def serialise_person_aspect(view: PersonUpdaterType) -> PersonSerialiserType:
    @wraps(view)
    def wrapper(person: MutableMapping, data: Mapping) -> str:
        response = view(person, data)
        r = sorted((str(k), repr(v)) for k, v in response.items())
        return ';'.join('{}:{!r}'.format(k,v) for k, v in r)

def respond_etag_aspect(view: PersonSerialiserType) -> ETagAddType:
    ...

def parse_data_aspect(view: ETagAddType) -> ParserType:
    ...

def retrieve_person_aspect(view: ParserType) -> ViewType:
    ...

# View(s)

@app.route('/people/<uuid>/', methods=['PATCH'])
@retrieve_person_aspect
@parse_data_aspect
@respond_etag_aspect
@serialise_person_aspect
def update_person(person: MutableMapping, data: Mapping) -> MutableMapping:
    person.update(data)
    db.person.save(person)
    return person

Perceba que, com a separação dos aspectos em funções distintas, o código ficou muito mais semântico:

  • retrive_person_aspect apenas recupera o documento do banco;
  • parse_data_aspect apenas faz o parsing dos dados recebidos;
  • respond_etag_aspect apenas gera o formato correto da resposta;
  • serialise_person_aspect apenas serializa o documento;
  • finalmente, update_person apenas atualiza o documento.

Observação

  • db é um objeto de banco de dados MongoDB, apenas para fim de exemplo.
  • app é uma aplicação Flask, apenas para fim de exemplo.
  • A ordem dos decoradores é importante, o retorno de cada decorador precisa ser do mesmo tipo do argumento do decorador seguinte.
  • Os imports foram omitidos:
    from typing import Callable, Mapping, MutableMapping, Tuple
    from numbers import Integral
    from functools import wraps
    from hashlib import md5
    import json
    

Na parte II abordaremos mixins.

Concept | Python

DEV Profile 👩‍💻👨‍💻