0

I am attempting to create a blueprint for modular, clean architectures. One of the principles that I want to follow is "all objects must be valid at all times".

Also, I am using the module system (java 11).

The problem is that I cannot inject dependencies of my services via their constructor, thus cannot respect the said principle.

Is there any non-obvious way to sneak in parameters via the constructor when using ServiceLoader? I am fine with an isolated usage of reflection in Main to accomplish this.

Alternatives that I've been thinking about:

  • using another method akin to "initialize" which requires the dependencies. The problem with this, or any other setter-based approach is the temporal coupling between the call to this method and actually using the service; or in other words, violating the principle stated above
  • making the "service" not an actual service, but a factory with only one method, which takes the dependencies via parameters, similar to the previous point. My issue with this approach is the accidental complexity created by doing this.

So far, my intention is to take the second approach, but I don't like the accidental complexity. Also, each service will offer multiple operations (meaning that the temporal coupling is a real thing in this case).

I am aware of the other question regarding this, but I am curious if it's possible with a newer java version and/or with with reflection.

Flavius
  • 13,566
  • 13
  • 80
  • 126
  • Ideally, I would need a way to get the class from ServiceLoader, not the object, so that I can do reflection on the constructor prior to creating the object. Ideally, also sorted topologically (the dependency graph of the modules, since modules can have dependencies among each other). – Flavius Dec 29 '19 at 05:42
  • Do you mean ```@AutoService(YourClass.class) ```? – Jay Ehsaniara Dec 29 '19 at 05:46
  • You can create another interface for your parameters or factory class and implement them. This is how it has been done for google guice. https://stackoverflow.com/a/9243191/5801823 – papaya Dec 29 '19 at 05:52

1 Answers1

3

Use the factory method pattern.

The service loader is a factory method pattern, but as you've stated, it doesn't support parameters.

So make the class obtained from the service loader a factory class, i.e. a class with factory methods that takes parameters. That way you can now create the target objects fully initialized, i.e. valid.

It would likely be best to make the factory class do the service loader part, e.g. design you classes similar to how JDBC does.

In JDBC, you use the factory class DriverManager and call e.g. getConnection​(String url, String user, String password).

The factory class will do the ServiceLoader logic to find the implementation of the Driver interface, which is basically a Factory interface. It will then invoke the connect​(String url, Properties info) method to create the actual Connection object.

You should do something similar.

Andreas
  • 154,647
  • 11
  • 152
  • 247
  • I don't think that using the naive implementation of a factory would work. The reason is that each module has a different set of dependencies. So the likely approach with a factory would be that the factory is a marker interface, each module implements it, then each factory impl has a method with an annotation from the core module, so that it can be discovered and reflected at runtime and the proper dependencies can be passed into the factory method. Does it make sense? – Flavius Dec 29 '19 at 09:11
  • @Flavius Don't make sense to me. Using JDBC as example again, the object discovered using `ServiceLoader` is the implementation of the `Driver` interface. What you just called a "marker interface", except it has actual methods, so it's not a "marker" interface. The interface methods are used to obtain objects (e.g. `Connection`), so it would be a "factory" interface. The obtained `Driver` implementation is responsible for any initialization of `Connection`, including obtaining any transitive dependencies. – Andreas Dec 29 '19 at 14:49
  • I suspect we're talking pass each other. The main module orchestrates the other modules ("plugins" in the architecture). These modules have dependencies among each other, with some being just leaves (a DAG). Each such plugin has different dependencies, so it can't be a common interface, because the parameters are varying. I think this detail is the missing piece in your mental model relative to mine. Also, I want to violate OOP and principles as little as possible (make the cleanest and smallest cut in the architecture with the most ROI). – Flavius Dec 29 '19 at 16:54
  • 1
    @Flavius Seems your question is lacking a lot of description, so perhaps you should edit and **clarify** it. – Andreas Dec 29 '19 at 17:04