Service object pattern in Phoenix/Elixir

1,275 views
Skip to first unread message

mads.ha...@gmail.com

unread,
Mar 28, 2016, 2:23:09 PM3/28/16
to phoenix-talk
Hi

I am trying to figure out the best way to group together workflows. In rails, we had the service/interactor pattern.

Currently, I am doing something like below (workflow related to tagging a conversation), problem is that it is kinda stuck in the synchronous mindset.

Problem with that is if some of the steps depend upon each other.

Im curious to see how other people solve this problem


defmodule Resend.Conversations.TagInteractor do
 
import Ecto.Changeset
 
import Resend.Utils.Random


 
alias Resend.{
   
Conversation,
   
Tag,
   
Repo,
   
Message,
   
Endpoint
 
}


 
@doc """
  Public API
  """



 
def run(params ) do
   
Repo.transaction fn ->
     
params
     
|> find_conversation
     
|> create_tag
     
|> create_status_message
     
|> notify_team
     
|> Map.get(:tag)
   
end
 
end


 
@doc """
  Private API
  """



  defp find_conversation
(params = %{id: conversation_id}) do
    conversation
=
     
Conversation |> Repo.get(conversation_id)


   
params |> Map.put(:conversation, conversation)
 
end


  defp create_tag
(params = %{user: user, id: id, type: type}) do
    tag
= Tag.create!(%{conversation_id: id, user_id: user.id, type: type})
   
params |> Map.put(:tag, tag)
 
end


  defp create_status_message
(params = %{conversation: conversation, user: user}) do
    message
= %{
      conversation_id
: conversation.id,
      type
: "event",
      subtype
: "archived",
      system
: true,
      data
: %{
        author_id
: user.id
     
}
   
}


    message
=
     
Message.create!(message)
     
|> Map.put(:author_id, user.id)
     
|> Map.put(:author_name, user.display_name)


   
params |> Map.put(:message, message)
 
end


  defp notify_team
(params = %{conversation: conversation, tag: tag, message: message}) do
    topic
= "apps:" <> conversation.app_id
   
Endpoint.broadcast!(topic, "conversation_tagged", tag)
   
Endpoint.broadcast!(topic, "new_message", message)
   
params
 
end


end



Stuart Corbishley

unread,
Mar 30, 2016, 4:22:26 AM3/30/16
to phoenix-talk, mads.ha...@gmail.com
I've been asking myself similar questions as well, and your example approach is almost identical to the one I've taken.

I added a `services` directory to `web` and started putting my aggregate API in module in there,
I'm curious, as you are - of any other approaches which may be better suited to Elixir or in an FP language.

On the style guide topic, I've also had concerns about putting complex (multiple model) validations in the models themselves.
A service module feels like it adds a little too much indirection for a small number of cases.
I'm cautious of introducing some patterns that I've become accustomed to with Ruby.

Anyway, curious to see what everyone's experiences are around this.

Antonio Carpentieri

unread,
Mar 30, 2016, 7:51:42 AM3/30/16
to phoeni...@googlegroups.com, mads.ha...@gmail.com
I have exactly the same concerns and so far I've taken the same choices.
Waiting for some more "functional" guys here the express his point of view :)

A

--
You received this message because you are subscribed to the Google Groups "phoenix-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to phoenix-talk...@googlegroups.com.
To post to this group, send email to phoeni...@googlegroups.com.
Visit this group at https://groups.google.com/group/phoenix-talk.
To view this discussion on the web visit https://groups.google.com/d/msgid/phoenix-talk/5970502f-b84b-4e51-9c1e-8eb42fb07b25%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Bradley O'Hearne

unread,
Mar 30, 2016, 10:25:06 AM3/30/16
to phoeni...@googlegroups.com, mads.ha...@gmail.com
On Monday, 28 March 2016 20:23:09 UTC+2, mads.ha...@gmail.com wrote
I am trying to figure out the best way to group together workflows. 

I asked a few questions in the recent past which were getting at the same issue of workflow. With a branching workflow, I noticed the proliferation of workflow functions where functions became bound to other workflow steps via direct function calls, limiting their ability to be reused. I had hoped to be able to dynamically assemble independent workflow steps (being able to add / remove workflow steps without any direct dependencies between steps prior and after). I still am interested if there’s a graceful way to accomplish this, so I’m glad for your question. I too am interested in how others are accomplishing this, and look forward to their guidance. 

B


Chris Keathley

unread,
Mar 31, 2016, 9:37:55 AM3/31/16
to phoenix-talk, mads.ha...@gmail.com
I'm definitely not an FP expert but from my point of view there's nothing necessarily _wrong_ with this pattern.

I tend to create functions that interact with one external source at a time (database, external service, etc.) and then connect them together in the controller action.

If I was going to change anything here it would be to move the non-database specific code into its own module. For instance I would remove the `notify_team/1` function from this module and call `notify_team` directly in the controller or put it in a module and call the function on that module. Moving the notify call out of this module removes hidden side effects from your `run/1` function. Your `run/1` function is now simpler, easier to test in isolation and will be easier to re-use. It also means that you can re-use `notify_team/1` in other parts of your application.

The other benefit that this has is that the failure cases for these two actions are no longer coupled together. If you need to handle failures more explicitly then you can do so in the controller action.

Thats the pattern that I use but I'm sure there are others with even better ideas then mine.

Chris.
Reply all
Reply to author
Forward
0 new messages