#1439 Dependency injection

panicdotal Sat 5 Mar 2011

It would be nice if Fantom had some DI out-of-the-box.

Simple requirements

  • Dependency resolution: when constructing an object, resolve the dependent objects.
  • Mapping: map a Type (Mixin or class) to a specific Implementation
  • Mapping: map a Name (Str) to a Type or (pre-made) Obj
  • Scoping: By default, a class gets instantiated each time it is needed, but have a Singleton facet that marks a class as only getting created once.
  • Entensibility: allow the resolution/mapping/scoping algorithms to be overriden

It could be done with separate classes, or perhaps integrated into the Type class.

I've implemented it with a facet ( Singleton ), a class ( InjectionMapper ) that provides the dependency resolution and a sub-class ( SimpleInjectionMapper ) that implements the mapping functionality.

I've posted my implementation on bitbucket.org - I've never used it or Mercurial before so I hope I've done it right. Let me know if you can't get it or I messed anything up.

Here's a sample from my unit tests:

**
** InjectionMapper
** Find implementation for Mixin
** 
Void test2()
{
  // static
  MB mb := InjectionMapper.find(MB#)
  // no guarantee which implementation will be found
  verify (mb is B1 || mb is B2)

  B2 b2 := InjectionMapper.find(B2#)
  verify (b2.a.name == null)
  verify (b2.a.age == 0) 

  // non-nullable make parameter is resolved
  verify (b2.name == "") // defVal

  // nullable make parameter is not resolved
  verify (b2.description == null)
}

**
** SimpleInjectionMapper
** configure type-mapping programmatically
** 
Void test3()
{
  mapper := SimpleInjectionMapper()
  mapper[MB#] = B1#

  A a := mapper[A#]
  verify(a.name == null)

  MB mb := mapper[MB#]
  verify (mb is B1) 

  // examine the mapping
  Type t := mapper.map[MB#]
  verify (t == B1#)

  mapper[MB#] = B2#
  mapper[A#] = A() { name = "fanDI" } // Object

  mb = mapper[MB#]
  verify(mb is B2) 
  verify((mb as B2).a.name == "fanDI")
}

**
** SimpleInjectionMapper
** configure name-mapping programatically
** 
Void test4()
{
  mapper := SimpleInjectionMapper()
  A a := A() { name = "fanDI"; age = 1 }

  mapper["MyA"] = a
  mapper["MyMB"] = B2#
  mapper["Num"] = 12

  verify ((mapper["MyA"] as A).name == "fanDI")
  verify ((mapper["MyMB"] as B2).a.name == null)
  verify (mapper["Num"] == 12)

  mapper[A#] = a
  verify ((mapper["MyMB"] as B2).a.name == "fanDI")

  Obj a1 := mapper[A#]
  Obj a2 := mapper["MyA"]
  verifyEq (a1, a2)

}

**
** SimpleInjectionMapper
** configuration
** 
Void test5()
{
  // configuration from test4()

  config := 
  """fanDI::SimpleInjectionMapper {
       map=[fanDI::A#: fanDI::A {
                name="fanDI"; age=1 },
            "MyA": fanDI::A {
                name="fanDI"; age=1 },
            "MyMB": fanDI::B2#,
            "Num": 12
       ]
     }"""

  mapper := InjectionMapper(config)
  verify ((mapper["MyA"] as A).name == "fanDI")
  verify ((mapper["MyMB"] as B2).a.name == "fanDI")
  verify (mapper["Num"] is Int)
  verify (mapper["Num"] == 12)

  Obj a1 := mapper[A#]
  Obj a2 := mapper["MyA"]
  // NOTE: these will be two different objects with identical fields
  verify (a1 != a2)

}

**
** InjectionMapper
** test Singleton facet
** 
Void test6()
{
  mapper := InjectionMapper()

  B3 b3a := mapper[B3#]
  verify (b3a.name == "")
  b3a.name = "Bob"

  B3 b3b := mapper[B3#]
  verify (b3b.name == "Bob")
  verify (b3a.hash == b3b.hash)
}

**
** SimpleInjectionMapper
** test Singleton facet
** 
Void test7()
{
  mapper := SimpleInjectionMapper()
  mapper[MB#] = B3#
  mapper["MyMB"] = MB#

  B3 b3a := mapper[B3#]
  verify (b3a.name == "")
  b3a.name = "Bob"

  B3 b3b := mapper[B3#]
  verify (b3b.name == "Bob")
  verify (b3b.hash == b3a.hash)

  B3 b3c := mapper["MyMB"]
  verify (b3c.name == "Bob")
  verify (b3c.hash == b3b.hash)

}

**
** SimpleInjectionMapper
** test class hierarchy
** 
Void test8()
{
  mapper := SimpleInjectionMapper()
  A a := A() { name = "Jim" }
  mapper[A#] = a
  B3 b3 := mapper[B3#]
  b3.name = "Joe"

  BB bb := mapper[BB#]
  verify(bb.name == "Jim")
  verify(bb.a == a)

  B3 b3x := mapper[B3#]
  verify(b3.hash == b3x.hash)

  //Singleton facet on superclass does not apply to subclass
  BB bbx := mapper[BB#]
  verify(bb.hash != bbx.hash)
}

@Serializable
class A 
{
  Str? name 
  Int age
}
mixin MB { }

class B1 : MB { }

class B2 : MB 
{
  A a
  Str name
  Str? description

  new make(A a, Str name, Str? description)
  {
    this.a = a;
    this.name = name
    this.description = description
  }
}

@Singleton
class B3 : MB {
  Str name
  new make(Str name) { this.name = name }
}
class BB : B3 {
  A a
  new make(A a) : super(a.name)
  {
    this.a = a
  }
}

timclark Sat 5 Mar 2011

Why is a DI framework required in Fantom, and why would it need to be embedded in sys::Type?

Don't we already have type safe DI already built into the language, for example:

class Pie
{
   new make(Filling filling)
   {
     this.filling = filling
   }
   const Filling filling
}

class Main
{
  Void main()
  {
    applePie := Pie(Apple())
    pecanPie := Pie(Pecan())
  }
}

For some applications having a registry of the objects that implement different types may have some benefits but surely not every application.

Good luck with the code though, it is nice to see a load of unit tests to demonstrate the ideas!

brian Sat 5 Mar 2011

I must admit I often don't get the whole "big DI framework" thing. At a basic level wiring up types using loose bindings can be done all sorts of way already:

  • sys::Service
  • Env.config
  • Env.index
  • reflection

Also remember that Fantom makes static factories and constructors look exactly alike.

So if there some really compelling concrete use case that isn't easily handled with one line of code?

panicdotal Sat 5 Mar 2011

@timclark: Yes, that is dependency injection, but what I'm talking about is automatic dependency resolution.

jodastephen Sat 5 Mar 2011

I think of the DI that I want more in terms of "my class needs x" rather than a big framework to be able to manage it. I'm no great fan of Spring for example. Here are some thoughts:

Lets say I'm writing a piece of code that accesses the database. When I write this class, what I want to say is "my class needs a database connection". So, lets express that programmer intent directly:

class PersonDAO {
  requires DbConnection conn
  Person query(Str name) {
    // use the db connection to find a person
  }
}

So, conn is simply a slot that is auto-populated before the instance is used.

At the other end, there is some class that supplies the relevant things.

class Main {
  {
    publish {
      conn := createDbConnection()  // local variable published
      processApplication()
    }
  }
}
// somewhere else deep within processApplication():
{
  dao := PersonDAO()   // published variable injected
  person := dao.query("Stephen")
}

Again, its about code documenting intent. (I think the publish part could have an alternate form in config, but I'm generally in favour of code as config.)

Why syntax? Well I suspect that some of this can then be checked, potentially at compile time. For each method/class that requires something, there needs to be something that does the publish higher in the call stack.

Now, of course its not this simple. You can have per-thread objects, multiple objects of the same type, dynamic factories and other complications. But if you see the code above as more expressive of intent (as I do) then its probably worth investigating.

More generally, DI is about injecting stuff that is environmental and subject to change. One classic example is the system clock:

class Foo {
  requires Clock clock;
  boolean isThursday() {
    return clock.today().weekday == Weekday.thu
  }
}

This approach allows regular code to publish the genuine system clock, and testing code to publish a fixed clock.

The need for a weak, late bound set of injected data is now pretty widely used in Java and seems like good practice. Fantom appears to have some of that in the service structure, although I think you'd find it isn't widely understood and relies on a single global namespace area, which I'm a bit uncomfotable with. I think a publish/requires type approach would be simpler to comprehend and closer to the programmers intent, but the devil is in the details.

rfeldman Sun 6 Mar 2011

In terms of the paradigm of DI, as I understand it the important thing is the inversion of control in which the component declares its dependency on other components rather than looking up specific implementations on its own.

You can achieve that structure trivially by simply declaring a field non-nullable; that way the constructor will be forced to set it before the component can be used. Inversion of control? Achieved.

That said, it's nice to be able to shortcut this. The Spring @Autowired annotation, for example, tells Spring to populate the field with the first instance it can find that could go in that slot. If there are multiple candidates, Spring throws an exception.

Couple that with the ability to annotate a class with @Service to get it to automatically register itself and you save yourself a lot of boilerplate constructor-work.

I haven't done a lot with Fantom reflection, but it seems like you could make this happen with a library. If you required it-block constructors for eligible services, I believe you could satisfy non-nullable fields pretty easily without resorting to runtime-generated proxy classes.

In fact, you could probably even get away without using an @Autowired annotation like Spring does - you could just inspect all the not-nullable fields and try to populate them all using already-registered services.

Something like this:

@Service
class Foo
{
  Bar bar
  // ...
}

@Service
class Bar
{
  Baz baz
  // ...
}

@Service
class Baz
{
  // ...
}

...and then one call to Service.registerByFacets() or something and you get singletons of each of those instantiated, populated via it-block constructors, and ready to be used, all without having to manually code a single constructor.

Man, now I kinda want to write that library...

panicdotal Sun 6 Mar 2011

The fanDI code I posted is not a great big framework, just a class that handles the dependency resolution. There is not a lot of code to it.

It uses constructor injection. All non-nullable parameters to make() are resolved.

This is the key algorithm:

// get constructor
  ctor := (impl as Type).method("make", false)
  if (ctor == null) return t.make

  // resolve constructor parmeters
  Obj?[]? args := [,]
  ctor.params.each { 
    args.add(it.type.isNullable ? null : resolveType(it.type, mapper))
  }

  //call constructor
  return ctor.callList(args) 

It does not force the use of the InjectionMapper; obviously, you can write your classes with constructor-based dependencies and construct and supply dependencies manually.

If the type to be resolved is not concrete, InjectionMapper will search the pod for a concrete implementation.

virtual Obj mapToImplementation(Type t)
{
  // if t is concrete, return it
  t.isClass ? t : locate(t)
}

virtual Obj locate(Type t, Pod p := t.pod)
{
  // find an implementation of t in the Pod
  return p.types.find |Type target->Bool| {target.isClass && target.fits(t)}
}

brian Mon 7 Mar 2011

In terms of the classic case of binding a service provider to a service consumer, the design intention is to use sys::Service:

class SomeApp
{
  const DbService db      := Service.find(DbService#)
  const UserService users := Service.find(UserService#)
}

You could even stick an it-block constructor on there to allow overrides of the service.

Since the Service framework is defined in sys it is a core concept usable by every pod.

Login or Signup to reply.