Hi everyone,
I’m still at the beginning of my Elixir journey, I’ve worked through several tutorials and I’m starting to get to grips with the language itself, but I’m struggling with how to enable reusability in a project. For my current project I’m trying to make up-to-date information from a WebSocket feed available to a subscriber. The idea I have is that I should be able to include this project as a dependency in other projects, so I’m trying to keep it very loosely coupled, but with a usable interface that aids the future developer (probably me!) in using it correctly.
I’ve created some helper functions and my main question here is whether these helper functions are the “right” way of doing things?
Perhaps an example will help. There are several WebSocket feeds and I am successfully consuming them using WebSockEx.
Following advice found elsewhere this forum, I’m creating a struct of the data as soon as it is received:
defmodule Exchange.Ticker do
defstruct [:event_time, :symbol,:close,:open,:high,:low]
def new(%{"e" => event_time, s" => symbol,"c" => close,"o" => open,"h" => high,"l" => low }) do
%__MODULE__{
event_time: Timex.from_unix(event_time, :milliseconds),
symbol: symbol,
close: Decimal.new(close),
open: Decimal.new(open),
high: Decimal.new(high),
low: Decimal.new(low)
}
end
end
The data arrives as a list in JSON so which is handled like this:
def handle_new_ticker_message(data) do
data
|> Jason.decode!()
|> Enum.map(&Exchange.Ticker.new/1)
|> Enum.map(&Exchange.PubSub.broadcast/1) ## This is one of the helpers.
end
And the helpers I’ve created are:
defmodule Exchange.PubSub do
## Correctly determines the topic from the data type and broadcasts it
def broadcast(%Exchange.Ticker{} = ticker) do
Phoenix.PubSub.broadcast!(
__MODULE__,
topic(ticker),
ticker
)
end
## Subscribe based on a string specifying the desired symbol
def subscribe(Exchange.Ticker, symbol) do
Phoenix.PubSub.subscribe(__MODULE__, topic(Exchange.Ticker, symbol))
end
# determine the correct topic from the struct
def topic(%Exchange.Ticker{symbol: symbol}) do
topic(Exchange.Ticker, symbol)
end
# determine the correct topic from a string
def topic(Exchange.Ticker, symbol) when is_binary(symbol) do
"Exchange.Ticker.#{symbol}"
end
end
Then if we needed to subscribe to the symbol “EURUSD” we could subscribe possibly in the start_link of a Genserver):
Exchange.PubSub.subscribe(Exchange.Ticker, "EURUSD")
and in subscribed modules we could have:
def handle_info(%Exchange.Ticker{symbol: symbol, close: price}, state) do
### do stuff with the message
end
For each additional WebSocket data feed there would be additional structs with their requisite helper functions. I don’t think this has to be limited to incoming data either. For example a PubSub channel notifying about subscription events could have a %Subscription.Event{} struct or similar.
The main reason for doing this is that I’m used to using constants to help my IDE to give me auto complete and avoid typos on magic strings. The PubSub topic feels a bit like a magic string and the example in the Phoenix.PubSub documentation creates a message with what feels a bit like a magic atom :user_update
. I don’t think that compile time checks would pick up if you accidentally specified :users_update
in a handle_info
call or had a typo in the topic string?
Am I creating work for myself here? Am I creating a coupling where there doesn’t need to be one? Do I need to chill out and embrace magic strings/atoms a bit more?
Thanks in advance for any help.