How to handle domain logic that spans multiple model objects in an ORM

djangodomain-modelmodelingorm

So I know that business logic should be placed in the model. But using an ORM it is not as clear where I should place code that handles multiple objects.

E.g. let's say we have a Customer model which has a type of either sporty or posh and we wanted to customer.add_bonus() to every posh customer. Where would we do this? Do we create a new class to handle all this? If yes, where do we put it (alongside all the other model classes, but not subclass it from the ORM?)?

I'm currently using django framework in python, so specific suggestions are even more wanted.

Best Answer

In general it depends on your architecture. Since you're using django, it's natural to walk the django path of fat models.

It's not clear from your example when you'd like to customer.add_bonus() but one approach you can take is modelling posh and sporty customers as different classes using inheritance. e.g.

class Customer(models.Model):
  pass

class PoshCustomer(Customer):
  def discount(self):
    self.add_bonus()

  class Meta:
    proxy = True

class SportyCustomer(Customer):
  def discount(self):
    pass

  class Meta:
    proxy = True

Here we're using Django's proxy classes (https://docs.djangoproject.com/en/dev/topics/db/models/#proxy-models) to indicate that these customers are stored in the same database table, and only the behaviour is different.

Update:

Your comments below taken into consideration, I'd recommend not locking yourself into an architectural decision until you know more about your use cases. Continuing on your last comment, it sounds like you have behavior that's mostly split in two:

  • Identifying a customer group
  • Applying an action to the customers of the identified group.

I would probably model this as two different concepts, and stitch them together with use case-aligned services.

Identifying customer groups can be encapsulated using a custom model manager. (https://docs.djangoproject.com/en/dev/topics/db/managers/#custom-managers). E.g.

class CustomerManager(models.Manager):
  def made_purchase_in(self, timespan):
    return self.filter(last_purchase__in=timespan)

The actions could be modelled anyway you want, e.g. as methods on the Customer or as classes or functions that accepts customers as parameters:

class Customer(model.Model):
  ..
  def remind(self, email):
    self.send_mail(email)

or

class RegularCustomerReminder:
  def __init__(self, customers):
    self.customers = customers

  def remind(self):
    for customer in self.customers:
      customer.send_email(self.make_reminder())

And an example service:

class CustomerPromotionService:
  def send_promotion(self):
    customers = Customer.objects.made_purchase_in(timespan.LAST_30_DAYS)
    reminder = RegularCustomerReminder(customers)
    reminder.remind()