skip to content
alcher.dev

Lith Labs 009: Refactor to Apps

/ 3 min read

Part of the Lith Labs series

Goal

The goal is to split out the monolithic lith app into multiple modular apps.

But why?

There is nothing inherently wrong with a monolithic Django app, particularly if publishing of apps is not an immediate concern. A single Django app with domains as submodules is an idiomatic and natural way to write Python.

Django apps on the other hand is Django’s recommended mechanism to separate distinct domains. While this is technically breaks Inversion of Control as the domain is now tied into the framework’s interface, as long as each domain’s internals are properly hidden and interfaces are correctly respected, practical DDD is still possible.

Least of all the reasons to go for multiple Django apps is the familiarity of the community. Being the default way of working is pretty hard to beat!

The game plan

To illustrate, lith currently has this directory structure in place for its templates:

src/lith/templates
`-- lith
    |-- auth
    |-- dashboard
    |   `-- partials
    |-- marketing
    |   `-- partials
    `-- partials

I can point out three app candidates based on this structure:

  1. authnz for authentication and authorization (auth is a reserved app name)
  2. dashboard for top-level dashboard concerns (specific concerns would get their own app)
  3. marketing for public-facing pages

Creating the apps

To start, I create the apps:

$ make "startapp authnz"
$ make "startapp dashboard"
$ make "startapp marketing"

And installed them:

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "authnz.apps.AuthnzConfig",
    "dashboard.apps.DashboardConfig",
    "marketing.apps.MarketingConfig",
    "crispy_forms",
    "crispy_bootstrap5",
]

Moving the logic to each app

The actual refactoring is an involved process, and each detail is better explored in the ensuing branch. But the steps came down to these:

  1. Start by moving the tests to their respective apps
  2. Copy the source files to their respective apps
  3. Use the IDE’s refactoring tools to autoupdate the import paths
  4. Delete the lith app
  5. Guided by tests, update the import paths that the IDE can’t figure out
  6. Any non-app specific template / static file goes to the top-level template and static directories, respectively
  7. Run the linter and formatter
  8. Do a manual test to confirm nothing broke

Final Outcome

The refactored structure now looks like this:

src/
|-- authnz
|   |-- __init__.py
|   |-- admin.py
|   |-- apps.py
|   |-- forms.py
|   |-- migrations
|   |   |-- 0001_initial.py
|   |   `-- __init__.py
|   |-- models.py
|   |-- models_tests.py
|   |-- templates
|   |   `-- authnz
|   |-- urls.py
|   |-- views.py
|   `-- views_tests.py
|-- config
|   |-- __init__.py
|   |-- __pycache__
|   |-- asgi.py
|   |-- settings.py
|   |-- urls.py
|   `-- wsgi.py
|-- dashboard
|   |-- __init__.py
|   |-- admin.py
|   |-- apps.py
|   |-- migrations
|   |   `-- __init__.py
|   |-- models.py
|   |-- templates
|   |   `-- dashboard
|   |       `-- partials
|   |-- urls.py
|   |-- views.py
|   `-- views_tests.py
|-- manage.py
|-- marketing
|   |-- __init__.py
|   |-- admin.py
|   |-- apps.py
|   |-- migrations
|   |   `-- __init__.py
|   |-- models.py
|   |-- templates
|   |   `-- marketing
|   |       `-- partials
|   |-- tests.py
|   |-- urls.py
|   |-- views.py
|   `-- views_tests.py
|-- static
|   `-- lith
|       |-- css
|       |   `-- vendor
|       |       `-- tabler
|       `-- img
`-- templates
    `-- lith
        `-- partials

Conclusion

In this lab, I refactored the monolithic lith Django app into reusable apps and used the existing test suite to protect from regression bugs.

The source is available at the feature/009-refactor-to-apps branch.