Fl4m3Ph03n1x
How to manage files that have behaviours and defer to an implementation at the same time?
Background
I have an umbrella project that has several apps. One of these apps, is called manager and it has a Public API that is used by other apps.
The file where all of this happens does two things at the same time:
- Specifies the behaviour of the Public API
- Defers to the implementation of the Public API
Code
Here is an example of such a file:
defmodule Manager do
alias Manager.{Interpreter, PriceAnalyst}
@callback activate(String.t, String.t) :: any
@callback valid_strategy?(String.t) :: boolean
##### Implementation #####
@behaviour __MODULE__
@impl __MODULE__
defdelegate valid_strategy?(strategy), to: PriceAnalyst
@impl __MODULE__
defdelegate activate(syndicate, strategy), to: Interpreter
end
Problem
Now, the problem with this is that it doesn’t really work. Specifically, this code generates the error:
got “@impl Manager” for function valid_strategy?/1 but this behaviour does not specify such callback. There are no known callbacks, please specify the proper @behaviour and make sure it defines callbacks
Which is crazy to me, because I am defining the callbacks.
Alternative Code
Another way of doing the same thing that avoids the error would be:
defmodule Manager do
alias Manager.{Interpreter, PriceAnalyst}
@callback valid_strategy?(String.t) :: boolean
@spec valid_strategy?(String.t) :: boolean
defdelegate valid_strategy?(strategy), to: PriceAnalyst
@callback activate(String.t, String.t) :: any
@spec activate(String.t, String.t) :: any
defdelegate activate(syndicate, strategy), to: Interpreter
end
However this way I run the risk of deviating the defdelegate from the callback, since this is not implementing any behaviour. This is not a risk I want to incur into.
Questions
So, by now I am sure some of you may be asking:
- If this file does 2 things, why not have 2 files?
And it is a fair question. The problem here is:
- What would I name such files?
- Where would I put them? (what would be the folder structure in this project?)
Both questions have no clear answer to me.
So, a couple of final questions remain:
- How can I avoid the behaviour error?
- How would you solve this issue?
Trending in Discussions
Other Trending 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
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #hex
- #performance










