I love Elixir and the implementation of protocols. Here is an example of how you can implement a protocol for a given data type/structure.

What this example shows:

  • 1st: There is a protocol named Usecase. It declares all functions of the implementation of the protocol Usecase needs.

So, you can execute it like so:

Define a module for your use case

defmodule DateUsecase do
  use Clean  # <= this implements all functions for a Usecase

  # The only function you have to define is `execute`
  def execute(nil), do: NaiveDateTime.utc_now()
  def execute(y: y, m: m, d: d), do: NaiveDateTime.new(y, m, d, 0, 0, 0)
end

Now you can use it like so

%DateUsecase{}
|> Usecase.request(y: 1964, m: 08, d: 31)
|> Usecase.execute()
|> Usecase.entity()
# ~N[1964-08-31 00:00:00]
  • 2nd: The ‘magic’ is stupid simple

It’s all about the structure %YourUsecase{} which will be injected by the line use Clean and is defined there as

defstruct request: nil, errors: [], result: nil, halt: false, client: :system

request holds the parameter/arguments passed in Usecase.request(…..).

errors is just a list where errors being added to during execution.

result holds whatever your execute(...) function returns. Usually, it is an ok/error tuple like {:ok, valid_result} or {:error, reason}.

halt is either false or true. It will initialize with false, but any step previous to execute can set it to false. Thus the execution function will not be executed.

client defaults to :system but is supposed to hold the client-metadata for which the use case should execute. In web apps, this may be the current user, the remote_ip, the user-agent,…

By now, there is no validate function. But you simply can implement it and call it just before execute. You can add this as homework ;-)

Full Example

No need to compile this file. Just execute it with

$ iex poc_usecases.exs

Repo github.com/iboard/elixir-pocs

File: github.com/iboard/elixir-pocs/poc_usecases.exs

defprotocol Usecase do
  def request(usecase, opts \\ [])
  def execute(usecase)
  def args(usecase)
  def result(usecase)
  def errors(usecase)
  def state(usecase)
  def entity(usecase)
  def client(usecase)
end

defmodule Clean do
  defmacro __using__(_opts) do
    quote do
      defstruct request: nil, errors: [], result: nil, halt: false, client: :system

      defimpl Usecase, for: __MODULE__ do
        def request(usecase, ... = opts \\ []) do
          %{usecase | request: opts}
        end

        def execute(args) do
          if !args.halt do
            case args.__struct__.execute(args.request) do
              {:ok, _} = r -> %{args | result: r}
              {:error, error} = e -> %{args | result: e, errors: args.errors ++ [error]}
              result -> %{args | result: result}
            end
          else
            args
          end
        end

        def result(usecase), do: usecase.result
        def client(usecase), do: usecase.client
        def errors(usecase), do: usecase.errors
        def args(usecase), do: usecase.request

        def state(usecase) do
          case usecase.result do
            {:ok, _} -> :ok
            {:error, _} -> :error
            x -> x
          end
        end

        def entity(usecase) do
          case Usecase.state(usecase) do
            :ok -> usecase.result |> elem(1)
            :error -> nil
            x -> x
          end
        end
      end
    end
  end
end

defmodule DateUsecase do
  use Clean
  def execute(nil), do: NaiveDateTime.utc_now()
  def execute(y: y, m: m, d: d), do: NaiveDateTime.new(y, m, d, 0, 0, 0)
end

defmodule FizzbarUsecase do
  use Clean
  def execute(_params), do: "Fizzefazze!"
end

defmodule MyApp do
  def run() do
    ## Usecase without params, no request() neccessary
    %DateUsecase{}
    |> Usecase.execute()
    |> IO.inspect(label: "result")

    IO.puts("")

    ## Same usecase but with input parameters
    uc =
      %DateUsecase{}
      |> Usecase.request(y: 1964, m: 08, d: 31)
      |> Usecase.execute()

    uc
    |> Usecase.args()
    |> IO.inspect(label: "args")

    uc
    |> Usecase.client()
    |> IO.inspect(label: "client")

    uc
    |> Usecase.result()
    |> IO.inspect(label: "result")

    uc
    |> Usecase.errors()
    |> IO.inspect(label: "errors")

    uc
    |> Usecase.state()
    |> IO.inspect(label: "state")

    uc
    |> Usecase.entity()
    |> IO.inspect(label: "entity")

    IO.puts("")

    ## Same usecase but with invalid input
    uc =
      %DateUsecase{}
      |> Usecase.request(y: 1964, m: 2, d: 30)
      |> Usecase.execute()

    uc
    |> Usecase.args()
    |> IO.inspect(label: "args")

    uc
    |> Usecase.result()
    |> IO.inspect(label: "result")

    uc
    |> Usecase.errors()
    |> IO.inspect(label: "errors")

    uc
    |> Usecase.state()
    |> IO.inspect(label: "state")

    uc
    |> Usecase.entity()
    |> IO.inspect(label: "entity")

    IO.puts("")

    %FizzbarUsecase{}
    |> Usecase.execute()
    |> Usecase.result()
    |> IO.inspect(label: "result")

    IO.puts("")

    %DateUsecase{}
    |> Usecase.execute()
    |> Usecase.entity()
    |> IO.inspect(label: "Today is")
  end
end

MyApp.run()

Output

result: %DateUsecase{
  client: :system,
  errors: [],
  halt: false,
  request: nil,
  result: ~N[2020-09-14 09:54:03.409234]
}

args: [y: 1964, m: 8, d: 31]
client: :system
result: {:ok, ~N[1964-08-31 00:00:00]}
errors: []
state: :ok
entity: ~N[1964-08-31 00:00:00]

args: [y: 1964, m: 2, d: 30]
result: {:error, :invalid_date}
errors: [:invalid_date]
state: :error
entity: nil

result: "Fizzefazze!"

Today is: ~N[2020-09-14 09:54:03.422391]