How to bind multiple reusable Django apps together?

后端 未结 4 1939
忘了有多久
忘了有多久 2020-12-23 17:59

I try my best to write reusable Django apps. Now I\'m puzzled how to put them all together to get the final project.

Here is an example of what I mean: I have a pict

4条回答
  •  暖寄归人
    2020-12-23 18:15

    I am a Django newbie and I faced the same problem. It is shameful how there's so much buzz about reusable apps in the Django community, but not a single authoritative reference on how to connect them in a project without hard coded dependencies.

    I have been working on a new Django project and I wanted to set up my models to avoid hard coding as much as possible. This is the pattern that I used.

    Layout

    project_root/
        core/
            models/
                mixinmodels1.py
                mixinmodels2.py
                ...
            utils.py
            ...
        app1/
            models/
                __init__.py
                base.py
                basemixins.py
                mixins.py
                concrete.py
            /signals
                __init__.py
                handlers.py
            utils.py
            ...
        app2/
            ...
        ...
    

    App models

    base.py : This module implements only the 'reason for existence' of that app in abstract classes. Rules are:

    • This module usually only imports from the core app. It usually does not import anything from other apps in the same project.

    • An exception to the above rule is when the 'reason for existence' assumes the existence of another app. For example, a group app assumes that there is an user app somewhere. In this case, the way to link them is:

      # project_root/settings.py
      
      AUTH_USER_MODEL = 'app_label.UserModel'
      
      # project_root/groups/models/base.py
      
      from django.conf import settings
      

      and then using settings.AUTH_USER_MODEL to refer to the user model

    • Use this pattern for all apps, not just the user app. For example, you should also do

      # project_root/settings.py
      
      GROUP_MODEL = 'app_label.GroupModel'
      
    • If using the above pattern, only assume the functionality provided by base.py of the other app to which you are linking. Do not assume the functionality of elaborate concrete classes (I will discuss where to put concrete classes shortly)

    • Of course, importing from django, third party apps and python packages is allowed.

    • Make sure that the assumptions that you make in base.py of any app is rock solid and would not change much in the future. A good example is django-registration by James Bennett. Its an old app, but its appeal did not wane because it made rock solid assumptions. Since good reusable apps do one thing well, it is not difficult to find that set of assumptions.

    basemixins.py: This module should implements plugs to the concrete models of that app. A 'plug' to a model M is any model that contains a foreign key to the model M. For example:

    # project_root/groups/models/basemixins.py
    
    from django.conf import settings
    from django.db import models
    
    class BaseOwnedByGroup(models.Model):
        """
        This is a plug to the group model. Use this
        to implement ownership like relations with 
        the group model
        """
        owner = models.ForeignKey(settings.GROUP_MODEL,
            related_name = '%(app_label)s_%(class)s_owner',
            verbose_name = 'owner')
    
        # functionality and manager definitions go here.
    
        class Meta:
            abstract = True
            app_label = 'groups'
    

    BaseOwnedByGroup is a 'plug' to the group model. The rules here are the same as 'base.py`

    • While defining 'plugs' in basemixins.py, only assume functionality provided by base.py.
    • Import only from core, django, third party apps and python packages.

    mixins.py : This module should be used for two purposes

    • To define elaborate 'plugs', which assumes the functionality of the elaborate concrete classes but not the relationships with other apps. The elaborate plugs should ideally inherit one of the 'base plugs' defined in basemixins.py.

    • To define mixin models (that are not 'plugs') which can be used by the concrete classes of that app.

    concrete.py : This module should be used to define (you guessed it) concrete classes of that apps and to set up relationships with other apps. In short, this module assumes your project and all the functionality you want to provide in it.

    Relationships to other apps should be set up as follows:

    • To establish a one to one or many to one relationship with model M of app another_app, do the following:

      # project_root/another_app/utils.py
      
      def plug_to_M_factory(version_label):
          """
          This is a factory method which returns
          the plug to model M specified by 
          version_label
          """
          if version_label == 'first_version':
              from another_app.models.basemixins import BasePlugToM
              return BasePlugToM
          if version_label == 'second_version':
              from another_app.models.mixins import PlugToM
              return PlugToM
          ...
      
      # project_root/groups/models/concrete.py
      
      from groups.models.base import BaseGroup
      from another_app.utils import plug_to_M_factory
      
      PlugToMClass = plug_to_M_factory(version_label = 'second_version')
      
      class ConcreteGroup(BaseGroup, PlugToMClass):
          # define your concrete model
      
          class Meta:
              app_label = 'groups'
      
    • To establish a many to many relationship, the recommended way is to use a through model. Inherit the correct plug in the through model in the exact same way (as we did in the ConcreteGroup model)

    signals : While setting up relationships, often we have to perform operations on a model M of app app1, when model N of app app2 changes. You can use signals to handle that. Your handlers can assume the functionality of the concrete sender, but often they don't need to. Assumption of the base version of the sender in base.py is enough. This is why it is a good idea to always use settings.ModelName to refer to a concrete model. You can extract out the model class from the settings.ModelName string by either using ContentType or using a project wide get_model_for_settings function as follows:

    # project_root/project/utils.py
    
    from django.db.models import get_model
    from django.core.exceptions import ImproperlyConfigured
    
    def get_model_from_settings(model_string):
        """
        Takes a string of the form 'app_label.model' as input, returns the 
        appropriate model class if it can find it.
        """
        try:
            app_label, model_name = model_string.split('.')
        except ValueError:
            raise ImproperlyConfigured("function argument must be of the " 
                "form 'app_label.model_name', got '%s'" % model_string)
    
        model = get_model(app_label, model_name)
    
        if model is None:
            raise ImproperlyConfigured("function argument refers to model "
                "'%s' that has not been installed" % model_string)
    
        return model
    

    core : The core app is a special app which stores project wide mixin functions.

    • These mixins should not assume anything about any other app. Only exception to this rule is mixins that rely on base functionality of settings.AUTH_USER_MODEL. This is because you can safely assume that most projects will have an user model.

    • Of course imports from django, third party and python packages are still allowed

    • Remember that all base.py and basemixins.py modules are allowed to import from core.

    Finally for everything to work as intended, import your models in models/__init__.py of every app.

    The advantages that I find from following this scheme is:

    • The models are reusable. Anyone can use the abstract base models and mixins to design their own concrete model. base.py, basemixins.py and related factory methods can be bundled together with a bare bones concrete model and shipped in a reusable app.

    • The apps are extendable. All mixins are versioned and there is a clear inheritance scheme.

    • The apps are loosely coupled. External mixins are accessed via factory methods and external models are referred to using django.conf.settings.

    • The apps are self contained. Any changes in an app will most likely break that app and that app only. Other apps will most likely remain unscathed. Even if external apps break, the place where this could happen is clearly marked.

    I have been using this scheme to reduce coupling between my apps. I am not an experienced Django programmer, and I have a lot to learn, so any feedback is appreciated.

提交回复
热议问题