svarlet

svarlet

Hi all,

I currently have this thought that relying solely on modules in Elixir hinders the writing of polymorphic code. Let me explain my thought process with an example.

Let’s imagine a shop project, using phoenix and ecto, and let’s roughly implement a route that creates a purchasable product. This has to follow some rules defined by the Sales and Strategy teams. For example, the name must be unique, there must be a sell by date no later than 3 months after creation, it has to be approved by the CEO before customers can purchase it, etc.

In the router:

post "/purchasables", PurchasablesController, :create

In the controller

def create(conn, params) do
  purchasable_creation_request = %PurchasableCreationRequest{
                                      name: params.name,
                                      approved: false,
                                      created_at: Timex.now()}
  case BusinessRules.Purchasables.create(purchasable_creation_request, PurchasablesGateway) do
    {:ok, purchasable} ->
      conn
      |> put_flash(:info, "Success")
      |> assign(purchasable, purchasable)
      |> render(:show)
    {:error, reason} ->
      conn
      |> put_flash(:error, "Creation failed because #{reason}")
      |> render(:create)
  end
end

Here we have the controller convert the http request into something the business rule can work with. The dependency and flow of control only go in one direction. While we have successfully split all the code into 2 modules, they are still tightly coupled.

If I look at the source code dependencies and the flow of control, they both go in the same direction:

  • The PurchasablesController uses BusinessRules.Purchsables.create/2
  • The PurchasablesController depends on BusinessRules.Purchsables.create/2

Concrete things use and depend on concrete things.

At the same time, it looks like BusinessRules.Purchasables.create/2 uses a gateway for purchasables but does not depend on a concrete one.

In a polymorphic setup, we would often observe that. The flow of control opposes the source code dependencies: it’s not because ModuleA uses Ecto that it has to depend on Ecto.

Interestingly this is very palpable in tests. Howvever, there is a variety of opposing opinions in this forum held for or against testing, for or against TDD, for or against mocking, … so I hope this topic is not going to deviate towards any of them.

Back to the example, if we wanted more polymorphism, we could loosen the dependency between the controller and the business rule:

In the router

post "/purchasables", PurchasablesController, :create,
  private: %{
    business_rule: BusinessRules.Purchasables
  }

In the controller

def create(conn, params) do
  purchasable_creation_request = %PurchasableCreationRequest{
                                        name: params.name,
                                        approved: false,
                                        created_at: Timex.now()}
  case conn.private.business_rule.create(purchasable_creation_request, PurchasablesGateway) do
    {:ok, purchasable} ->
      conn
      |> put_flash(:info, "Success")
      |> assign(purchasable, purchasable)
      |> render(:show)
    {:error, reason} ->
      conn
      |> put_flash(:error, "Creation failed because #{reason}")
      |> render(:create)
  end
end

So what happened here? While the controller needs a business rule, it doesn’t need to know which one it uses. I found this morning that the plug router now allows to pass stuff via conn.private to a plug (controllers are plugs). I thought it would be a better place to set the concrete business rule used by the controller. That makes my controller code easier to test as I can test the behaviours of the controller without involving the rest of the app like the business rule and the DB gateway.

Though it doesn’t feel quite right yet. Using the router for that feels like misplaced responsibility. I’d like to find a way to compose all the things together in a different way:

  1. make/create/start a purchasable gateway
  2. make/create/start a business rule with that gateway
  3. make/create/start a controller with the business rule
  4. associate the controller with a http route

Some might say it’s a OOP mindset to try to compose things together, but it’s not reserved to OOP, for example in F#: Integration Testing composed functions

Today, the community seems to prefer the use of Application config with module attributes everywhere there is a need for indirection.

For example, in the controller:

defmodule PurchasablesController do
  use Phoenix.Web, :controller

  @business_rule Application.get_env(:my_shop_app, :purchasable_creation_rule)

  def create(conn, params) do
    purchasable_creation_request = %PurchasableCreationRequest{
                                        name: params.name,
                                        approved: false,
                                        created_at: Timex.now()}
    case @business_rule.create(purchasable_creation_request, PurchasablesGateway) do
      {:ok, purchasable} ->
        conn
        |> put_flash(:info, "Success")
        |> assign(purchasable, purchasable)
        |> render(:show)
      {:error, reason} ->
        conn
        |> put_flash(:error, "Creation failed because #{reason}")
        |> render(:create)
    end
  end
