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
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
They are two main cases thou,
- 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.
- 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.
Last Post!
yordisprieto
Popular in Discussions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex









