yordisprieto

yordisprieto

Value Object and Primitive Obsession code-smell

What is your take about the “primitive obsession” code smell in Elixir?

Do you prefer to create structs that wrap the native values or wraps other values like Decimal.t() that are a bit more complex?

Or do you try to use the primitive values as much as you can?

I am particularly interested in those situations where you only have one value in your struct, since


defmodule Something do
  // value is either a native type or some other struct
  @enforce_keys [:value]
  defstruct [:value]
end

// another example

defmodule Amount do
  @enforce_keys [:value]
  defstruct [:value] 

  def new(value) do
    %__MODULE__{value: Decimal.new(value)}
  end
end

Recently coming back to revisit my code, I find myself refactoring a bit and introducing more value objects, that brings a bit more clarity to my code compared to old code.

Some of the value objects (structs) are there to lift up the meaning of the native type or nested structs. While others check some invariants behind the factory function to make sure I don’t have garbage values.

What are your general thoughts about the topic?

Most Liked

sodapopcan

sodapopcan

They aren’t, and you can have separate schemas and changesets for your forms! In small apps I never feel this is worth it, but we do this at work with more gnarly forms.

Gotta call this one out as Changesets are not just about validation but also about, ahem, changes. Sure they go hand-in-hand, but so many Phoenix apps do a lot of parameter manipulation in the controller/LiveView layer before passing them to a changeset. I know it’s in many other apps evident by the number of people who ask how you deal with maps that may have string or atom keys… you don’t! You use the changeset API!

K, rant over.

yordisprieto

yordisprieto

They are two main cases thou,

  1. Clarity of the code:
defmodule Amount do
  @moduledoc """
  Value object representing a money amount in the context of fintech.
  """

  @type t :: %__MODULE__{value: Decimal.t()}

  @enforce_keys [:value]
  defstruct [:value]

  @doc """
  Creates a new `t:t/0` object.

  ## Examples

      iex> amount = Amount.new(20_000)
      ...> Amount.value(amount)
      20_000
  """
  @spec new(Decimal.decimal()) :: t()
  def new(value) do
    %__MODULE__{value: Decimal.new(value)}
  end

  @doc """
  Returns the value of the `t:t/0`.

  ## Examples

      iex> amount = Amount.new(20_000)
      ...> Amount.value(amount)
      20_000
  """
  @spec value(t()) :: integer()
  def value(amount) do
    Decimal.to_integer(amount.value)
  end

  @doc """
  Check if the given amount is negative.

  ## Examples

      iex> amount = Amount.new(-20_000)
      ...> Amount.negative?(amount)
      true

      iex> amount = Amount.new(20_000)
      ...> Amount.negative?(amount)
      false
  """
  @spec negative?(t()) :: boolean()
  def negative?(amount) do
    Decimal.negative?(amount.value)
  end
end

In this case, it doesn’t seem to have much value, but I would argue that you shouldn’t care if I am dealing with the value a Decimal.t() or a string.

  1. Hide invariants:
defmodule PositiveAmount do
  @moduledoc """
  Value object representing a `Amount` that must be positive.
  This object is useful when you want to deal signed values to unsigned values
  with safely.
  """

  alias Amount

  @type t :: %__MODULE__{value: Amount.t()}

  @enforce_keys [:value]
  defstruct [:value]

  @doc """
  Creates a new `t:t/0` object.

  ## Examples

      iex> {:ok, amount} = PositiveAmount.new(20_000)
      ...> PositiveAmount.value(amount)
      20_000

  With some an negative amount:

      iex> PositiveAmount.new(-20_000)
      {:error, :negative_amount}
  """
  @spec new(amount :: Amount.t() | Decimal.decimal()) :: {:ok, t()} | {:error, :negative_amount}
  def new(%Amount{} = amount) do
    if Amount.negative?(amount) do
      {:error, :negative_amount}
    else
      {:ok, %__MODULE__{value: amount}}
    end
  end

  def new(value) do
    value
    |> Amount.new()
    |> new()
  end

  @doc """
  Returns the value of the `t:t/0`.

  ## Examples

      iex> {:ok, amount} = PositiveAmount.new(20_000)
      ...> PositiveAmount.value(amount)
      20_000
  """
  @spec value(t()) :: integer()
  def value(amount) do
    Amount.value(amount.value)
  end
end

In this case, the value object doesn’t exist simply to lift up the meaning and hide the underline type, but also to make sure that the invariants check are correct, therefore, people don’t have to figure out what it means to be “PositiveAmount”.

I hope that helps.

garrison

garrison

Free yourself of this mentality before it’s too late.

Last Post!

yordisprieto

yordisprieto

Where Next?

Popular in Discussions Top

New
scouten
I’m looking for a host for the server part of a small (personal) side project that I’m working on. It’s currently written in Node.js and ...
New
AstonJ
If a newbie asked you about Phoenix Contexts, how would you explain the basics to them? Feel free to be as concise or in-depth as you li...
New
und0ck3d
Hello everyone! A few days ago I’ve created a topic here about how people were creating CMSs with Elixir and Phoenix. I’ve been studying...
New
chuck
Let me start by stating an assumption: Phoenix is a great approach to building REST APIs. There are many reasons for this, but I will ass...
New
Rustixir
Hi everyone, im working on find best language/framework/system for high concurrency, high performance and stable performance after wor...
New
crispinb
On reading dhh’s latest The One Person Framework it strikes me that Phoenix with LiveView is already pretty much this. However, never hav...
New

Other popular topics Top

nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 44532 311
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New

We're in Beta

About us Mission Statement