Phoenix - Contexts
02 Dec 2018- ramblings about module naming in Elixir and code organization
- CQRS approach
- breaking up a context into smaller pieces with defdelegate
- using umbrella apps as bounded contexts
- secondary contexts
ramblings about module naming in Elixir and code organization
as far as I can see folks prefer nouns to verbs when naming modules.
in this regard module acts like a namespace that groups related actions together.
say, in Ruby I might have several operations User::Create
, User::Update
, etc.
with a single call
method (so called function objects) - in Elixir most likely
I would have a single User
module with several functions create
, update
,
etc. inside.
moreover User
module might define a struct and have functions working with
this struct inside the same module - of course struct must be passed to each
such function (modules are stateless).
if some functions inside User
module can be grouped further it’s possible to
create another module (= namespace) that is nested inside User
(but usually
stored in a separate file in user/ directory) - User.Authorization
module
for example. and so on and on…
that is why IMO Elixir modules tend to be bigger than Ruby classes.
UPDATE
the same principles apply to contexts (new feature of Phoenix 1.3) - they group common functionality together (contexts are top-level domains of your applicaiton). contexts can be further divided into subdomains (say, I have such subdomains named after schemas - they contain operations, queries, etc. that refer to (= bounded by) that specific schema).
CQRS approach
when using context most operations that constitute context public interface and are used internally inside context are implemented inside context module itself. complex operations can be extracted into their own modules (services, operations) bounded by schemas as described above.
in CQRS approach it’s recommended to separate read and write operations and group them by related schema inside corresponding loaders and mutators. in this case it’s possible to expose only those operations via context public interface (by importing these operations or by creating wrapper functions) which are meant to be used outside current context (in other contexts or in web part of Phoenix application). all other operations are considered to be internal and should be used inside current context by calling functions from required loaders and mutators directly.
implementing most operations in schema loaders and mutators and adding only required ones to context public interface has the benefit of not polluting the latter with operations which are not meant to be used outside current context.
still it makes sense to extract complex operations (which might have private
helper functions) into their own modules (services, operations) within schema
namespace - just like when not using CQRS approach. these operations can use
different schema loaders and mutators and can be added to context interface.
for example, these operations can be placed inside operations
namespace.
alternatively complex operations might be implemented as context functions if they are just a sequence of calls to corresponding loaders and mutators.
breaking up a context into smaller pieces with defdelegate
context module might grow big - it’s possible to split it into several modules
and then expose all their functions in a single “facade” context module using
Kernel.defdelegate/2
macro.
using umbrella apps as bounded contexts
it’s possible to create a separate app for each bounded context inside umbrella project:
Create an umbrella project;
One app for each bounded context;
An app should never call internal code from another app;
Use a module on each app to act as a public API to exchange data between apps, and also to call functions to mutate data;
An app should never return its internal data structures on public apis, it should return only raw data like maps and lists;
these rules can be further applied to ordinary Phoenix contexts inside separate apps within umbrella project or contexts inside non-umbrella project.
secondary contexts
currently all modules within context (primary context in terms of this article) can be accessed directly, contexts interact with each other via context modules which act as facades.
Devon Estes suggests isolating not only contexts but resources within contexts as well by introducing secondary contexts which are named after resources in plural form and contain both loader and mutator functions. I guess all resource related operations should be proxied via these secondary contexts as well.
this way it becomes very easy, say, to move a resource to another primary context but at the same time it increases complexity (IMO) because it makes you create facades within facades (secondary contexts within primary ones) instead of using resource related modules directly within primary contexts.
all in all this idea is worth paying attention to but for the time being I’ll stick to my current scheme with separate loaders and mutators for each resource (basically loader + mutator = secondary context).