Hi Folks,
I am trying to put some of the ideas that I’ve been exposed to into practice in one of my applications.
One of those ideas is architecting the application to have a pure core
and an imperative shell
. The way that this manifests itself, at least to me, in practice is isolate impure functions and bubble them up to the surface so to speak. If our core is pure, then our code becomes easier to reason about, thanks to referential transparency
and the substitution model
.
It seems like many Elixir libraries hew closely to this idea of a pure core
and an imperative shell
. For example, in Ecto
, an Ecto.Query
struct is nothing more than a bag of data. It is only when this bag of data is passed into functions in Ecto.Repo
that side effects occur. (I’m leaving out the part that the Ecto.Queryable
protocol plays, since it is just a way to convert one bag of data into an Ecto.Query
bag of data).
Anyways, I want to apply this same thinking to my application, which needs to reach out to an API. The application doesn’t care about the return value from the API, it just needs to send a message to the API so that the API can do some work. So my thinking is this:
- Create a new struct that contains all the information that
HTTPoison
needs to make the request. Headers, HTTP method, etc., etc. - Create a new module that knows how to take that bag of data and make the request with HTTPoison.
The benefits of this is that my core remains pure. When testing, I can simply ensure that the values in my bag of data are what I expect them to be.
I guess the reason for my post is general comments on what I’m proposing to do, and also how folks would go about testing this. I can test the bag of data, that’s fine. However, for testing the actual side effect, it seems like Jose (in his Mocks and Explicit Contracts post) prefers to actually make a request, but I was wondering if I do this by creating a mock
(used as a noun) for different environment, or if I should pass that in as a dependency. (Maybe even as a property on the bag of data). Or am I conflating a lot of different things here? (Jose seems to make a clear separation between these different strategies - i.e., different mocks, dependency injection, and defining a struct / protocol).