Um recurso bacana em Ruby é concern: este módulo do ActiveSupport abstrai a criação de mixins, deixando muito simples e prático fazer programação orientada a aspecto. Dê uma olhada lá na documentação, pois, logo no começo da página, há dois exemplos de mixin, sem e com concern, a título de comparação.
Isso me fez pensar: e Python?
Já escrevi um artigo sobre mixins em Python, que deixa claro a complexidade quando fazemos a cola dos mixins na classe principal usando herança múltipla. Basta olhar a assinatura da classe para ver como isso pode ficar complicado com a adição de novos mixins:
class Grades(SerialisableGradeMixin, PersistenceMixin, GradesMixin):
Pensando nisso, resolvi implementar uma versão de concern em Python.
O que precisamos fazer: a factory deve injetar no contexto em que ele for chamado métodos e atributos da classe mixin.
A primeira coisa que precisamos é ter acessível no corpo da função o contexto da criação da classe. Para isso usamos o módulo inspect
.
O atributo f_locals
do frame, retornado pela função frame.currentframe()
, é equivalente a locals()
, mas queremos os “locais” do frame
que chama nossa função. Esse é o f_back
:
context = inspect.currentframe().f_back.f_locals
Já temos o contexto da criação da classe à disposição, agora só falta injetar nele o dicionário de atributos do mixin:
context.update(aspect.__dict__)
O corpo todo do módulo Python fica:
# file: concerns.py
import inspect
def concern(aspect: type) -> None:
context: dict = inspect.currentframe().f_back.f_locals
context.update(aspect.__dict__)
Só isso já é suficiente! Porém não temos garantias de que funcione, precisamos de testes.
Para testar, vamos criar um mixin de serialização e usá-lo num classe muito simples de pessoa.
O mixin terá dois métodos: um método de serialização e outro de classe de desserialização. Vamos usar JSON para a serialização, ordenando as chaves para facilitar os testes.
# file: serial.py
import json
class SerialMixin:
def serialize(self) -> str:
return json.dumps(self._asdict(), sort_keys=True)
@classmethod
def deserialize(cls, data: str):
return cls(**json.loads(data))
A classe pessoa será uma
Podemos contornar isso facilmente criando uma classe base, que será a tupla em si, e uma classe herdeira, que permite acesso ao dicionário de atributos:
# file: person.py
from typing import NamedTuple
from concerns import concern
from .serial import SerialMixin
class PersonBase(NamedTuple):
name: str
surname: str
class Person(PersonBase):
concern(SerialMixin)
Já temos a classe com uso de mixin. Vamos criar agora o teste, que verifica se instâncias de pessoa podem ser serializadas e desserializadas:
# file: test_concern.py
from unittest import TestCase
from .person import Person
class TestConcern(TestCase):
def test_serialize(self):
p = Person(name='John', surname='Doe')
self.assertEqual(
p.serialize(),
'{"name": "John", "surname": "Doe"}',
)
def test_deserialize(self):
p = Person.deserialize('{"name": "J", "surname": "Quest"}')
self.assertIsInstance(p, Person)
self.assertEqual(p.name, "J")
self.assertEqual(p.surname, "Quest")
Rodando os testes:
sh> python3 -munittest test_concern.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
E a classe Grades
do exemplo anterior?
Usando nossa factory, seu cabeçalho ficaria assim:
class Grades:
concern(SerialisableGradeMixin)
concern(PersistenceMixin)
concern(GradesMixin)
Perceba que, nessa abordagem, a adição de novos mixins não suja a assintura da classe.
[update 2018-08-16]
O projeto está no PyPI:
sh> pip3 install concerns
[/update]