3

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.

4

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()
  • 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 '14 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 '14 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 '14 at 15:56
  • I've updated my answer with a new proposal aligned with your comment above. – tpresthus May 30 '14 at 9:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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