end

I think it’s dangerous because the tests often can’t run safely and concurrently, I tend to define all tests as async: true by default. It’s muscular memory at this point. Am I willing to trade test execution speed / time to feedback for this? No. I found using the Mox library, a good, safe and fast fix though.

I wonder if we could make these dependencies more explicit though. Whether with the router or with application config, the dependencies are rather implicit, with Application being the worse.

Let’s look back at our last version of the example (no matter which approach between the router or app config, you pick). I noticed that the business rule is now polymorphic: I can easily pick a different one. Yet the controller is still making a decision about which concrete gateway the business rule is using. I don’t think that’s the job of the controller and so the code should look like this:

In the controller:

defmodule PurchasablesController do
  use Phoenix.Web, :controller

  @business_rule Application.get_env(:my_shop_app, :purchasable_creation_rule)

  def create(conn, params) do
    purchasable_creation_request = %PurchasableCreationRequest{
                                        name: params.name,
                                        approved: false,
                                        created_at: Timex.now()}
    # Removed the gateway in the next line
    case @business_rule.create(purchasable_creation_request) do
      {:ok, purchasable} ->
        conn
        |> put_flash(:info, "Success")
        |> assign(purchasable, purchasable)
        |> render(:show)
      {:error, reason} ->
        conn
        |> put_flash(:error, "Creation failed because #{reason}")
        |> render(:create)
    end
  end
end

We’ve just decoupled things a bit more. Our business rule still needs a gateway though. Sure, we could once again rely on Application config to make the business rule fetch its gateway at compile time into a new module attribute. Things are getting more and more implicit now, it’s spreading quickly. Perhaps we could combine this solution with the router+option solution:

post "/purchasables", PurchasablesController, :create,
  private: %{
    business_rule: BusinessRules.Purchasables,
    gateway: PurchasablesGateway
  }

and revert the change made to the controller to

defmodule PurchasablesController do
  use Phoenix.Web, :controller

  @business_rule Application.get_env(:my_shop_app, :purchasable_creation_rule)

  def create(conn, params) do
    purchasable_creation_request = %PurchasableCreationRequest{
                                        name: params.name,
                                        approved: false,
                                        created_at: Timex.now()}
    case @business_rule.create(purchasable_creation_request, conn.private.gateway) do
      {:ok, purchasable} ->
        conn
        |> put_flash(:info, "Success")
        |> assign(purchasable, purchasable)
        |> render(:show)
      {:error, reason} ->
        conn
        |> put_flash(:error, "Creation failed because #{reason}")
        |> render(:create)
    end
  end
end

Well, now we have combined 2 different solution so it’s more complex. Worse, the controller knows way too much about lots of little details to make things work, it is nosy.

What if we look at the other solution using the router options only? A solution is to define a module which wraps the actual business rule and decide what gateway should be used. That’s like decorators in the OOP world.

#Can this scale into the composition root described by ploeh in the blog shared above?
defmodule PurchasableCreationRuleWithBatteriesIncluded do
  def create(purchasable_creation_request) do
    BusinessRules.Purchasables.create(purchasable_creation_request, PurchasabesGateway)
  end
end

In the router:

post "/purchasables", PurchasablesController, :create,
  private: %{
    business_rule: PurchasableCreationRuleWithBatteriesIncluded
  }

The controller:

defmodule PurchasablesController do
  use Phoenix.Web, :controller

  def create(conn, params) do
    purchasable_creation_request = %PurchasableCreationRequest{
                                        name: params.name,
                                        approved: false,
                                        created_at: Timex.now()}
    # the concrete business rule is set in the router and has the desired gateway baked in
    case conn.private.business_rule.purchasable_creation_request) do
      {:ok, purchasable} ->
        conn
        |> put_flash(:info, "Success")
        |> assign(purchasable, purchasable)
        |> render(:show)
      {:error, reason} ->
        conn
        |> put_flash(:error, "Creation failed because #{reason}")
        |> render(:create)
    end
  end
