yunojuno / django-side-effects

Django app used to centralise and document external side effects
MIT License
18 stars 4 forks source link

[question] what do you use for internal side effects that do affect state? #35

Closed simkimsia closed 9 months ago

simkimsia commented 10 months ago

Do you go back to signals?

There's this django lifecycle which I like but it's restricted to just before and after save,create,delete,update.

hugorodgerbrown commented 10 months ago

No - even Django themselves don't fully endorse signals:

https://docs.djangoproject.com/en/4.2/topics/signals/

Signals give the appearance of loose coupling, but they can quickly lead to code that is hard to understand, adjust and debug. Where possible you should opt for directly calling the handling code, rather than dispatching via a signal.

A more robust / easier to test and debug pattern is having anything that updates state as an isolated function. We use a "commands" namespace for these functions. Models then store state only.

simkimsia commented 10 months ago

So what do you use personally for updating internal application state that's side effect of a function?

simkimsia commented 10 months ago

Sorry I realized you said commands namespace

Could you elaborate on this? And how do you tie the side effects to any requests coming in either as API or regular web requests?

Or maybe even management commands?

hugorodgerbrown commented 9 months ago

Wherever possible we try to ensure that model methods only ever update state of the model itself - so whenever we need a method that does some business logic - updating multiple objects / models, we use a free-floating function in a commands namespace. This is what we decorate with side effects.

Here's an example. Say we have a model called Book, and that when we "publish" the book we update the publication_date of the book object, and also update a property on the book.author. In this case we might have a single Book.publish() method that updates the book itself, but does not update the author, and then a command function that calls this (to update the book), and also updates the author. This is the function that we decorate.

# books/models.py
class Book(models.Model):

    def publish(self) -> None:
       """Update publication date of the book."""
       self.publication_date = date.today()
       self.save(update_fields=["publication_date"])
# books/commands.py
@has_side_effects("publish_book")
def publish_book(book: Book) -> None:
    """Publish a new book."""
    book.publish()
    book.author.date_last_book_published = book.publication_date()
    book.author.save(update_fields=["date_last_book_published"])

A secondary benefit of this pattern is that the methods on the objects themselves do not fire side-effects, so you can always drop into the shell and update objects without accidentally firing off emails, notifications etc.