Skip to content

henrique-ft/use_case

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

61 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PhoenixUp

UseCase

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.

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.2.2"}
  ]
end

About

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.

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

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)
%SayHello{name: "Henrique"} |> pipe([SayHello, LogOperation], my_option: true)
pipe([%SayHello{name: "Henrique"}, LogOperation], my_option: true)

Mix Tasks

mix use_case.gen.interactor

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 or your-branch-name

commits

[your_branch_name] Your commit or [your-branch-name] Your commit

About

A way to increase Elixir projects readability and maintenance. Heavily inspired by Clean Architecture.

Resources

Stars

Watchers

Forks

Packages

No packages published