First 10 of 16 Posts
LostKobrakai
A module cannot define a behaviour and implement it as well. Therefore you need 2 modules as you seem to have gathered already. But that’s not what I’d gather you should fix/change.
What you seem to be missing is the following: Usually the implementation implements a behaviour not the delegating abstraction. So within
:managerthe implementation seems to bePriceAnalyst. It needs to implement a behaviour. The behaviour might beManager.PriceAnalyzer.Now you’ll have (iirc)
:cli, which depends on:manager. If you want to mock out that dependency in:cliyou’d limit the implementation calling functions onManagerto a single module, say:Cli.Backend.Managerand this one implements a behaviour as well. But this time it’s a behaviour defined within:cli(e.g.Cli.Backend), which caters to the needs of what exactly:clineeds of any backend implementation.The public interface of the
Managermodule doesn’t really need a behaviour. It’s the module as is.When testing
:manageryou can mockManager.PriceAnalyzer, when testing:cliyou can mockCli.Backend. This way whenever:manageradds new features (like new callbacks),:clidoesn’t need to change at all – at least unless it as well needs new features based on those added to:manager.Edit:
If you don’t need to mock anything within
:manageryou can surely skip that step. The important part is that:clishould not mock behaviours of:manager.Fl4m3Ph03n1x
This example is actually a simplification of the actual issue. The real
Managermodule does not only delegate toPriceAnalyst, it also delegates to other modules. Following is a more complete version (though not the full thing, for the sake of brevity) of the file:I thought that by removing complexity and simplifying my example I would make life easier for the people trying to understand the issue, but now I see my simplification removed too much and has led you astray.
Apologies for that. I hope now the issue is more clear.
EDIT
I have updated the Description of the issue to better reflect the problem at hand. It should be clearer now.
LostKobrakai
To be honest, this doesn’t change anything to my answer:
Managerdoesn’t need to implement a behaviour. You might have behaviours in:manager, but they’re implemented by the modules actually doing the work not byManager. Anything depending on:managershould create its own behaviour, which is then implemented by a module, which calls functions onManager. Those dependant apps should only mock their own behaviours.wolf4earth
You might be interested to hear about
kniggewhich is a library I wrote to literally address this exact issue.It’s opinionated in that it assumes that you have a single behaviour and delegate to one implementation of this behaviour but it certainly goes into the direction you’re suggesting here.
Fl4m3Ph03n1x
The thing is, if I do it your way, I will have a
Cli.Backend.Managermodule that will be in practice a copy of the Public API ofManager. This way, changes onManagerwill always mean I have to changeCli.Backend.Manageras well. If I forget to do that,Cli.Backend.Managerbecomes an ad-hock mock, i.e., a mock that doesn’t represent the real thing (that real thing beingManager’s Public API).This was the issue I tried tackling in the first discussion, and the solution I found was to just make
Clidepend onManager’s Public API. This way, changes inManager’s Public API create compile errors in theCli, which is good imo, because it forces me to have both apps in sync.I am unsure of this solution. I understand the “Don’t mock what you don’t own” principle, but I am not really sure it applies here, as I own all the apps. Even so, adding a Facade just because of that doesn’t seem to grant me any benefits (or at least I don’t see them yet).
LostKobrakai
That’s imho a big fallacy. This might for sure be true right now as you’re essentially developing one functionality in two applications forcefully trying to add separation. But imagine besides
:clithere’s also a:httpdependant on:manager. The manager adds some kind of session handling because the http app needs that. The:cliapp doesn’t and therefore shouldn’t be bothered by the change in:manager. That’s also the point where you’re no longer dealing with 1:1 copies. The:clibehaviour will become different to the:httpbehaviour and both will be different to the functions onManager. It also automatically means:clican at any point have another implementation of its behaviour, which doesn’t even depend on:manageranymore. That independence is actually what ideas like DDD or “Don’t mock what you don’t own” want to point people to. It’s also the reason people usually ditch those principles for smaller scale, simple projects. The independence only becomes truely useful when things grow.If you treat all your apps as “your own” then there is no separation. I still think you don’t need a behaviour for
Managerthough, but only for the implementations within:manager. Generally you can ask the question of: How would it work if everything would be in one application and take the answer.Fl4m3Ph03n1x
This option however raises some questions:
Cliapp ever depend on something else that is notManager?Managersomehow not affectCli.Backend.Manager?My answer to both would be:
Clianyway even if I have a Facade.Manager’s Public API will eventually mean changes inCli.Backend.Managerand then changes acrossCliin a very classic Shotgun Surgery code smell: Shotgun surgery - WikipediaI must admit I am terrified of point 2, having to work in a project that suffers from this issue I can say it doesn’t feel nice.
But, going with your solution I have one more question:
Cli.Backend?I understand that
Cli.Backend.Managerwould be a Facade of Manager, so if that is the case, what would be the contents of the fileCli.Backend? Would it be an empty file?LostKobrakai
If the cli mocks a manager behaviour it means it needs to “change” with any change to the behaviour. You just don’t notice it because
moxautomatically implements all callbacks.In your current situation I feel you could just move the behaviour you have in
:managerinto:cli, have a super dump implementation withCli.Backend.Manager, which basically delegates everything toManager. Currently:cliis driving the interface andManagerjust does need to provide the implementation.But what does this give you:
Again, another client to
:managerrequires some function to be split up into two separate functions – sayfun_old/2becomesfun_a(a) |> fun_b(b). So the public API of:managerchanged. Instead of needing to hunt down eachfun_old/2call in:cliyou can just leavefun_old/2be the interface ofCli.Backend, but change only the implementation:The core of
:clididn’t change at all. It still depends onfun_old/2to be available like before. The implementation with manager is the only place concerned with the change inManager.This again is something you benefit the most once things actually diverge and no longer fit 1:1. As long as
Manageris a perfect implementation for the interface:clidepends on this will look like useless indirection as you said.christhekeele
I messed with an imperfect metaprogramming solution to this a few years ago: Behaviours with Defaults for Elixir · GitHub
A better pattern is to define a
Thing.Behaviourmodule and a separateThing.Behaviour.Implmodule for other people to use. They can even pass the latter todefdelegate, but I don’t recall if that lets them flag things as@impl.Fl4m3Ph03n1x
Playing around with this architecture something struck me: How do you then test
Cli.Backend.Manager?You would need a mock for
Managerand you would eventually find the same issue I have here - you would need to use the behaviour directly fromManageror you would be forced to do an integration test to make sure you are calling it with the correct parameters.