Reduzindo a complexidade em modelos Django com objetos de valor
Implementando o padrão de objeto de valor
Martin Fowler define objetos de valor da seguinte maneira:
Um pequeno objeto simples, como dinheiro ou um intervalo de datas, cuja igualdade não é baseada na identidade.
Tá, mas o que isso significa?
O importante sobre Value Objects é que eles são iguais entre si se tiverem a mesma informação, não apenas se forem o mesmo objeto na memória. Assim, dois objetos de valor que representam a mesma data ou a mesma quantia em dinheiro seriam considerados iguais. Além disso, os objetos de valor são declarados como imutáveis, o que significa que seus valores não podem ser alterados depois de criados, o que garante ainda mais a consistência dos dados em um aplicativo.
O uso de objetos de valor ajuda a evitar o problema da “obsessão primitiva”, que ocorre quando usamos tipos de dados básicos como números inteiros ou strings no lugar de estruturas de dados mais significativas. Por exemplo, usando uma string para representar um endereço de e-mail. Ao usar objetos de valor, podemos criar representações de dados mais significativas e independentes.
O conceito de Value Objects origina-se do Domain-Driven Design (DDD) e é utilizado junto com entidades e agregados para modelar modelos de domínio. Neste artigo, demonstrarei o uso de Value Objects com modelos brutos do Django. Isso pode servir como um primeiro passo útil para uma arquitetura melhor ou simplesmente como uma medida de refatoração que ajuda a manter os modelos concisos e menos complexos.
Para ilustrar o uso de objetos de valor em modelos Django, vamos ver um cenário comum: descrever as datas de início e término de um evento. Uma implementação básica disso pode se parecer com o seguinte:
class Appointment(models.Model):
start = models.DateTimeField(null=True)
end = models.DateTimeField(null=True)
Por exemplo, pode ser necessário verificar se um compromisso se sobrepõe a outro ou determinar se ele começa antes ou depois de outro evento.
Para lidar com esses cenários, podemos usar um objeto de valor personalizado para descrever o intervalo de datas de um compromisso.
@dataclass(frozen=True) (1)
class DateRange:
start: datetime
end: datetime
def __post_init__(self) -> None: (2)
if self.start >= self.end:
raise ValueError("Can not end before starting.")
def __str__(self) -> str:
return f"DateRange({self.start} - {self.end})"
def __add__(self, other: timedelta) -> "DateRange": (3)
if isinstance(other, timedelta):
return DateRange(self.start + other, self.end + other)
raise TypeError()
def hour_later(self) -> "DateRange": (4)
return self + timedelta(hours=1)
@property
def duration(self) -> timedelta: (5)
return self.end - self.start
def overlaps(self, date_range: "DateRange") -> bool: (5)
return (self.start <= date_range.end
and self.end >= date_range.start)
def starts_before(self, date_range: "DateRange") -> bool: (5)
return self.start < date_range.start
def range(self, step: timedelta) -> Iterator[datetime]: (5)
current_datetime = self.start
while current_datetime <= self.end:
yield current_datetime
current_datetime = current_datetime + step
- Validação básica para garantir que o modelo esteja sempre em um estado válido.
- Adicionar dois intervalos de datas não faz sentido, portanto, substituímos o
+operador para permitir o reagendamento. Como nossa classe é imutável, devemos retornar um novo objeto. - Como uma alternativa mais detalhada, podemos utilizar métodos previamente declarados para criar funções que usam “linguagem de negócios”, tornando o código mais fácil de entender.
- Utilitários comuns podem ser escondidos atrás de uma interface simples.
class Appointment(models.Model):
_start = models.DateTimeField(null=True) (1)
_end = models.DateTimeField(null=True)
objects = CustomManager()
@property
def date_range(self) -> DateRange: (2)
return DateRange(self._start, self._end)
@date_range.setter
def date_range(self, value: DateRange) -> None: (3)
self._start = value.start
self._end = value.end
@classmethod
def new_in_date_range(cls, date_range: DateRange): (4)
return cls(_start=date_range.start, _end=date_range.end)
- A propriedade getter retorna o
DateRangeobjeto de valor. - Permite atribuir um
DateRangeobjeto a um arquivoAppointment. - O
new_in_date_rangemétodo é um método de fábrica que cria novosAppointmentobjetos com umDateRangeobjeto de valor.
class CustomManager(models.Manager):
def get_queryset(self):
return CustomQuerySet(self.model, using=self._db)
def in_range(self, date_range: DateRange):
return self.get_queryset().filter(
_start__gt=date_range.start,
_end__lt=date_range.end
)
class CustomQuerySet(models.QuerySet):
def filter(self, *args, **kwargs):
date_range = kwargs.get("date_range")
if date_range and isinstance(date_range, DateRange):
kwargs["_start"] = date_range.start
kwargs["_end"] = date_range.end
return super().filter(*args, **kwargs)
Outros exemplos de objetos de valor incluem:
- Dinheiro: Duas quantias de moedas diferentes não podem ser somadas.
- Taxa de IVA: pode ser feita uma distinção entre
VatRate"0" eVatRate"NOT_APPLIES", embora ambos sejam avaliados como 0. - Fuso horário
- Endereço.





































![O que é uma lista vinculada, afinal? [Parte 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)