Kodumaro :: Aspectos – parte Ⅱ

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

Na parte I demos uma passada rápida no conceito de aspectos. Agora veremos os mixins.

Mixins são classes incompletas que apenas atribuem determinado comportamento a suas herdeiras.

Vamos a um exemplo muito superficial, mas suficiente: um objeto que armazena notas de alunos em um arquivo.

from typing import TypeVar
from numbers import Integral, Number
from pickle import dumps, loads
import dbm

__all__ = ['Database', 'Grades']

FileType = TypeVar('File', str, '_gdbm.gdbm')


class Database:

    def __init__(self, file: FileType=None):
        if file is None:
            file = 'grades.db'
        if isinstance(file, str):
            file = dbm.open(file, 'c')
        self.file = file

    def close(self) -> None:
        if self.file:
            self.file.close()
            self.file = None


class Grades:

    def __init__(self, registration: Integral, db: Database):
        self.registration = registration
        if not isinstance(db, Database):
            raise TypeError('expected Database instance, got {.__name__}'
                            .format(type(db)))
        self.db = db

        try:
            self.values = loads(db[bytes(str(registration), 'ascii')])
        except KeyError:
            self.values = {}

    def save(self) -> None:
        db[bytes(str(self.registration), 'ascci')] = dumps(self.values)

    @property
    def first_bimester(self) -> Number:
        return self.values.get('1bim')

    @first_bimester.setter
    def first_bimester(self, value: Number) -> None:
        self.values['1bim'] = float(value)

    @property
    def second_bimester(self) -> Number:
        return self.values.get('2bim')

    @second_bimester.setter
    def second_bimester(self, value: Number) -> None:
        self.values['2bim'] = float(value)

    @property
    def third_bimester(self) -> Number:
        return self.values.get('3bim')

    @third_bimester.setter
    def third_bimester(self, value: Number) -> None:
        self.values['3bim'] = float(value)

    @property
    def fourth_bimester(self) -> Number:
        return self.values.get('4bim')

    @fourth_bimester.setter
    def fourth_bimester(self, value: Number) -> None:
        self.values['4bim'] = float(value)

    @property
    def catch_up(self) -> Number:
        return self.values.get('rec')

    @catch_up.setter
    def catch_up(self, value: Number) -> None:
        return self.values['rec'] = float(value)

    @property
    def avg_grade(self) -> None:
        grades = (
            self.first_bimester or 0,
            self.second_bimester or 0,
            self.third_bimester or 0,
            self.fourth_bimester or 0,
        )
        m = sum(grades) / len(grades)
        ca = self.catch_up
        return m if ca is None else (m + ca) / 2

Repare que temos o mesmo problema apresentando na parte I: está tudo misturado em uma única classe!

Podemos separar as partes de gerência de banco e serialização em classes diferentes, dedicadas a seu próprio aspecto, chamadas mixins.

A classe de faz serialização pode ser apenas isso:

class SerialisableGradeMixin:

    def load(self) -> None:
        s = self.retrieve()
        self.values = loads(s) if s else {}

    def __bytes__(self) -> None:
      return dumps(self.values)

A gerência de banco vai para outro mixin:

class PersistenceMixin:

    def retrieve(self) -> bytes:
        try:
            return self.db[bytes(str(self.registration), 'ascii')]
        except KeyError:
            return None

    def save(self) -> None:
        db[bytes(str(self.registration), 'ascii')] = bytes(self)

Preferindo, é possível separar a gerência de notas em um mixin também:

class GradesMixin:

    @property
    def first_bimester(self) -> Number:
        return self.values.get('1bim')

    @first_bimester.setter
    def first_bimester(self, value: Number) -> None:
        self.values['1bim'] = float(value)

    ...

    @property
    def catch_up(self) -> Number:
        return self.values.get('rec')

    @catch_up.setter
    def catch_up(self, value: Number) -> None:
        return self.values['rec'] = float(value)

    @property
    def avg_grade(self) -> None:
        grades = (
            self.first_bimester or 0,
            self.second_bimester or 0,
            self.third_bimester or 0,
            self.fourth_bimester or 0,
        )
        m = sum(grades) / len(grades)
        ca = self.catch_up
        return m if ca is None else (m + ca) / 2

Ao final, a classe principal será apenas uma cola dos mixins:

class Grades(SerialisableGradeMixin, PersistenceMixin, GradesMixin):

    def __init__(self, registration: Integral, db: Database):
        self.registration = registration
        if not isinstance(db, Database):
            raise TypeError('expected Database instance, got {.__name__}'
                            .format(type(db)))
        self.db = db
        self.load()

A API da classe continua idêntica: recebe o número da matrícula e o banco na instanciação, propriedades para acessar as notas e método save para salvá-las em arquivo, porém agora cada aspecto está isolado e encapsulado em seu próprio mixin.

Concept | Python

DEV Profile 👩‍💻👨‍💻