When is Assisted-inject useful?

842 views Asked by At

I'm currently using Guice in my App. However I find myself mostly using assisted inject because there is a chain of injected objects that all depend on what was the input of the program. Hence almost everything is assisted inject.

For instance A need B who need C who need Z which needs input from the command line. In the end I feel like everything will be assisted injected. So given that I'm stuck with it I want to be sure that I'm using it right.

I personally feel like writing my own factories would be as good. Moreover, in addition to the same advantage I could further limit the creation of my objects in question to these factories.

Hence my question here is, how useful is it really to use assisted inject, is it only the idea of having things assisted and non assisted as the same time? What if like in my case you have only have assisted parameters?

Their must be some gain at organizing it with assisted injection. I just don't see it.

PS:

My dependency is as such:

I have an InfrastructureService, which require a KnowledgeBaseService which in turn require an ConFigDataObject. My configDataObject contains information coming from the inputs of the program. The configDataObject store these information after doing some validation and processing on those inputs. For instance a string representing file path can be provided to it, and it will validate that it is file that exist, and have a getter method that return that File to its consumer. Other things might be URLNname to real URL Object, and so on.

The important point here is this following graph: InfrastructureService -> KnowledgeBaseService -> ConFigDataObject -> InputData

Therefore the InfrastructureService can only work with a knowledgeBaseService that is started with the right inputFile, URL, workingfolder and etc.... which is provided to it with a configDataObject, that receive them from the input of the program and store a processed version of them.

Therefore as of now what I do is to have a assistedFactory to create the knowledgeBaseService. It takes as parameter the ConfigDataObject. The configDataObject is created with a factoryMethod (Scala Companion Object). Finally, the an assistedFactory is also create for the InfrastructureService which takes as parameter for its creation method a KnowledgeBaseService.

As you can guess everything is pretty much created upfront, and somehow manually. I find it odd.

2

There are 2 answers

0
MaatDeamon On BEST ANSWER

Here is what i did to finally solve the issue in a more elegant way than using an unnecessary chain of assisted injected due to the parameters of the application.

that is If as in my case your data comes from the command Line then the right appraoch i believe is to bind the type of the input data structure to instance input structure obtained from the command line:

object MyApp extends App {

  val config = ConfigData(args(0))

  val injector = Guice.createInjector(module(config))
  val service = injector.getInstance(classOf[InfrastructureService])

  println("The service name is:" + service.kbService.config.configName)

}


case class module(config: ConfigData) extends AbstractModule {
  def configure(): Unit = {
    bind(classOf[ConfigData]).toInstance(config)
  }
}

case class ConfigData(val configName: String)

class KbService @Inject() (val config: ConfigData)

class InfrastructureService @Inject() (val kbService: KbService)

I believe the Key here is to remind yourself that the module can be parameterize with any input data deem necessary

6
Vladimir Matveev On

It looks like that you are overusing a notion of "dependency". You should separate classes which encapsulate user input (in fact, any data) from classes containing business logic first, and then you should pass these data via methods, not through injection, because user input is not a dependency.

This way you will almost never need assisted injection, because you can create "data" classes directly with new (it is OK because they have no dependencies), and "behavior" classes can be injected into each other via constructor in standard way. Then "behavior" classes will do they work, passing around objects of "data" classes, not getting them as dependencies. You will see that the need for assisted inject disappears, and your program becomes much more simple and understandable.

For example, instead of having something like the following:

public class OtherDependency {
    private final int s;

    @Inject
    OtherDependency(@Assisted int s, ...) {
        this.s = s;
        ...
    }

    public void doWork() { /* use s */ ... }
}

public class SumService {
    private final int x1;
    private final int x2;
    private final OtherDependencyFactory depFactory;

    @Inject
    SumService(@Assisted int x1, @Assisted int x2, OtherDependencyFactory depFactory) {
        this.x1 = x1;
        this.x2 = x2;
        this.depFactory = depFactory;
    }

    public void doWork() {
        int s = x1 + x2;

        OtherDependency dep = depFactory.create(s);
        dep.doWork();
    }
}

public class EntryPoint {
    private final SumServiceFactory sumServiceFactory;

    @Inject
    EntryPoint(SumServiceFactory sumServiceFactory) {
        this.sumServiceFactory = sumServiceFactory;
    }

    public void start() {
        Scanner sc = new Scanner(System.in);
        int x1 = sc.nextInt();
        int x2 = sc.nextInt();

        SumService sumService = sumServiceFactory.create(x1, x2);
        sumService.doWork();
    }
}

(I don't know much about your program, obviously, but this is what I have thought of when I have seen "chain of dependencies which need user input first")

you should make something like

public class OtherDependency {
    @Inject
    OtherDependency(...) {
        ...
    }

    public void doWork(int s) { /* use s */ ... }
}

public class SumService {
    private final OtherDependency dep;

    @Inject
    SumService(OtherDependency dep) {
        this.dep = dep;
    }

    public void doWork(int x1, int x2) {
        int s = x1 + x2;
        dep.doWork(s);
    }
}

public class EntryPoint {
    private final SumService sumService;

    @Inject
    EntryPoint(SumService sumService) {
        this.sumService = sumService;
    }

    public void start() {
        Scanner sc = new Scanner(System.in);
        int x1 = sc.nextInt();
        int x2 = sc.nextInt();

        sumService.doWork(x1, x2);
    }
}

All user input is transferred from class to class via method parameters, and these classes themselves are stateless and are just normally injected. No need to use assisted inject at all.

Update

I have read your update. There are multiple possibilities to do what you want, with varying complexity.

First, one of the most simple variants (and I'd argue the best one in these circumstances) is, again, to pass your input data via method calls. I do not see how your architecture forbids this. Just make all methods on KnowledgeBaseService which need this data accept it, and pass it from InfrastructureService. You do not need assisted inject here.

Second, you can acquire your user input before creating an injector, and then do toInstance() bind. This depends on the actual architecture and may not work because of it. I believe this is the easiest approach which give very simple and flexible structure.

Third, you can use providers. Create a provider for your ConfigDataObject which asks for user input and creates corresponding instance, then bind ConfigDataObject to it in Singleton scope. This will work if user input does not fail, because you should not throw exceptions from providers. If you need user input validation, use throwing providers extension. User input will then be triggered upon the first access to the provider, and then due to its scope the result will be cached.

There may be other ways, but these are which I was able to come up with.