Dependency injection in Erlang (Disgression from: Longstanding issues: structs & standalone Erlang)

Mikael Karlsson mikael.karlsson@REDACTED
Sat Feb 25 17:47:23 CET 2006


Romain Lenglet wrote:
[...]
> I was one of the initial designers of Fractal, so I am quite
> fluent in Fractal concepts ;-), and like you I am missing some
> of Fractal features in Erlang. I even started implementing a
> generic Fractal membrane in Erlang.

Great! 
...

> Dependency injection would consist in having a naming domain
> local to every process, and letting the mapping between
> process-local names and real process names or pids be done from
> outside of the process implementation (hence, the architecture
> becomes a separate concern, implemented separately from
> functional code).
>
> There are two ways to do that in Erlang. Let's take gen_server as
> an example.
>
> 1- constructor injection: simply pass the pids or names of all
> the processes messages will be sent to, in the Args parameter,
> e.g.:
> gen_server:start_link(client1, [server1], []),
> gen_server:start_link(client1, [server2], []),
> gen_server:start_link(client2, [server1, server10, AnyOtherArg],
> []),
> ...
...
> Drawback: there is no way to modify dependencies after the
> process is started. This is solved with method 2-.
>
> Alternatively, the process names could be stored directly in the
> State record in that case (one record field for every
> dependency), but it makes reflective access more difficult, cf.
> method 2- below...
>
>
> 2- getter/setter/interface injection: implement gen_server calls
> to get / set dependencies, i.e. to modify the process' local
> naming domain. E.g.:
>
> -module(client2).
> ...
> handle_call({getdep, Key}, From, State) ->
>     Deps = State#state.deps,
>     {reply, dict:find(Key, Deps), State};
> handle_call({setdep, Key, Pid}, From, State) ->
>     %% should check that Key is valid...
>     Deps = State#state.deps,
>     NewDeps = dict:store(Key, Pid, Deps),
>     NewState = State#state{deps = NewDeps},
>     {reply, ok, NewState}.
>
>
> Of course, it is preferable to implement both approaches
> simultaneously. In addition, we could also add as in Fractal the
> distinction between optional and mandatory client interfaces, and
> the distinction between singleton and collection interfaces.
> And maybe it would be more efficient to use the process'
> dictionary directly (using get/1 and put/2)...??
>
> Attribute control should be done the same way: through init/1
> parameters, and through gen_server calls ({getattr, Attr} and
> {setattr, Attr, Val}). Although both concerns seem very similar
> that way, they must be separate (i.e. we must not to mix binding
> and attribute control) because the callbacks have a different
> semantics. For instance, when setting a dependency (setdep
> call), one would like to automatically link/1 the client and the
> server process.
>
> The dependency injection implementation above is the very
> minimum, but it allows many things already: transparent
> interposition, application-specific distributed bindings
> implemented in Erlang (e.g. one could implement a transparent
> proxy process between communicating processes, to do load
> balancing between several server processes, or to do group
> communication transparently...), etc.
> Of course, if we want to implement generic membranes as in
> Fractal, we would have to add a lot of things around, but
> functional modules would not have to implement more than the DI
> callbacks shown above, just as in Fractal/Java.
> My opinion is: KISS for developers, and be as Erlang- and
> OTP-compliant as possible.

I agree that one should keep it as simple as possible for developers, and the 
above concept, which resembles Fowlers Spring approach? allow you, as you 
state, to do many things already.
Maybe, for Erlang, you could be a bit more "Fractalish" and expose the Fractal 
interface names as well, meaning that you could expose the binding controller 
in a section like:
handle_call({'binding-controller', bindFc, {Key, PiD}}, From, State) ->
     Deps = State#state.deps,
     NewDeps = dict:store(Key, Pid, Deps),
     NewState = State#state{deps = NewDeps},
     {reply, ok, NewState};

and attribute controller in a similar way. Binding and attribute control could 
also use library functions to make life easier for a developers. 

This would make it easy (but not a must) to use erlang modules
for your own interfaces
 
