Casper van der Wel 1 year ago
parent
commit
df2df11ec4
1 changed files with 86 additions and 46 deletions
  1. 86 46
      README.md

+ 86 - 46
README.md

@@ -1,77 +1,117 @@
 # clean-python
 
-Introduction
+``clean-python`` contains abstractions for *clean architecture* in Python
 
-Usage, etc.
+using asyncio. It is independent of frameworks.
 
-## Installation
+The terminology used is consistently derived from the "Big Blue Book" (Domain Driven Design by E. Evans, 2004). Software consists of one or more modules, each having four layers: presentation, application, domain, and infrastructure. Each layer has its own responsibilities, in short:
 
-We can be installed with:
+- presentation: show information to the user and interpret the user's commands.
+- application: implement use cases that direct the domain objects.
+- domain: all domain concepts and rules; this layer is the heart of the software.
+- infrastructure: generic capabilities that support the higher layers
 
-    $ pip install clean-python
+A big inspiration for this was the ``easy`` typescript framework by S. Hoogendoorn and others
+(https://github.com/thisisagile/easy).
+
+## Motivation
+
+The main goals of using layered architecture is isolating the domain-specific concepts from
+other functions related only to software technology. In this way:
+
+- The knowledge embedded in the domain is distilled and can more easily be 
+  understood and changed.
+- Developers are able to quickly grasp the code base because it uses
+  a consistent structure and naming system.
+- Depenencies are reduced resulting in a higher maintainability.
+- Unittests can be made more easily (increasing reliability).
+
+## Dependencies
+Layers are loosly coupled with dependencies in only one direction: presentation > application > infrastructure > domain. In other words: the number of dependencies of the software's core business are as limited as possible.
+
+A module may only depend on another module though its infrastructure layer. See ``InternalGateway``.
 
-(TODO: after the first release has been made)
+This library was initially developed as a web backend using FastAPI. Its core dependencies are ``pydantic``, ``inject`` and ``asgiref``. Optional dependencies may be added in case for instance an ``SQLGateway`` is needed.
 
+## Core concepts
 
-## Development installation of this project itself
+### Domain Layer
 
-We use python's build-in "virtualenv" to get a nice isolated
-directory. You only need to run this once:
+The domain layer is where the model lives. The domain model is a set of concepts; the domain layer
+is the manifestation of that model. Concepts in the domain model must have a 1:1 representation in the
+code and vice versa. 
 
-    $ python3 -m venv .
+THe layer does not depend on all other layers. Interaction with the infrastructure layer may be done
+using dependency injection from the application layer. It is allowable to have runtime dependencies on the
+infrastructure layer to set for instance default ``Gateway`` implementations.
 
-A virtualenv puts its commands in the `bin` directory. So `bin/pip`,
-`bin/pytest`, etc. Set up the dependencies like this:
+There are 5 kinds of objects in this layer:
 
-    $ bin/pip install -e .[test]
+- *Entity*: Types that have an identity (all attributes of an instance may change- but the instance is still the same)
+  Entities have an ``id`` and default fields associated with state changes ()``created_at``, ``updated_at``). 
+- *ValueObject*: Types that have no identity (these are just complex values like a datetime).
+- *DomainService*: Important domain operations that aren't natural to model as objects. A service is stateless.
+- *Repository*: A repository is responsible for persistence (``add`` / ``get`` / ``filter``). This needs
+  a *Gateway* to interface with e.g. a database; an instance of a *Gateway* is typically injected into a
+  Repository from the application layer.
+- *DomainEvent*: A domain event may be emitted to signal a state change.
+  
+Associations between objects are hard. Especially many-to-many relations. We approach this by grouping objects
+into *aggregates*. An aggregate is a set of objects that change together / have the same lifecycle (e.g. delete together). One entity is the aggregate root; we call this the ``RootEntity``. A ``ChildEntity`` occurs only very
+rarely; mostly a nested object derive its identity from a ``RootEntity``.
 
-There will be a script you can run like this:
+All change and access goes through the repository of a ``RootEntity``. The ``RootEntity`` can be a complicated
+nested object; how to map this to an SQL database is the issue of the infrastructure layer.
 
-    $ bin/run-clean-python
+### Infrastructure Layer
 
-It runs the `main()` function in `[clean-python/scripts.py`,
-adjust that if necessary. The script is configured in
-`TODO, MISSING NOW` (see `entry_points`).
+An infrastructure layer primarily contains ``Gateway`` objects that interface with a single external resource.
+The ``Gateway`` implements persistence methods to support the domain and application layers. Much of the implementation will be in frameworks or other dependencies.
 
-In order to get nicely formatted python files without having to spend
-manual work on it, get [pre-commit](https://pre-commit.com/) and install
-it on this project:
+The methods of a ``Gateway`` may directly return a domain object, or return a dictionary with built-in types (``Json``).
 
-    $ pre-commit install
+Other gateway examples are: email sending and logstash logging.
 
-Run the tests regularly with coverage:
+### Application layer
 
-    $ bin/pytest --cov
+The application layer defines the use cases of the application. Example use cases are `create_user` or `list_user_roles`. These methods have nothing to do with a REST API or command-line interface; this is
+the business of the presentation layer.
 
-The tests are also run automatically [on "github
-actions"](https://github.com/nens/clean-python/actions) for
-"main" and for pull requests. So don't just make a branch, but turn it into a
-pull request right away. On your pull request page, you also automatically get
-the feedback from the automated tests.
+In addition to directing the domain objects, an application layer method could trigger other behavior
+like logging or triggering other applications. At first, it may as well be just a single function call.
 
-If you need a new dependency (like `requests`), add it in
-`pyproject.toml` in `dependencies`. And update your local install with:
+This layer is kept thin. It directs domain objects, and possibly interacts with other systems 
+(for instance by sending a message through the infrastructure layer). The application layer should
+not contain fundamental domain rules.
 
-    $ bin/pip install -e .[test]
+### Presentation Layer
 
+The presentation layer shows information to the user and interprets the user's commands.
+Its main job is to get the application-layer use cases to be usable for an actual user.
 
-## Steps to do after generating with cookiecutter
+The currently only option in ``clean-python`` is a REST API using FastAPI.
 
-- Add a new project on <https://github.com/nens/> with the same name.  Think
-  about the visibility to ("public" / "private") and do not generate a
-  license or readme.
+## Modules
 
-  Note: "public" means "don't put customer data or sample data with real
-  persons' addresses on github"!
+The primary objective of compartimentalizing code into modules is to prevent cognitive overload.
+The modules divide the domain layer, everything else follows. There should be low coupling
+between modules and high cohesion whithin a module. Modules are first and foremost a conceptual
+structure.
 
-- Follow the steps you then see (from "git init" to "git push origin main")
-  and your code will be online.
+In Python, a module should be implemented with a single .py file or a folder of .py files (respectively
+called modules and packages).
 
-- Go to
-  https://github.com/nens/>clean-python/settings/collaboration
-  and add the teams with write access (you might have to ask someone with
-  admin rights (like Reinout) to do it).
+Modules have a public API (presentation layer) and encapsulate their database. Only in this way
+the internal consistency can be guaranteed by the module's domain layer.
+
+Our current approach is to have 1 *aggregate* (whose root is implemented as a ``RootEntity``) per module.
+
+## Installation
+
+``clean-python`` can be installed with:
+
+    $ pip install clean-python
 
-- Update this readme.
+Optional dependencies can be added with:
 
-- Remove this section as you've done it all :-)
+    $ pip install clean-python[sql,fastapi]