2

I have a slight dilemma involving Guice and avoiding non-Guice singletons. Consider a multi-module project where there are 3 modules: shared, frontend and backend. Both the frontend and the backend use an instance of a Profiling class inside of the shared module (which times methods, and is used extensively throughout the project).

Almost every single class requires the use of this Profiling instance (including User objects created dynamically when a user connects).

If every single class requires an instance of the Profiling class, there are drawbacks to different methods of doing it:

Solution 1 (in the constructor, copied to the instance field):

private final Profiling profiling;

@Inject
public User(Profiling profiling, String username)

Drawback: You have to include a Profiling object in every single constructor. This is cumbersome and slightly pointless. You also have to store Guice's Injector statically (or inject it) so that you can create the User objects on-the-fly and not just when the program is first loaded.

Solution 2 (as a instance field only):

@Inject
private Profiling profiling;

public User(String username)

Drawback: Similar as above, you have to use Guice's Injector to instantiate every single object. This means that to create User objects dynamically, you need an instance of the Injector in the class creating the User objects.

Solution 3 (as a static field in one (main) class, created manually by us)

public static final Profiling PROFILING; // Can also use a get method

public Application() {
    Application.PROFILING = injector.getInstance(Profiling.class)
}

Drawback: Goes against Guice's/dependency injection recommendations - creating a singleton Profiling object which is accessed statically (by Application.PROFILING.start()) defeats the purpose of Guice?

Solution 4 (as a static field in every single class, injected by Guice)

@Inject
private static Profiling profiling;

// You need to request every single class:
// requestStaticInjection(XXXX.class)

Drawback: Again, this goes against Guice's/dependency injection recommendations because it is statically injecting. I also have to request every single class that Guice needs to inject the Profiler into (which is also cumbersome).

Is there any better way to design my project and avoid falling back to the singleton design patterns I used to use?

TL;DR: I want to be able to access this Profiling instance (one per module) across every single class without falling back to singleton design patterns.

Thanks!

1
  • 1
    I would inject it into any guice-managed class as you suggested (non static). I would use Constructor injection only (because that enforces that a manual construction will have to pass the same arguments and can not create an invalid instance). However, User does not sound like a guice-managed class. I would (maybe, there is not enough info here) implement that through a User-factory that can be injected with the Profiling class and abstract away the issue of having a profiling instance in each user. Commented Mar 13, 2017 at 11:41

1 Answer 1

5

Pragmatically, I would use a normal singleton, maybe through a single-field Enum or a similar pattern.

To see why, you should ask the question: What is the purpose of Guice, and of dependency injection in general? The purpose is to decouple the pieces of your application so they can be independently developed and tested, and centrally configured and rearranged. With that in mind, you need to weigh the cost of coupling against the cost of decoupling. This varies based on the object you're choosing to couple or decouple.

The cost of coupling, here, is that you would be unable to operate any piece of your application without a real working instance of Profiling, including in tests, including for model objects like User. Consequently, if Profiling makes any assumptions about its environment—the availability of high-resolution system timing calls, for instance—you will be unable to use classes like User without allowing Profiling to be disabled. Furthermore, if you want your tests to profile with a new (non-Singleton) Profiling instance for the sake of test isolation, you'll need to implement that separately. However, if your Profiling class is lightweight enough not to pose a huge burden, then you might still choose this way forward.

The cost of decoupling is that it can force every object to become an injectable, as opposed to a newable. You would then be able to substitute new/dummy/fake implementations of Profiling in your classes and tests, and reconfigure to use different Profiling behavior in different containers, though that flexibility may not have immediate benefits if you don't have a reason to subsitute those implementations. For classes like User created later, you would need to pursue factory implementations, such as those provided through Guice assisted injection or AutoFactory code generation. (Remember that you can create an arbitrary number of objects by injecting Provider<T> instead of T for any object you would otherwise inject, and that injecting a Factory instance would be like customizing a Provider to take get parameters you choose.)

Regarding your solutions:

  • Solutions 1 and 2, per-object injection: This is where a Factory would shine. (I'd favor constructor injection, given the choice, so I'd go with solution 1 between them.) Of course, everything that creates a new User would need to instead inject a User.Factory, so that may turn a tightly-scoped project into a project to convert every class in your codebase to DI—which may be an unacceptable cost for what you're trying to do now.

    // Nested interface for your Factory:
    public interface Factory { User get(String username); }
    
    // Mark fields that the user provides:
    @Inject public User(Profiling profiling, @Assisted String username) { ... }
    
    // Wire up your Factory in a Module's configure:
    install(new FactoryModuleBuilder().implement(User.Factory.class));
    
    // Now you can create new Users on the fly:
    @Inject User.Factory userFactory;
    User myNewUser = userFactory.get("timothy");
    
  • Solution 3, requesting static injection of a main holder approximates what I had in mind: For the objects not created through dependency injection, request static injection for a single class, like ProfilingHolder or somesuch. You could even give it no-op behavior for flexibility's sake:

    public class ProfilingHolder {
      // Populate with requestStaticInjection(ProfilingHolder.class).
      @Inject static Profiling profilingInstance;
      private ProfilingHolder() { /* static access only */ }
      public static Profiling getInstance() {
        if (profilingInstance == null) {
          // Run without profiling in isolation and tests.
          return new NoOpProfilingInstance();
        }
        return profilingInstance;
      }
    }
    

    Of course, if you're relying on calls to VM singletons, you're really embracing a normal VM-global static singleton pattern, just with interplay to use Guice where possible. You could easily turn this pattern around and have a Guice module bind(Profiling.class).toInstance(Profiling.INSTANCE); and get the same effect (assuming Profiling can be instantiated without Guice).

  • Solution 4, requestStaticInjection for every single class is the only one I wouldn't consider. The list of classes is too long, and the likelihood that they'll vary Profiling is too slim. You'd turn a Module into a high-maintenance-cost grocery list rather than any sort of valuable configuration, and you'd force yourself into breaking encapsulation or using Guice for testing.

So, in summary, I'd choose Guice singleton injection for your current injectable objects, normal singleton for your current newable objects, and the option of migrating to Factories if/when any of your newables make the leap to injectables.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.