Machines d'état et Django

Lorsque vous travaillez sur un projet Django, il existe un certain nombre de bibliothèques tierces indispensables si vous ne voulez pas réinventer la roue sans cesse. Un outil pour déboguer les requêtes SQL (debug-toolbar, silk, --print-sql de django-extensions), quelque chose pour stocker des arborescences, des tâches périodiques / différées (au fait, uswgi a une interface de type cron . EAV est toujours nécessaire , bien qu'il puisse souvent être remplacé par jsonfield. Et l'une de ces choses extrêmement utiles, mais pour une raison moins souvent discutée sur le net est FSM. Pour une raison quelconque, je ne les rencontre pas souvent dans le code de quelqu'un d'autre.





Presque tous les enregistrements de la base de données ont un état. Par exemple, pour un commentaire, il peut être - publié / supprimé / supprimé par un modérateur. Pour commander en magasin - émis / payé / livré / retourné, etc. De plus, le passage d'un état à un autre est souvent étalé sur le code et il y a une logique métier dedans, qui doit être abondamment couverte de tests (il faut quand même, mais on peut éviter de tester des choses élémentaires, par exemple, qu'une commande ne peut passer à l'état « remboursement » qu'après avoir été en état « payé ».





Il serait tout à fait logique de décrire ces transitions de manière plus déclarative et en un seul endroit. Avec la logique nécessaire et la vérification d'accès.





Voici un exemple de code issu des tests de la bibliothèque django-fsm





class BlogPost(models.Model):
    """
    Test workflow
    """
    state = FSMField(default='new', protected=True)

    def can_restore(self, user):
        return user.is_superuser or user.is_staff

    @transition(field=state, source='new', target='published',
                on_error='failed', permission='testapp.can_publish_post')
    def publish(self):
        pass

    @transition(field=state, source='published')
    def notify_all(self):
        pass

    @transition(field=state, source='published', target='hidden', on_error='failed',)
    def hide(self):
        pass

    @transition(
        field=state,
        source='new',
        target='removed',
        on_error='failed',
        permission=lambda self, u: u.has_perm('testapp.can_remove_post'))
    def remove(self):
        raise Exception('No rights to delete %s' % self)

    @transition(field=state, source='new', target='restored',
                on_error='failed', permission=can_restore)
    def restore(self):
        pass

    @transition(field=state, source=['published', 'hidden'], target='stolen')
    def steal(self):
        pass

    @transition(field=state, source='*', target='moderated')
    def moderate(self):
        pass

    class Meta:
        permissions = [
            ('can_publish_post', 'Can publish post'),
            ('can_remove_post', 'Can remove post'),
        ]
      
      



C'est parfait pour l'API de repos, entre autres. Nous pouvons créer automatiquement des points de terminaison pour les transitions entre les états. Par exemple, la requête/commandes/id/annulation ressemble à une action parfaitement logique pour un ensemble de vues. Et nous avons déjà les informations nécessaires pour vérifier l'accès ! Et aussi pour les boutons dans le panneau d'administration, et la possibilité de dessiner de beaux graphiques avec le workflow :) Il existe même des éditeurs de workflow visuels, c'est-à-dire les non-programmeurs peuvent décrire les processus métier





Plus nous écrivons de code déclaratif et générique, plus il est fiable. Moins de code, moins de duplication, moins de bugs. Les tests sont partiellement transférés à l'auteur de la bibliothèque et vous pouvez vous concentrer sur la logique métier propre au projet.








All Articles