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?

Showing Posts 1 to 10

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 94592 917
New
cblavier
Hey there, It’s been more than a year since we started using LiveView as our main UI library and building a whole library of UI componen...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
heathen
Quite interesting article Google brought me. Didn’t find any mentions about it here. What do you think in general? Would you use togethe...
New
maennchen
:warning: Security advisory: Decimal DoS vulnerability A vulnerability has been published for decimal where very large exponents can cau...
New
marciol
It would be helpful to have a list of companies worldwide that hire engineers without prior experience in Elixir. Often, it can be quite ...
New
durvia
Anyone running long-lived stateful processes on BEAM? We’re building an AI agent runtime and would love to compare notes. We’re a small ...
New

Other Trending Topics Top

marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
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
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New
webofbits
Aludel - LLM Evaluation Workbench Aludel is an embeddable Phoenix LiveView dashboard for evaluating and comparing LLM prompts across mult...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews