It works really nice when functions require 2-4 params, but more than that requires to start using a well formed map. He lays out this in one of the comments
Normalize the free-form input in the interface into a well-shaped data (maps or structs)
Pass that data to the core
Have core only return well-shaped results on success.
Returning a changeset on error is fine as long special branching on the type of error in the client (interface)
code is not required.
So I used a struct to type a map that I use as input, something along this lines
# my_context.ex
defmodule MyApp.MyContext.CreateParams do
@enforce_keys [:name]
@type t() :: %__MODULE__{name: String.t(), modified_at: DateTime.t() | nil, ...a bunch of others...}
defstruct [::name, modified_at: nil, ... and a bunch of others...]
end
defmodule MyApp.MyContext do
@type create(CreateParams.t()) :: {:ok, Thing.t()}
def create(params) do
# creation logic
end
end
Now, this works, but is quite ugly as I need to alias that CreateParams module whenever I want to call create to comply with spec, I need to start inventing names for that struct module and worst of all, Iβm not sure where is best to place that module (for now I colocate in the same file as my context)
I prefer to not use alias in my code, instead I prefer to be explicit, so that I know at a glance what I am really calling.
In my case I also use an API module to establish the boundaries between modules calling each other, therefore I always have module called MyApp.Api where I use defdelegate to map the calls and have all my modules that depend on others just coupled by MyApp.Api module. So in your case I would always have a call to MyApp.Api.create_mycontext/1 and withour using alias.
I really donβt like the usual approach of spreading the code of a resource across different folders, unless they are child folders of the current resource:
the file annotation.ex is the context for that resource action.
So, in my case if it is a struct common to all actions in the the annotations resource, then I would add it into rumbl/annotations/create_params.ex or if it was common to all resources at rumbl/annotations/create_params.ex.
You can try the Domo library to have typed structs enforced at runtime. The library will also write the type specs for your therefore your code will look nicer.
Yes, I guess there are not many options, either co-located in the same file or in a different file (at which level depending on who uses it as you mentioned)
About using a function like MyApp.Api.create_mycontext/1, not sure Iβm getting this right, but seems that using that function puts me in the same position of having to pass a lot of params, no? My problem was that I need to use a Map when the functions require more than 4 params (Iβll edit my example to illustrate this better)
I should had been more explicit. Using this approach was more to address your issue of using alias everywhere you need to invoke the creation of the struct.
To try to be more clear using MyApp.Api.create_mycontext/1 is shorter then using MyApp.MyContext.CreateParams.create/1 everywhere while at same time decouples the client from the implementation.
defmodule MyApp.Api do
defdelegate create_mycontext(params), to: MyApp.MyContext.CreateParams, as: create
end
The name create_mycontext is just a placeholder and should be replaced with what makes sense to your business domain.
Nice, that is indeed less verbose than using a struct and saves the hassle to create a module, so is much better than what I have. Thank you @sasajuric and thank you also for the great blog post series!