binaryseed
Dynamic defdelegate - idea for contract shim modules
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
First Post!
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 + @behaviours pattern, 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.
Most Liked
jeffs
I did notice one potential issue - Kernel.Utils.defdelegate says it will be removed in v2.0
https://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 )
Popular in Discussions
Other popular topics
Chat & Discussions>Discussions
Latest on Elixir Forum
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
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex









