binaryseed
I like using defdelegate along with @behaviours to make nice simple contract shim modules. The only gotcha is that defdelegate is a compile-time construct, so the module that is delegated to is fixed per-env upon compile.
I’ve put together a little experiment that mirrors defdelegate but enables runtime configuration of the delegated module..
It lets you define a contract module
defmodule SomeContract do
use RuntimeDelegate, default: DefaultImpl
@callback fun(binary, atom) :: term
defdelegate fun(foo, bar)
end
defmodule DefaultImpl do
@behaviour SomeContract
def fun(foo, bar) do
# Do the real thing
{__MODULE__, foo, bar}
end
end
defmodule AlternateImpl do
@behaviour SomeContract
def fun(foo, bar) do
# Do it another way
{__MODULE__, foo, bar}
end
end
And then you can control it at runtime:
assert {DefaultImpl, "foo", :bar} = SomeContract.fun("foo", :bar)
Application.put_env(:contract, SomeContract, AlternateImpl)
assert {AlternateImpl, "foo", :bar} = SomeContract.fun("foo", :bar)
It could be used to avoid hitting APIs in tests, or to do controlled rollout of new code, etc…
It’s not a lot of code, just a little macro that re-uses some of the code for Kernel.defdelegate
defmodule RuntimeDelegate do
defmodule DefDelegate do
defmacro defdelegate(fun) do
fun = Macro.escape(fun, unquote: true)
quote bind_quoted: [fun: fun] do
[{name, args, as, as_args}] = Kernel.Utils.defdelegate(fun, [])
def unquote(name)(unquote_splicing(args)) do
impl = Application.get_env(:contract, __MODULE__) || @default_impl
unless impl, do: raise(ArgumentError, "No implementation found")
impl.unquote(as)(unquote_splicing(as_args))
end
end
end
end
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
@default_impl opts[:default]
import Kernel, except: [defdelegate: 2]
import RuntimeDelegate.DefDelegate
end
end
end
Thought I’d toss it out there to see what folks thought.. I think of it in the spirit of Jose’s article about explicit contracts: Mocks and explicit contracts « Plataformatec Blog
Trending in Discussions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #blog-post
- #elixir-ls
- #ai
- #elixirconf-us
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 2- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
jeffs
Thanks @binaryseed, I think this is great! I am following Jose’s post on mocks, but I struggle with swapping out contract implementations in a frictionless way (dependency injection sadly doesn’t work that well in my case). I recently discovered the
defdelegate+@behaviourspattern, but in my particular case I need runtime config, so I was stuck. I believe this will solve my needs perfectly, so thanks for sharing!I’m not sure if there are any gotchas or anti-patterns around this, so I am curious to hear what others think as well, and if I run into any trouble with it I will share.
jeffs
I did notice one potential issue -
Kernel.Utils.defdelegatesays it will be removed in v2.0https://github.com/elixir-lang/elixir/blob/master/lib/elixir/lib/kernel/utils.ex#L28
(Oh, and I only just now realized this is 2 years old. That might explain why I’m getting an error using it at the moment. The error is fixable if you remove the wrapping square brackets from the match of
Kernel.Utils.defdelegate)