5

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.

1 Answer 1

5

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()
4
  • That's interesting, though I wouldn't want to use proxies at the moment as the posh / sporty was just an example to show work on many model instances. I don't think it matters where I'd like to call add_bonus(). I want the ability to call it from the shell, through a GUI, etc.
    – duality_
    May 26, 2014 at 15:56
  • Do you think you could elaborate a bit on your use case. I usually find that functional requirements and use-cases are the best drivers of design decisions.
    – tpresthus
    May 28, 2014 at 10:27
  • Not sure yet what I'll need, but I know it'll be things that include multiple customers. Things like "add a coupon to customers that bought something in the last month", "send email to customers that haven't visited in three months", etc.
    – duality_
    May 28, 2014 at 15:56
  • I've updated my answer with a new proposal aligned with your comment above.
    – tpresthus
    May 30, 2014 at 9:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.