A way to increase Elixir projects readability and maintenance. Heavily inspired by Clean Architecture. We can join it with clean mixer for a very nice Clean Architecture code experience.
The package can be installed by adding use_case
to your list of dependencies in mix.exs
:
def deps do
[
{:use_case, "~> 0.2.2"}
]
end
Lets see some of the benefits from using the library (or only the idea behind use cases interactors). Imagine a library system where we have the context "books". On normal Phoenix systems, the design may look like the example below:
▾ library/
▾ books/
author.ex
book.ex
application.ex
books.ex
repo.ex
There we have some problems:
- It is not yet clear what our application intend to do.
- Contexts files (books.ex) can get extremely fat with a lot of business logic.
Now, thinking in use case interactors we can imagine Phoenix contexts as a Facade for your use cases, and do that:
▾ library/
▾ books/
author.ex
book.ex
create_author.ex
create_book.ex
sell_book.ex
application.ex
books.ex
repo.ex
Our context will look like the example above:
defmodule Library.Books do
@moduledoc """
The Books context.
"""
import UseCase, only: [call: 1]
import __MODULE__.{CreateAuthor, SellBook, CreateBook}
@doc """
Creates a authors.
iex> create_authors(name)
{:ok, %Library.Books.CreateAuthor.Output{ ... }}
iex> create_authors(bad_name)
{:error, %Library.Books.CreateAuthor.Error{message: "Bad name given"}}
"""
def create_author(name),
do: call(%CreateAuthor{name: name})
# ...
def create_book(name, author),
do: call(%CreateBook{name: name, author: author})
# ...
def sell_book(book_id),
do: call(%SellBook{book_id: book_id})
Let's say that now CreateBook
, CreateAuthor
and SellBook
are gateways for our business rules. Controllers, views and even Phoenix know almost nothing about our business, they know that we can "create books" and "sell books", and for that we need the params "name", "author" or "book_id", but nothing about what goes inside. Goals:
- Its clear what our application intend to do. It screams.
- Contexts files are only facades, an api for our use cases interactors to the external world. They dont know Repos or Schemas.
- When we call an use case interactor, we will get a specific output or an specific error from that use case (and we have a specific input too), making the system code more assertive in relation to what it is doing.
And this is just the tip of the iceberg, to full enjoy this library, i recommend you to read the Clean Architecture book.
The most basic interactor can be created using the UseCase.Interactor
module, defining an output
for it and creating a call/2
function:
defmodule SayHello do
use UseCase.Interactor,
output: [:message]
def call(%{name: name}, _opts), do: ok(message: "Hello #{name}!")
def call(%{name: nil}, _opts), do: error("name is obrigatory")
end
Now our SayHello
module has the ok
and error
macros and a struct for Output
like %SayHello.Output{message: "something"}
.
The ok
and error
macro can be used to define when our interactor success or fail.
After define, we can call it in many ways:
iex> UseCase.call(SayHello, %{name: "Henrique"})
iex> {:ok, SayHello.Output{message: "Hello Henrique!", _state: nil}}
iex> SayHello.call(%{name: "Henrique"})
iex> {:ok, SayHello.Output{message: "Hello Henrique!", _state: nil}}
iex> UseCase.call(SayHello, %{name: nil})
iex> {:error, SayHello.Error{message: "name is obrigatory!"}}
iex> UseCase.call!(SayHello, %{name: "Henrique"})
iex> SayHello.Output{message: "Hello Henrique!", _state: nil}
iex> UseCase.call!(SayHello, %{name: nil})
iex> **** SayHello.Error name is obrigatory!
Sometimes we want to guarantee the inputs our interactors will receive, we can do it defining this way:
defmodule SayHello do
use UseCase.Interactor,
output: [:message],
input: [:name] # Add this
def call(%SayHello{name: name}, _opts), do: ok(message: "Hello #{name}!")
def call(%SayHello{name: nil}, _opts), do: error("name is obrigatory")
end
Now, with UseCase
module we can call it using the input directly:
iex> UseCase.call %SayHello{name: "Henrique"}
iex> {:ok, SayHello.Output{message: "Hello Henrique!", _state: nil}}
iex> UseCase.call! %SayHello{name: "Henrique"}
iex> SayHello.Output{message: "Hello Henrique!", _state: nil}
If we want to send extra informations in errors, we can do it as input
and output
.
defmodule SayHello do
use UseCase.Interactor,
output: [:message],
input: [:name],
error: [:code] # Add this
def call(%SayHello{name: name}, _opts), do: ok(message: "Hello #{name}!")
def call(%SayHello{name: nil}, _opts), do: error("name is obrigatory", code: 500) # And use it
end
iex> UseCase.call(SayHello, %{name: nil})
iex> {:error, SayHello.Error{message: "name is obrigatory!", code: 500}}
When not defined, input, output and error defaults to:
input: [:_state],
output: [],
error: [:message]
Fields :_state
in input
and :message
in error
are always appended. The :_state
field is very useful for pipe operations.
Lets define an LogOperation
interactor:
defmodule LogOperation do
use UseCase.Interactor
def call(%{message: message}, _opts) do
# .. log message
ok()
end
end
We can compose with our SayHello
simple as that:
iex> %SayHello{name: "Henrique"} |> UseCase.pipe [SayHello, LogOperation]
iex> {:ok, LogOperation.Output{_state: nil}}
iex> UseCase.pipe [%SayHello{name: "Henrique"}, LogOperation]
iex> {:ok, LogOperation.Output{_state: nil}}
iex> UseCase.pipe [%SayHello{name: nil}, LogOperation]
iex> {:error, SayHello.Error{message: "name is obrigatory!", code: 500}}
iex> %SayHello{name: "Henrique"} |> UseCase.pipe! [SayHello, LogOperation]
iex> LogOperation.Output{_state: nil}
iex> UseCase.pipe! [%SayHello{name: "Henrique"}, LogOperation]
iex> LogOperation.Output{_state: nil}
iex> UseCase.pipe! [%SayHello{name: nil}, LogOperation]
iex> **** SayHello.Error name is obrigatory!
All we need is match outputs and inputs and use one of pipe UseCase
functions.
All UseCase
functions last argument is the options keyword list that is sent to interactors:
import UseCase
call(%SayHello{name: "henrique"}, my_option: true)
%SayHello{name: "Henrique"} |> pipe([SayHello, LogOperation], my_option: true)
pipe([%SayHello{name: "Henrique"}, LogOperation], my_option: true)
mix use_case.gen.interactor
UseCase is not only for me, but for the Elixir community.
I'm totally open to new ideas. Fork, open issues and feel free to contribute with no bureaucracy. We only need to keep some patterns to maintain an organization:
your_branch_name or your-branch-name
[your_branch_name] Your commit or [your-branch-name] Your commit