Il semble que vous demandiez quelle est la différence entre le modèle de données et le modèle de domaine - Le second est l'endroit où vous pouvez trouver la logique commerciale et les entités telles que perçues par votre utilisateur final, le premier est l'endroit où vous stockez réellement vos données.
En outre, j'ai interprété la troisième partie de votre question comme suit : comment remarquer l'absence de séparation de ces modèles ?
Ce sont deux concepts très différents et il est toujours difficile de les séparer. Toutefois, il existe des modèles et des outils communs qui peuvent être utilisés à cette fin.
À propos du modèle de domaine
La première chose que vous devez reconnaître, c'est que votre modèle de domaine ne porte pas vraiment sur les données, mais plutôt sur actions y questions tels que "activer cet utilisateur", "désactiver cet utilisateur", "quels utilisateurs sont actuellement activés ?", et "quel est le nom de cet utilisateur ?". En termes classiques : il s'agit de demandes de renseignements y commandes .
Penser en termes de commandes
Commençons par examiner les commandes de votre exemple : "activer cet utilisateur" et "désactiver cet utilisateur". Ce qui est bien avec les commandes, c'est qu'elles peuvent facilement être exprimées par de petits scénarios de type "donné, quand, alors" :
donné un utilisateur inactif
quand l'administrateur active cet utilisateur
puis l'utilisateur devient actif
y un e-mail de confirmation est envoyé à l'utilisateur
y une entrée est ajoutée au journal du système
(etc. etc.)
De tels scénarios sont utiles pour voir comment différentes parties de votre infrastructure peuvent être affectées par une seule commande - dans ce cas, votre base de données (une sorte de drapeau "actif"), votre serveur de messagerie, votre journal système, etc.
De tels scénarios vous aident également à mettre en place un environnement de développement piloté par les tests.
Enfin, le fait de penser en termes de commandes vous aide réellement à créer une application orientée vers les tâches. Vos utilisateurs l'apprécieront :-)
Expression des commandes
Django fournit deux façons simples d'exprimer les commandes ; ce sont toutes deux des options valables et il n'est pas inhabituel de mélanger les deux approches.
La couche de service
En module de service a déjà été décrit par @Hedde . Ici, vous définissez un module distinct et chaque commande est représentée par une fonction.
services.py
def activate_user(user_id):
user = User.objects.get(pk=user_id)
# set active flag
user.active = True
user.save()
# mail user
send_mail(...)
# etc etc
Utilisation des formulaires
L'autre moyen est d'utiliser un formulaire Django pour chaque commande. Je préfère cette approche, car elle combine plusieurs aspects étroitement liés :
- exécution de la commande (que fait-elle ?)
- validation des paramètres de la commande (peut-elle le faire ?)
- présentation de la commande (comment faire ?)
forms.py
class ActivateUserForm(forms.Form):
user_id = IntegerField(widget = UsernameSelectWidget, verbose_name="Select a user to activate")
# the username select widget is not a standard Django widget, I just made it up
def clean_user_id(self):
user_id = self.cleaned_data['user_id']
if User.objects.get(pk=user_id).active:
raise ValidationError("This user cannot be activated")
# you can also check authorizations etc.
return user_id
def execute(self):
"""
This is not a standard method in the forms API; it is intended to replace the
'extract-data-from-form-in-view-and-do-stuff' pattern by a more testable pattern.
"""
user_id = self.cleaned_data['user_id']
user = User.objects.get(pk=user_id)
# set active flag
user.active = True
user.save()
# mail user
send_mail(...)
# etc etc
Penser en termes de requêtes
Votre exemple ne contenait aucune requête, j'ai donc pris la liberté d'inventer quelques requêtes utiles. Je préfère utiliser le terme "question", mais "requête" est la terminologie classique. Les requêtes intéressantes sont : "Quel est le nom de cet utilisateur ?", "Cet utilisateur peut-il se connecter ?", "Montrez-moi une liste des utilisateurs désactivés", et "Quelle est la distribution géographique des utilisateurs désactivés ?".
Avant de vous lancer dans la réponse à ces questions, vous devriez toujours vous poser cette question : est-ce que.. :
- a présentation une requête juste pour mes modèles, et/ou
- a logique d'entreprise une requête liée à l'exécution de mes ordres, et/ou
- a reporting requête.
Les requêtes de présentation sont simplement effectuées pour améliorer l'interface utilisateur. Les réponses aux requêtes de logique métier affectent directement l'exécution de vos commandes. Les requêtes de rapport sont simplement destinées à des fins analytiques et ont des contraintes de temps plus lâches. Ces catégories ne s'excluent pas mutuellement.
L'autre question est : "est-ce que j'ai un contrôle total sur les réponses ?" Par exemple, lorsque nous interrogeons le nom de l'utilisateur (dans ce contexte), nous n'avons aucun contrôle sur le résultat, car nous nous appuyons sur une API externe.
Faire des requêtes
La requête la plus basique dans Django est l'utilisation de l'objet Manager :
User.objects.filter(active=True)
Bien entendu, cela ne fonctionne que si les données sont effectivement représentées dans votre modèle de données. Ce n'est pas toujours le cas. Dans ces cas, vous pouvez envisager les options ci-dessous.
Balises et filtres personnalisés
La première solution est utile pour les requêtes qui ne sont que de pure présentation : balises personnalisées et filtres de modèles.
modèle.html
<h1>Welcome, {{ user|friendly_name }}</h1>
template_tags.py
@register.filter
def friendly_name(user):
return remote_api.get_cached_name(user.id)
Méthodes d'interrogation
Si votre requête n'est pas purement formelle, vous pouvez ajouter des requêtes à votre site Web. services.py (si vous l'utilisez), ou introduisez un fichier queries.py module :
queries.py
def inactive_users():
return User.objects.filter(active=False)
def users_called_publysher():
for user in User.objects.all():
if remote_api.get_cached_name(user.id) == "publysher":
yield user
Modèles de proxy
Les modèles proxy sont très utiles dans le contexte de la logique d'entreprise et du reporting. Vous définissez essentiellement un sous-ensemble amélioré de votre modèle. Vous pouvez remplacer l'ensemble de requêtes de base d'un gestionnaire en modifiant l'attribut Manager.get_queryset()
méthode.
models.py
class InactiveUserManager(models.Manager):
def get_queryset(self):
query_set = super(InactiveUserManager, self).get_queryset()
return query_set.filter(active=False)
class InactiveUser(User):
"""
>>> for user in InactiveUser.objects.all():
… assert user.active is False
"""
objects = InactiveUserManager()
class Meta:
proxy = True
Modèles de requête
Pour les requêtes qui sont intrinsèquement complexes, mais qui sont exécutées assez souvent, il est possible d'utiliser des modèles de requêtes. Un modèle de requête est une forme de dénormalisation où les données pertinentes pour une seule requête sont stockées dans un modèle séparé. L'astuce consiste bien sûr à garder le modèle dénormalisé en synchronisation avec le modèle primaire. Les modèles de requête ne peuvent être utilisés que si les modifications sont entièrement sous votre contrôle.
models.py
class InactiveUserDistribution(models.Model):
country = CharField(max_length=200)
inactive_user_count = IntegerField(default=0)
La première option consiste à mettre à jour ces modèles dans vos commandes. C'est très utile si ces modèles ne sont modifiés que par une ou deux commandes.
forms.py
class ActivateUserForm(forms.Form):
# see above
def execute(self):
# see above
query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
query_model.inactive_user_count -= 1
query_model.save()
Une meilleure option serait d'utiliser des signaux personnalisés. Ces signaux sont bien sûr émis par vos commandes. Les signaux ont l'avantage de vous permettre de garder plusieurs modèles de requêtes en synchronisation avec votre modèle d'origine. De plus, le traitement des signaux peut être déchargé vers des tâches d'arrière-plan, en utilisant Celery ou des frameworks similaires.
signaux.py
user_activated = Signal(providing_args = ['user'])
user_deactivated = Signal(providing_args = ['user'])
forms.py
class ActivateUserForm(forms.Form):
# see above
def execute(self):
# see above
user_activated.send_robust(sender=self, user=user)
models.py
class InactiveUserDistribution(models.Model):
# see above
@receiver(user_activated)
def on_user_activated(sender, **kwargs):
user = kwargs['user']
query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
query_model.inactive_user_count -= 1
query_model.save()
Maintenir la propreté
En utilisant cette approche, il devient ridiculement facile de déterminer si votre code reste propre. Il suffit de suivre ces directives :
- Mon modèle contient-il des méthodes qui font plus que gérer l'état de la base de données ? Vous devriez extraire une commande.
- Mon modèle contient-il des propriétés qui ne correspondent pas aux champs de la base de données ? Vous devez extraire une requête.
- Mon modèle fait-il référence à une infrastructure qui n'est pas ma base de données (comme le courrier) ? Vous devriez extraire une commande.
Il en va de même pour les vues (car les vues souffrent souvent du même problème).
- Ma vue gère-t-elle activement les modèles de base de données ? Vous devriez extraire une commande.
Quelques références
Documentation Django : modèles de proxy
Documentation Django : signaux
Architecture : Conception pilotée par le domaine
16 votes
Lire à propos des signaux
1 votes
Vous avez supprimé l'étiquette mais vous pourriez utiliser l'ICD pour réaliser la séparation entre ce que le système fait (la fonctionnalité) et ce qu'il est (le modèle de données/domaine).
3 votes
Vous proposez d'implémenter toute la logique commerciale dans des rappels de signaux ? Malheureusement, toute mon application ne peut pas être liée à des événements dans la base de données.
0 votes
Rune FS, j'ai essayé d'utiliser l'ICD, mais il m'a semblé qu'il n'en fallait pas beaucoup pour mon projet : Contexte, définition des rôles comme mixin aux objets, etc. Il existe un moyen plus simple de séparer "fait" et "est" ? Pourriez-vous donner un exemple minimal ?