Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Loose coupling of apps & model inheritance

Tags:

I have a design question concerning Django. I am not quite sure how to apply the principle of loose coupling of apps to this specific problem:

I have an order-app that manages orders (in an online shop). Within this order-app I have two classes:

class Order(models.Model):
    # some fields
    def order_payment_complete(self):
        # do something when payment complete, ie. ship products
        pass

class Payment(models.Model):
    order = models.ForeignKey(Order)
    # some more fields     
    def save(self):
        # determine if payment has been updated to status 'PAID'
        if is_paid:
            self.order.order_payment_complete()
        super(Payment, self).save()

Now the actual problem: I have a more specialized app that kind of extends this order. So it adds some more fields to it, etc. Example:

class SpecializedOrder(Order):
    # some more fields
    def order_payment_complete(self):
        # here we do some specific stuff
        pass

Now of course the intended behaviour would be as follows: I create a SpecializedOrder, the payment for this order is placed and the order_payment_complete() method of the SpecializedOrder is called. However, since Payment is linked to Order, not SpecializedOrder, the order_payment_complete() method of the base Order is called.

I don't really know the best way to implement such a design. Maybe I am completely off - but I wanted to build this order-app so that I can use it for multiple purposes and wanted to keep it as generic as possible.

It would be great if someone could help me out here! Thanks, Nino

like image 353
Nino Avatar asked Aug 11 '09 10:08

Nino


2 Answers

I think what you're looking for is the GenericForeignKey from the ContentTypes framework, which is shipped with Django in the contrib package. It handles recording the type and id of the subclass instance, and provides a seamless way to access the subclasses as a foreign key property on the model.

In your case, it would look something like this:

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

class Payment(models.Model):

    order_content_type = models.ForeignKey(ContentType)
    order_object_id = models.PositiveIntegerField()
    order = generic.GenericForeignKey('order_content_type', 'order_object_id')

You don't need to do anything special in order to use this foreign key... the generics handle setting and saving the order_content_type and order_object_id fields transparently:

s = SpecializedOrder()
p = Payment()
p.order = s
p.save()

Now, when your Payment save method runs:

if is_paid:
    self.order.order_payment_complete()  # self.order will be SpecializedOrder
like image 159
Jarret Hardie Avatar answered Oct 12 '22 07:10

Jarret Hardie


The thing you want is called dynamic polymorphism and Django is really bad at it. (I can feel your pain)

The simplest solution I've seen so far is something like this:

1) Create a base class for all your models that need this kind of feature. Something like this: (code blatantly stolen from here)

class RelatedBase(models.Model):
    childclassname = models.CharField(max_length=20, editable=False)

    def save(self, *args, **kwargs):
        if not self.childclassname:
            self.childclassname = self.__class__.__name__.lower()
        super(RelatedBase, self).save(*args, **kwargs) 

    @property
    def rel_obj(self):
        return getattr(self, self.childclassname)

    class Meta:
        abstract = True

2) Inherit your order from this class.

3) Whenever you need an Order object, use its rel_obj attribute, which will return you the underlying object.

This solution is far from being elegant, but I've yet to find a better one...

like image 31
Maxim Sloyko Avatar answered Oct 12 '22 09:10

Maxim Sloyko