Dependency injection in Julia


#1

Wondering if anybody found a good way to implement dependency injection in Julia. I have many situations where I use “adapters”: take for instance a Database module which uses various adapters (MySQL, SQLite, Postgres, etc).

And what I usually end up with is:

  • a const in the module global scope which references the adapter
  • a lot of eval statements in the line of:
if haskey(SearchLight.config.db_config_settings, "adapter") && SearchLight.config.db_config_settings["adapter"] != nothing
  db_adapter = Symbol(SearchLight.config.db_config_settings["adapter"] * "DatabaseAdapter")

  include("database_adapters/$(db_adapter).jl")
  Core.eval(@__MODULE__, :(using .$db_adapter))
  Core.eval(@__MODULE__, :(const DatabaseAdapter = $db_adapter))
  Core.eval(@__MODULE__, :(export DatabaseAdapter))
else
  const DatabaseAdapter = Nothing
end

Looks like a code smell to me.

I think a nice solution would be to have a mechanism to pass arguments when using a module, in the line of:
using Database(adapter = MySQLAdapter)


#2

I think a more Julian approach would be to utilize a kind of DataBaseAdapter interface and rely on the caller to call setup appropriately; something like:

module Adapters

abstract type Adapter end

"Subtype must define `connect!(x::MyAdapter)` to satisfy interface"
function connect! end

"Subtype must define `query(x::MyAdapter)` to satisfy interface"
function query end

end



module AdapterAs

import Adapters

struct AdapterA
end

Adapters.connect!(::AdapterA) = # ...
Adapters.query(::AdapterA) = # ...

end



module App

import Adapters

const ADAPTER = Ref{Adapter}()

function setadapter!(x::Adapter)
    ADAPTER[] = x
end

function query(sql::String)
    Adapters.connect!(ADAPTER[])
    Adapters.query(ADAPTER[])
end

end

With this kind of setup, you leave it to the driving application or script to call import AdapterAs and App.setadapter!, or potentially import AdapterBs if there was an alternative adapter they wished to use.


#3
  1. Naming is a bit hacky. I would try to slim it.
  2. I would try to factorize by parametrizing as mush as i can (see below)
  3. otherwise not a bad pattern
# parametrizable with (conf section=="DB")

if haskey(FOO.config.db, "driver") && FOO.config.db["driver"] != nothing        # todo parametrize config section "db"
    driver = Symbol(FOO.config.db["driver"] * "DbDriver")                       # parametrize julia type suffix

    include("db/$(driver).jl")                                                 # parametrize directory
    Core.eval(@__MODULE__, :(using .$driver))
    Core.eval(@__MODULE__, :(const DbDriver = $driver))                         # parametrize julia type suffix (see previous one)
    Core.eval(@__MODULE__, :(export DbDriver))                                  # idem
else
    const DbDriver = Nothing                                                    # idem
end


#4

Another reason for reviving DBAPI? :wink:


#5

Thanks

I’m actually happy with the naming. Must not forget that this is not user code, but framework code. It’s not meant to be typed by the users of the library (they just need to define the config Dict). In such cases (write once, read multiple times) I prefer to err on the side of readability.


#6

Wouldn’t that just move the problem somewhere else?

Also, curious if there’s any solution for

  1. defining optional dependencies (right now I have to make SQLite.jl, MySQL.jl and PgLib.jl explicit dependencies of my package forcing users to install all, even if they’ll only use one backend).
  2. creating a plugins system where a 3rd party user can extend my library and add their own dependencies (like in this case, somebody plugging in a MariaDB adapter with its dependency on an a MariaDB.jl)

For @kristoffer.carlsson :slight_smile:


#7

You can use Requires.jl for some of that.


#8

@kristoffer.carlsson Cool, thanks - let me give it a try.


#9

Naming can be very subjective, so i will not try to defend one vs another. My purpose was to reduce variations without real functionality eg. noise to better allow factorization of function. If you feel it works well. That’s ok.

My understanding is that it is not only a problem of user vs admin. But a problem of toolchain selection.
A choice not only made by one person and forever. It can be very wide and open.

Do you know the backend selection used by Plots.jl @ https://github.com/JuliaPlots/Plots.jl/tree/v0.19.3/src/backends.jl

Requires.jl can help to load different pkg on the fly.
Plots.jl has some good tracks to store choices and handle tiny workflow to make them callable.


#10

Definitely:

There are only two hard things in Computer Science: cache invalidation and naming things.
– Phil Karlton

What I meant was that the above code was “private”. The user would just say driver = MySQL.

Yes, the Plots.jl backends analogy is exactly what I have in mind. I don’t want to comment yet as I still have to test Requires.jl (although deep in my heart I have the feeling that Pkg will complain if the dependencies are not explicitly declared and installed – and that users won’t be able to add new “backends” on the fly without changing the Project.toml of the original package).