View Source Domainex
DomainEx
is an Elixir library which provides a set of common typespec and domain models and also provides
a set of function helpers for basic function and domain building
About Domainex
Why TypeSpces
Elixir is not a static typing language, it's dynamic typing, which mean when we doesn't need to define any variable
or function type parameters. But Elixir provides their TypeSpecs
that really useful to:
- Documentation. I'm one of believer that good (and beautiful) code documentation is important
- Code analysis using
Dialyzer
When I begin to learn Elixir's typespec , I'm starting to learn the mental model, and I really like it. The typespec actually is just a typehint , but somehow I've felt that the type specification mechanism still able to help us to provide rich modeling domain business and in the same time can help us to building a great domain business documentation from our codes.
The Domainex
provides common types such as:
@type error :: {:error, {error_type(), error_payload()}}
@type success :: {:ok, any()}
@type result :: success() | error()
Domain Driven Design
Although Elixir is not a static type language, we are still possible to modeling business needs by take a leverage of typespec.
Domainex
also build with purpose to provide a helpers and also specs to define some common DDD concepts.
Aggregate
@type aggregate_name :: String.t() | atom()
@type aggregate_payload :: struct() | map()
@type aggregate :: {:aggregate, Aggregate.Structure.t()}
The main aggregate's structure will be like this:
@enforce_keys [:name, :contains, :events, :processors]
defstruct [:name, :contains, :events, :processors]
@type t :: %__MODULE__{
name: BaseType.aggregate_name(),
contains: BaseType.aggregate_payload() | %{atom() => BaseType.aggregate_payload()},
events: list(BaseType.event()),
processors: list(module())
}
Initiate new aggregate:
fake_entity = %FakeEntityStruct{name: "fake_entity"}
aggregate = Aggregate.new(:fake_entity, fake_entity, [FakeEventProcessor])
Initiate new aggregate with multiple entities:
fake_entity_1 = %FakeEntityStruct{name: "fake_entity_1"}
fake_entity_2 = %FakeEntityStruct{name: "fake_entity_2"}
aggregate = Aggregate.new(:fake_agg, %{:fake1 => fake_entity_1, :fake2 => fake_entity_2}, [FakeEventProcessor])
Aggregate and Functional Programming
We should treat an aggregate
as a single unit of business domain, which mean, we should put our business logic inside an aggregate
. It is a common to create an object which in OOP will be a class that hold refences to its internal states and behaviors based on business needs. There is a chance that some of aggregates need some common states,properties or even activities, maybe something like emitting domain event. All of these common properties and behaviors can be grouped into some base aggregate which will be inherited by other child aggregates.
The problem is, I rarely see such a thing in functional languages, including in Elixir. There is no way to extend from some defined structure or a struct()
.
In functional, actually it help us to made all things becomes more simpler. There are no internal states, no inheritance. There are just an input parameters and a functions. An input parameter is just a value , and a function used to do some computation, transform a value to other value.
|-------| |-----------| |---------|
| input | ---------> | function |------->| output |
|-------| |-----------| |---------|
Even better, each of value is also immutable , so there is no chance that we can update the value directly, what we can do is create another new value based on some given values.
If we can't extend this aggregate()
type and structure, then how do we use it in our real application, real business needs ?
There is no chance to extend, but, we can use this DomainEx.Aggregate.Structure
as a value, and use all of available functions form Domainex.Aggregate
as a helper functions, as long as the input value following spec:
@type aggregate :: {:aggregate, Aggregate.Structure.t()}
You are still free to create your own aggregate
based on your business needs, Domainex
will not limiting the solution or force you to follow some rules.
Example of possible solutions :
defmodule My.Aggregate do
defmodule Structure do
# The `:base` property defined here used to store our `aggregate()` type
defstruct [:base, :field1, :field2]
end
@spec new(base :: Domainex.aggregate())
def new(base) do
%Structure{
base: base
}
end
@spec your_business_activity(structure :: Structure.t()) :: {:ok, term()} | {:error, term()}
def your_business_activity(structure) do
# do whatever you needs
end
end
So, the logic flow become like this:
flowchart TD;
Start-->CreateBaseAggregate
CreateBaseAggregate-->|inject| CreateYourAggregate
CreateYourAggregate-->|result| YourOwnAggregate
Please remember, that since the
Domainex.Aggregate
which act as base aggregate used as a value, it needs to always be passed as a function parameters.
Domain Event
@type event_name :: atom()
@type event_payload :: struct() | map()
@type event :: {:event, Event.Structure.t()}
The main event's structure will be like this:
@enforce_keys [:name, :payload, :timestamp]
defstruct [:name, :payload, :timestamp]
@type t :: %__MODULE__{
name: BaseType.event_name(),
payload: BaseType.event_payload(),
timestamp: DateTime.t()
}
By default you almost doesn't need to do anything with these domain event structure and even its specs. When you initiate a new aggregate, it will also initiate an empty events.
Aggregate and domain events will follow Observer
design pattern. When you initiate an aggregate, you need to register
some event's processor, like an example above. An Event.Processor
is a Elixir's behaviour, or an interface called in other languages.
Each time you emit_events/1
from an aggregate, it will send all available aggregate's event to its event's processor. Its up to the module which implement Event.Processor
to do anything with given event list, maybe doing some computation using Elixir's GenStage
.
The power of Tuple
When I learning Elixir, I've seen a lot of tuple used to grouping some context. Let just take our previous sample for return values :
@type error :: {:error, {error_type(), error_payload()}}
@type success :: {:ok, any()}
@type result :: success() | error()
In the first time, it just look weird, but after learn more, I just think that it actually a reall simple and powerfull concept. We can use tuple to build a set of context from some value, not just its type but also the context, what kind of information do we get from some value. From the example above, when we got the information that the value is a succes or an error, we know how to handling it.
It's same with previous aggregate. By just defining an aggregate as a tuple , when we got a value which is a tuple and the first element is :aggregate
, what we need to do next is extract the following elements like for aggregate_name
and it's payload.
And thanks to Elixir's pattern matching its really simple to match and extract tuple values
iex(1)> {typed, value} = {:ok, "hello world"}
{:ok, "hello world"}
iex(2)> typed
:ok
iex(3)> value
"hello world"
iex(4)>
Installation
If available in Hex, the package can be installed
by adding domainex
to your list of dependencies in mix.exs
:
def deps do
[
{:domainex, "~> 0.1.0"}
]
end