Zastanawialiśmy się ostatnimi czasy, w jaki sposób zbudować aplikację tak, by z jednej strony była uniwersalna (abstrakcyjne modele bazowe), a z drugiej zawierała całą niezbędną logikę. Problem polega na tym, że — z oczywistych względów — klucza obcego do modelu abstrakcyjnego stworzyć się nie da. Pozostaje więc zwalić pracę na programistę, który konkretyzuje abstrakcyjne prototypy, prawda?
Dzisiaj do głowy przyszło mi rozwiązanie może nie do końca ładne, ale na pewno działające. Fabryka abstrakcyjnych modeli:
from django.db import models
class AbstractModelMetaclass(type):
def __new__(cls, name, bases, attrs):
super_new = super(AbstractModelMetaclass, cls).__new__
if bases == (object, ):
return super_new(cls, name, bases, attrs)
new_attrs = attrs.copy()
module = attrs['__module__']
new_cls = super_new(cls, name, bases, {'__module__': module})
fields = getattr(new_cls, '_fields', {}).copy()
for key, val in attrs.items():
if key not in ['construct', '__new__']:
fields[key] = val
new_attrs['_fields'] = fields
return super_new(cls, name, bases, new_attrs)
class AbstractModelFactory(object):
__metaclass__ = AbstractModelMetaclass
@staticmethod
def construct():
return {}
def __new__(cls, *args, **kwargs):
attrs = cls._fields.copy()
attrs.update(cls.construct(*args, **kwargs))
attrs['__module__'] = cls.__module__
attrs['Meta'] = type('Meta', (), {'abstract': True})
return type(cls.__name__, (models.Model, ), attrs)
Idea jest taka, że implementując swoją podklasę fabryki modeli mamy możliwość zadecydować o polach, jakie trafią do ostatecznego modelu. Dla lepszego zrozumienia, rozważmy abstrakcyjne klasy produktu i kategorii:
class CategoryFactory(AbstractModelFactory):
name = models.CharField(max_length=100)
def __unicode__(self):
return self.name
class ProductFactory(AbstractModelFactory):
name = models.CharField(max_length=100)
@staticmethod
def construct(category):
return {'category': models.ForeignKey(category)}
def __unicode__(self):
return u'%s / %s' % (self.category, self.name)
Skonkretyzujmy klasy własną implementacją:
class MyCategory(CategoryFactory()):
pass
class MyProduct(ProductFactory(category=MyCategory)):
pass
Upewnijmy się, że zależności działają prawidłowo:
from . import models
c = models.MyCategory(name='cat')
p = models.MyProduct(name='prod', category=c)
print p # 'cat / prod'
Oczywiście — ktoś zaraz powie — to samo można uzyskać tworząc funkcję zwracającą klasę zdefiniowaną w jej ciele. Można, ale nie da się po niej wygodnie dziedziczyć (rozszerzenie istniejącej fabryki).