yordisprieto

yordisprieto

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?

First 10 of 30 Posts Switch mode

ityonemo

ityonemo

You will take a performance, and a readability hit. IMO the only reason to do this if you are have some sort of common strategy that traps the module associated with the struct, e.g. a protocol, or something where you are doing:

def some_function_that_takes_data_interface(value = %module{}) do
  module.operation(value)
end

presumably somewhere, you have some structs that get sent to above function that have more than one map field, in addition to the one-field structs.

ityonemo

ityonemo

by readability hit, I mean both in terms of your code, and in terms of when you’re debugging and inspecting stuff, not to mention if somehow your error logs happen to have erlang formatting instead of elixir formatting… Structs are not pretty in erlang formatting. I guess I feel a better strategy to lift up the meaning of primitive data types is to use good variable names. I once had a project where my linter would reject my code i If I had generic variable names like “value” “result”, “x”, etc. I would like to have a static linter some day that can infer types and will require that certain variables have suffixes, e.g. _id must be a uuid and vice versa.

dimitarvp

dimitarvp

In the general case I am completely with you here.

Just definitely don’t obsess. There are many cases where just passing integers and Strings is completely fine.

But I find myself using value objects for another reason as well: libraries like domo / typed_struct can help you with better enforcing contracts at runtime plus give you better error messages in case of a violation.

al2o3cr

al2o3cr

Can you post an example of code that uses this approach? In isolation, it doesn’t seem like this is anything more than a Decimal with extra steps.

yordisprieto

yordisprieto OP

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.

yordisprieto

yordisprieto OP

As far as I can remember, we ended up with the following value objects after tackling the “Primitive Obsession” and “Parse, Dot no Validate.”

Also, some value objects may not make sense to you, probably because you are missing out on the context and the why based on that given context. In the end, structs are just a way to provide a map of an identity; such an identity allows you to make assumptions about it. Sometimes, they are valuable due to invariants, and other times, they are beneficial due to assumptions made instead of validating all over the place. Context > Consistency

  • Currency
  • Money
  • Amount
  • TransferAmount
  • PositiveAmount
  • CreditAmount
  • DebitAmount

What to avoid is being too obsessive about creating wrapping functions instead of calling Map directly, focusing primarily on factory functions to ensure invariants above everything, and having less to do with hiding fundamental values. I wish I could find Rich Hickey’s video in which he talks about this.

dimitarvp

dimitarvp

I might not be reading you well but why is “Parse, Don’t Validate” an anti-pattern?

yordisprieto

yordisprieto OP

I am not sure what I wrote or how it could be interpreted! Maybe I misspoke or said backward … I meant to say, “It is good to parse instead of validate.” :sob: Changing the wording, just in case, let the user decide what it is an anti-pattern :sob: English is too hard sometimes

Oddman

Oddman

Value objects are there to enforce contraints and in DDD-land: business invariants. The point is to ensure that you have valid data and structures at all times. It also makes it MUCH easier to ensure you’re always working with that data by declaring your struct dependencies defined in those modules.

I use them everywhere in my game I’m building, including for IDs. IDs might seem like a bit of an “obsessive” approach, until you make one simple mistake where you pass the wrong id as an argument, and poison data. Do that once, and you very quickly see the value in having IDs as value objects.

But not everything needs to be. Value objects have their real value in encapsulating logic that validates the values they’ll hold. They’re also good for pairing related data together, such as an Address or phone number (country code, area code, number.etc.). You can’t have a valid phone without those details, so grouping it together makes a lot of sense.

yordisprieto

yordisprieto OP

Ask me how I know :sob: … now a days I have VO for IDs as well

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 91898 914
New
AstonJ
The obligatory hello world thread! Who are you and where are you from? :stuck_out_tongue:
4616 55835 594
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
AstonJ
Just a general thread to post chat/news/info relating to AI/ML stuff that may be relevant for Nx now or in the future. Got anything to sh...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
mudasobwa
While I am working on the Language Agnostic Code Audit SaaS, which uses MetaAST (spoiler: I am expecting it to be in a good shape for ann...
New

We're in Beta

About us Mission Statement