18

Instead of using singletons, I made one class to hold an instance for every component. Let's call it MainApp. MainApp is initialized in the entry point of the program and determines the lifetime of all components and resources. It has a single responsibility and that is being a collection of components. That's why it didn't feel like a bad idea first. However, some components need access to other components, there is one component that is needed for almost all other components. To solve this I used dependency injection, but this makes these components dependent on MainApp. It feels like MainApp just became a big mediator class for all components.

Back to my question: Is this kind of class bad design? What would be better alternatives? Or isn't this class necessarily bad design, is the problem more related to the fact that a lot depends on it?

10
  • 4
    There is not necessarily anything untoward about having an overall "application" object. It's used to great effect in the Microsoft Office suite for example.
    – Steve
    Apr 7, 2020 at 13:08
  • 4
    I made one class to hold an instance for every component - kinda like an IoC container, then? Something has to know about all the classes in your program and tell them about each other! The thing is, that you sound like you've then hooked your DI up wrong; a class that has a constructor that takes what the class needs doesn't (shouldn't) depend on the IoC container- it should depend on something else and the IOC container should give it that thing, not give it itself. Kinda like, if grandad wants an apple, give him an apple he likes rather than the whole fruit bowl so he can take his own
    – Caius Jard
    Apr 7, 2020 at 14:17
  • 1
    (Because then you'll get into a protracted discussion about all the different kinds of apples, and oranges, and quinces therein and whether he knows how to eat them, or like them etc)
    – Caius Jard
    Apr 7, 2020 at 14:18
  • Never heard of an IoC container, but it sounds about the same thing I want to achieve. Nice analogy with the fruit ;)
    – TheNappap
    Apr 7, 2020 at 14:43
  • 2
    @TheNappap that is a good analogy. It's not that you can't hand grandad the whole fruit bowl. It's that we know the problems that causes. These two different patterns have shown up enough we've given them names: Service locator is the bowl. Dependency Injection is the apple. They solve the same problem but are different Apr 7, 2020 at 18:02

4 Answers 4

31

This MainApp class is not per se a bad idea, but probably reinventing the wheel.

Generally, you need one "central" component to orchestrate the whole idea of dependency injection. Also, you probably are better off with using a readymade solution (e.g. in Java a Weld container.)

However, the services should not depend on MainApp, but on the other services they need. Let's say, you have a User service which needs a Repository service. Your question sounds like you do this right now:

// BAD
class UserService {
    private MainApp mainApp;

    UserService(MainApp mainApp) {
        this.mainApp = mainApp;
    }

    void doSomethingThatNeedsARepository() {
        this.mainApp.getRepositoryService().doStuff();
    }
}

// in MainApp
UserService userService = new UserService(this);

If you do this, you lose control over which service needs which other service, resulting in a big mess.

Only inject what you actually need:

// Better
class UserService {
    private RepositoryService repository;

    UserService(RepositoryService repository) {
        this.repository = repository;
    }

    void doSomethingThatNeedsARepository() {
        this.repository.doStuff();
    }
}

// in MainApp
RepositoryService repository = new RepositoryService(...);
UserService userService = new UserService(repository);

(We might go on discussing the merits of using an interface instead of a concrete service, but somebody else will probably do this ;-))

2
  • 1
    Yep, that's basically the Service Locator anti-pattern in the first snippet. A glorified bunch of global variables stitched together into one. For greater horror, make it static class and give it getWhatever<T>() method: voila, now you have an unbounded amount of global variables and you don't even know which of them exist or not exist. Very handy for breaking borders between isolated parts of your application and turning it into a BBOM.
    – Joker_vD
    Apr 9, 2020 at 0:02
  • Not to mention the gazillions of lines of Powermockito-code to actually unit-test an isolated little piece of code...
    – mtj
    Apr 9, 2020 at 11:36
10

However, some components need access to other components, there is one component that is needed for almost all other components. To solve this I used dependency injection, but this makes these components dependent on MainApp. It feels like MainApp just became a big mediator class for all components.

Using MainApp as the Composition Root for Dependency Injection does not make anything else dependant on it unless you're doing something silly like letting the other components know that MainApp exists.

MainApp's job is to construct and inject components. It is not something that components should talk to or ask questions of. Stick to that and your design is fine.

Doing this doesn't make MainApp "represent" your entire program. It's simply how you build its persistent object graph. Once that's done MainApp can call start() on some component and step aside, never to be heard from again.

8
  • > unless you're doing something silly like letting the other components know that MainApp exists. That's exactly what I'm doing... -.-'
    – TheNappap
    Apr 7, 2020 at 13:25
  • MainApp is suppose to start everything and close everything on the end of the program. So for me, it represents the entire program itself. ;)
    – TheNappap
    Apr 7, 2020 at 13:28
  • For me that’s like thinking the scaffolding represents the building. Apr 7, 2020 at 16:07
  • I think the building is not a good analogy here, because a building doesn't really start and stop like a program. I look at it this way: my program is like a (car)engine, you can start and stop it, but you have no idea what goes on on the inside. In the same way MainApp starts and stops the program, but the user doesn't know anything about the implementation.
    – TheNappap
    Apr 8, 2020 at 7:19
  • 3
    @TheNappap But you're not thinking components there. If you take the engine analogy, candied_orange is talking about what the parts of the engine need to know. Think about a water pump. All it needs to do is pump water. If you bolt extra parts on the water pump to tell it what make of engine it's in, it doesn't help it pump water, and now you've added extra crap to keep in step with the rest of the engine if anything changes, for no good reason. Let the water pump do the one thing it does, and not care about anything else. Then the engine infrastructure takes the water to the right place.
    – Graham
    Apr 8, 2020 at 18:58
5

MainApp is fine. It is a composition root. It should act as glue, injecting dependencies, linking producers and listeners, that kind of stuff.

About the dependency injection, avoid injecting MainApp itself (unless some limitation of the platform forces you). Instead, you can have MainApp inject other things, including factories. If you need to create new types just to inject them, great, do that.

Nothing else should have knowledge of MainApp. A change in MainApp should not imply a change of the components.

0
2

TL;DR; Injecting your MainApp class loosly couples it to everything it's injected into. Your MainApp class will become the place to solve all sorts of problems because, as you say yourself, it is now a mediator for everything it's injected into. Just inject what is needed for each class, nothing more.

Longer explanation:

It looks to me that you've created an Inversion of Control container or IoC for short. This actually underpins the dependency inversion principle as determined by SOLID architecture. The idea is that a central repository understands how various parts of an application can be constructed and will either hold a reference to an instantiated instance or at least have the ability to construct a new instance with minimal configuration.

Many languages that make use of reflection can actually take this a step further by statically analysing the various dependencies a class needs and then recursively constructing those dependencies based on its knowledge of how they are also constructed. This can be incredibly powerful when building an application as all you need to do is register a class with the IoC and instantly allow the automatic construction of some quite complex dependency trees.

You don't appear to be to far away from this kind of design right now, but you mustn't inject your MainApp class into other classes. This does create dependencies on your registry when you really want to ensure it remains decoupled. Instead you should be looking to construct your classes using the instances of classes it actually needs, not by passing in the registry and letting your classes pull out what they want.

Depending on the language, there are already some excellent IoC implentations available for you to draw inspiration from:

And so on.

Not the answer you're looking for? Browse other questions tagged or ask your own question.