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:
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;update_person
apenas atualiza o documento.db
é um objeto de banco de dados MongoDB, apenas para fim de exemplo.app
é uma aplicação Flask, apenas para fim de exemplo.import
s 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.