handle_call({interface1, methodx, Args}, From, State) ->
    Deps = State#state.deps,
    {ok, FooServerName} = dict:find('foo', Deps),
    %% e.g., forward the request:
    Reply = interface2:hey(FooServerName, Args),
    {reply, Reply, State};

module interface1 would implement something like
methodx(Ref, Args) ->
    gen_server:call(Ref, {interface1, methodx, Args}).

module interface2
hey(Ref, Args) ->
   gen_server:call(Ref, {interface2, hey, Args}).

In this way you could implement interface specification and contract
checking in erlang modules, which could be more dynamic compared
to a static interface. The nice thing is that you will get a specification
for your client (and for implementing your server).
I think Fractal has yet another level, differing interface names and interface 
types, meaning that you should maybe bind your interface name to another
module name. This could be quite interesting, the Erlang Corba idl compiler
for instance generates interface modules with methods for idl specified metods 
like:

ping(OE_THIS, NeededArguments) ->
    corba:call(OE_THIS, ping, [NeededArguments], ?MODULE).

OE_THIS is the Corba Erlang object reference and not a erlang PiD, but it 
would still work to bind the erlang idl module as interface module and bind 
the server reference to the corba object. The client using the dependency 
injection could use either the Corba object or a gen_server Pid, without 
needing to know which, the interface would look the same. Maybe it is too 
complicated to bind both interface modules, and references though. Anyhow it 
would be easy to implement a gen_server proxy for the Corba object with 
similar looking interfaces.
And you could probably implement interface modules for for other types of 
interfaces, like UBF contract checking etc.

> For instance, I don't like ErlCOM, which imposes a lot of
> non-functional code in modules (altough the concepts are the
> same as in Fractal, both being rooted in RM-ODP):
> http://www.ist-runes.org/docs/publications/EUC05.pdf

I think a lot of the non-functional code could be hidden if it was implemented 
as its own behaviour.

> It tries to translate implementation solutions that make sense in
> object-oriented, statically typed languages, into Erlang/OTP.

One problem with ErlCOM, if I understood it right, was that you could not
have a many clients - one server relationship?

> But I think that it does not fit Erlang/OTP well.
> For instance, the idea to formally define interface signatures
> statically is a good idea in static typing languages such as
> Java (and I am a strong advocate of that), but does make little
> sense in a dynamic language such as Erlang, in which case it
> restricts flexibility for little gain.

Interfaces are good from a specification point of perspective, implementing
them as Erlang modules would give a better flexibility than static interfaces 
and add dynamics (like contract checking possibilites). But I agree that this 
should be a possibility, and not imposed. The example that you give below 
could be handled in an Erlang interface module if the State (or at least the 
attribute #state.acceptssayhello) is exposed in the interface.

> For instance, the set of messages that can be received or sent by
> an Erlang process at a given time, may change during the
> process' lifetime. In RM-ODP terms, its set of server and client
> interfaces mayh change over time. For instance, consider guards
> in  receive statements, or in handle_cast callbacks:
>
> test(State) ->
>     receive
>         {sayhello, Arg}
>         when State#state.acceptsayhello == true ->
>             ...
>     end.
>
> handle_cast({sayhello, Arg}, State)
>     when State#state.acceptsayhello == true ->
>     ...
>
> This cannot be captured in ErlCOM or Fractal, which consider that
> the set of client and server interfaces, and their signatures,
> (i.e. the component's type) do not change after component
> creation.
>
> One should not impose such limitations in Erlang. So le'ts keep
> it simple, stupid...
> And again, I think that the only thing that should be imposed to
> developers is the implementation of DI callbacks as described
> above.
> Any other control should be implemented outside of the functional
> modules' implementations, and even should be made optional.

I agree. 
One problem with a gen_server is that it will kill your client if you do not 
get a response within the Timeout time, 5 seconds default I think. This is 
not acceptable if you bind many components together expecting them to last 
for a long time. I think it could be a good idea to consider a fractal 
component behaviour, gen_fc ?, overriding the gen_server behaviour, making 
life, or at least the development, easier for developers.

Regards
Mikael Karlsson





More information about the erlang-questions mailing list