end

That is a solution I found while writing this up. Disclaimer: I haven’t actually tried it yet, but I still don’t feel very excited about it. It all feels like doing functional programming in older versions of Java: instead of composing functions, we are finding workarounds to compose modules, and that’s not … elegant.

Thoughts? Opinions?

Showing Posts 1 to 10

LostKobrakai

LostKobrakai

I’m not sure how a function would compose differently than a module. To me your post kinda sums up to the following:

My business logic needs X, but I don’t want the controller to know which X to use, but I also don’t really know who should be in charge of knowing the X to use.

Replace X with gateway/module/function/business_rule/whatever, the problem will stay the same: Someone needs to decide on a concrete implementation. This is usually where dependency injection comes into play as you seem to have already gathered.

The application environment / module attributes are simple ways to get started with dependency injection, but as you also already noted this makes testing harder, because the dependency is globally or even compile time set. The only way around that is explicitly passing data in. If you test a function you’d pass in e.g. a keyword list of dependencies. In case of http requests it might be headers, or you recycle the conn in a test and prime it with some assigns. Plugs can then use those settings and convert them to actual modules/function or whatever you actually need in your controllers. If you don’t explicitly pass data in you’ll need to rely on side effects to query for those dependencies, which likely means global state again.

If you don’t need that much flexibility up to the http request level I’d also suggest simply using plugs. They can select your dependencies (module plugs at compile- and runtime) and put them in the conn for the controllers to use. You can decide where to call those plug (for all requests, just a single scope or even just a single controller and the logic does not leak into e.g. the router itself.

From a quick glance at the blogpost you linked I also feel like plugs are the answer to your question about composition roots.

It seems to me that most people are eager to compose as early as possible, but the correct answer is:

As close as possible to the application’s entry point.

In case of phoenix this is basically MyApp.Endpoint and you can use plugs there to resolve dependencies based on the request coming in.

al2o3cr

al2o3cr

There’s a critically important piece missing from this discussion: why.

Your post does a good job of outlining the costs of different approaches to introducing decoupling and runtime flexibility. But without a reason WHY, it’s impossible to know which one is “better”.

svarlet

svarlet OP

@LostKobrakai Thanks for your answer.

Functions compose naturally: f() o g() works if the output of g is an acceptable input for f. With modules, you can’t compose M1 with M2 (at least in Elixir), it doesn’t return a new module or anything else that would make sense.

I believe that the application’s entrypoint should make all the plumbing/wiring decisions (what X to use in a Y and what Y to use in a Z). In some languages, it’s the “main” file. In Elixir, that would be application.ex.

Indeed.

Indeed. I actually use this solution in production now. It works. Unfortunately, it also means that the composition has request scope : everything get recomposed on every request, by every request (well by the plug chain for that request). So, yes it works, but I don’t think it’s the responsibility of plugs to do that because we are now mixing plumbing with processing. In my experience so far, it complicates the router, the controllers, …

We disagree, I think the application’s entrypoint is application.ex (“Phoenix is not your application.”). Yet, I don’t see how to compose in application.ex:

  • a usecase with a gateway,
  • a controller with that usecase
  • a router with that controller
  • an endpoint with that router

[EDIT] There is another thought frequently coming back to me about this: should I be using processes somehow somewhere for that? And another one is that perhaps the way I want to build things is fundamentally not aligned with how Phoenix/Plugs are designed. On that note, I want to have a look at Ace/Raxx by @Crowdhailer. Perhaps he found a solution for me there.

svarlet

svarlet OP

@al2o3cr Thank you for your answer.

Indeed I made an assumption (I’m an engineer, I do that for a living) that the reader would have a similar understanding and appreciation of some architectural principles to mines.

I believe in dependency inversion. I think the low level details should depend on the high level policies. That’s why the controller depends on the usecase in my example. Yet, by hiding information from the controller (which usecase? what does the usecase do? how does it its job and with what dependency?) we also gain a degree of flexibility which manifest immediately and positively in my tests, and in the longer term in changes to the codebase which are quite contained to a few modules (ideally one).

LostKobrakai

LostKobrakai

If you want application.ex to be your entry point then you’re limiting yourself to “per started application” composition, which goes against your problem of wanting to run async tests. An application does only once in its lifetime receive “inputs”, so again every additional changes would need to rely on side effects.

If you want async test you need to be able to change plumbing just for the codepath of that single test execution, which for phoenix controller tests means “per request” composition. This requires stuff to be handled somewhere in the plug pipeline. You can see this implemented in action in this blogpost: How to Add Concurrent, Transactional End-To-End Tests in a Phoenix-Powered Ember App - DockYard

Also I’m not saying you need to handle everything in your plug pipeline. You can surely mix and match depending on how much flexibility you really need. Your response sounds like you want plumbing to be static to an application, yet flexible to be tested in isolation. Maybe you should just acknowledge the different levels of granularity and build a multi layered system, where per request plumbing is only done for tests, while in prod the plug simply takes a static set of plumbing from e.g. MyApp.Application.plumbing and put’s it in the current conn. Controllers simply use what’s available to then through the data on the conn.

Processes won’t make your problems simpler in any way. If you use processes to store details on plumbing you still didn’t solve the problem of knowing which process (which set of plumbing) to use for a certain request. If you cannot do that you cannot do async tests with controllers. You might want to look at how e.g. the ecto sandbox works, which essentially deals with the same problem: How does it know which transaction to run a certain db interaction in, so it’s the same as the test starting the current code path.

Basically the answer is global state or explicit “ownership”.

LostKobrakai

LostKobrakai

And another though. In your entry post you used a module to compose things together. Modules are inherently compile time constructs (unless you really want to compile things at runtime). You’re also asking about dynamic composition (async tests), which to me sounds like you might want to try to encode your compositional facts into data not code. Because data can be changed at runtime, modules can’t.

Something like:

rule_plumbing = %{
  purchasable_creation_rule_with_batteries_included: %{
    business_rule: BusinessRules.Purchasables,
    gateway: PurchasabesGateway
  }
}

# In e.g. controller
ruleset = conn.private.rule_plumbing.purchasable_creation_rule_with_batteries_included
case ruleset.business_rule.create(purchasable_creation_request, ruleset.gateway) do
   …
svarlet

svarlet OP

Have you checked how Ace works?

svarlet

svarlet OP

I think my initial post documented this approach, search for “Yet the controller is still making a decision about which concrete gateway the business rule is using.”

LostKobrakai

LostKobrakai

I’m not sure where this would be the case. rule_plumbing can be defined wherever you seem fit. The controller has no idea which exact gateway it’s using. It just uses the one, which was put into the rule_plumbing data. The rule_plumbing being put into conn.private is just an example and a simple way to supply a controller with information, because all the data it receives (without global state) is the conn. Even the params in a usual def action(conn, params) come from a call like apply(controller, action, [conn, conn.params]). So you’ll hardly get around putting some information into the conn if you want to async controller tests with dependency injection.

It’s been some time, but I’m not sure how you expect it to work differently in regards to your problem. When a http request is handled the process, which is handling it, needs a way to resolve the abstract dependencies in the code into actual dependencies. It can either resolve them with knowledge available to the process already – basically all the information of the http request, or maybe the process dict, when we’re talking only about tests –, resolve it using information available to another process known ahead of time or non process based global or compiled state – e.g. hardcoded functions or modules being somewhere in your codebase. If this process handling a http request is spawned by phoenix or ace shouldn’t really make a difference in how those processes resolve their dependencies.

svarlet

svarlet OP

@LostKobrakai you’re right in your first paragraph, I spoke too soon.

I’m curious about Ace because it seems that controllers are processes which you can initialize when they start with any value. That means I could effectively start them in application.ex and configure them there. Regarding tests, since they just are implementation of a behaviour, like gen_servers, they can be tested safe and fast.

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
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New
Herve37
We’re evaluating API mocking tools for OpenAPI-based projects and would love to hear what other teams are using. We’re particularly inte...
New
matt-savvy
Is there a word for the ~> symbol used in Version strings? Do you also just call it a Squiggle Arrow™ ?!
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
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
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
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
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

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews