PhoenixUp

UseCase

A way to increase Elixir projects readability and maintenance based on Use Cases and Interactors, the main goals are:

  • Single Responsability Principle
  • Composability
  • Readability
  • Screaming architecture
  • Enforce inputs and outputs of our project use cases
  • Better errors (Know exactly where code fails)
  • Standardization

Table of contents

Installation

The package can be installed by adding use_case to your list of dependencies in mix.exs:

def deps do
  [
    {:use_case, "~> 0.1.3"}
  ]
end

Interactors

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!

Defining inputs

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}

Defining errors

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}}

Default fields

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.

Composing with pipes

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> 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> 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.

Sending options

All UseCase functions last argument is the options keyword list that is sent to interactors:

import UseCase

call(%SayHello{name: "henrique"}, my_option: true)
pipe([%SayHello{name: "Henrique"}, LogOperation], my_option: true)

Contribute

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:

branchs

your_branch_name

commits

[your_branch_name] Your commit