What good are a bunch of car parts sitting on a race track doing anyone?
If all your car class does is hold car parts it's as useful as a wet bag of parts.
As the driver, what I want is something I can control. That responds when I ask for speed. That handles like its on rails. That can stop on a dime.
What I want is a car class that is usable. That I can tell to do things without having to think about how the carburetor works. I just think about the gas pedal. How those are connected isn't something I worry about. That's abstraction.
Dependency injection has nothing to do with that. When I'm driving the car I'm not thinking about how it was built. As long as it works, I don't care how they put it together.
No, DI is what lets my pit crew quickly swap out my tires for better ones when it starts raining on the track. It's nice to be able to do that without having to hop into a completely different car.
DI is really about following a principle: Separate use from construction.
A car that can install new tires at 90 miles an hour might sound cool but I don't think it's going to win any races with a tire installing contraption on it.
DI is about installing your parts in a way that let's your pit crew get to them. When you use
new in the same place you program behavior it's like you're welding the carburetor into place. Sure an acetylene torch could remove it but please consider using nuts and bolts first.
That's what DI is. Sure it's not as easy as
newing up something as soon as you realize you want it. Instead you have to write separate code that knows how to build your car. But it sure makes it easier to change things later. And it means you don't have to drag a car assembly plant around the track with you.
Something, somewhere, has to know if the tires are Goodyear. Where then to put the car construction code? If not the car then the pit crew? No. The track? No. All of those have behavior code. Code that must perform during the race. Constructing the car should happen before the race in a place removed from behavior code. Mark Seemans called this place the Composition Root. Most people call it main.
It's a simple pattern. In main, construct the object graph then call one behavioral method on one object in the object graph. That's it. This is the ONLY place construction and behavior have to be together.
That doesn't mean construction has to be a pile of procedural code all laid out sequentially in main. You are free to use every tool in the language to do construction. Just don't mix it with behavior.
Doing this in the language and not using some DI framework or IoC container is called pure DI. It works very well. Has for a long time. We used to just call it reference passing.
What a DI tool buys you is the construction details moved into a different language (xml, json, whatever) that enforces the separation between construction and behavior. If you don't trust your fellow programmers not to use
new when they shouldn't that can be appealing.
The draw back is that it's tempting to let the DI tool details spread throughout the code base. Sometimes infecting the code base with proprietary annotations. The examples they provide certainly encourage this. The tool tends to move into the language space until you can't just advertise the job as a Java programming job but as a Java/Spring programming job.
For ages I was programming Car classes because it was obvious for me that I need a Car class if I am programming a car logic. But with DI it is not so obvious to me. Still wondering is it an idiomatic for DI not to create a Car class if I have no defined role for it?
I think you're learning about abstraction and changing how you decide a class is needed. That's good. But that's not about DI. DI doesn't help you decide if you need a car class. DI helps you keep the car from knowing, and therefor caring, if the tires are Goodyear tires. DI helps you keep the track from knowing if the cars are made in Japan.
One of the most fundamental questions in software design is
"what knows about what?" That's the main thing a UML diagram shows you. When you new up something you're reaching past it's interface to the concrete thing that you're now tied to. The car now has to know that the tires are Goodyear. Which kinda sucks if Michelin want's to sponsor you.
Avoiding doing that is called following the dependency inversion principle. Formally, a high level module (like the car class) should not directly depend on low level modules (like the GoodyearTire class). It should depend on an abstraction (like a Tire interface).
A way to avoid doing that is called Inversion of control. Here the emphasis is on changing the flow of control. Do the tires move the car or does the car move the tires? Thinking about this the right way allows us to not statically couple the car and tires together. DI is one particular way to follow Inversion of Control.
None of this tells you if you need a car class. If you're programming "car logic" it's nice if there is one place to keep it rather then scattering it every where. Just don't be fooled into thinking car construction logic is the same as car behavior logic so it all has to live in the same place. But if you have no defined roll for a car then you don't need either. Race motorcycles around the track if you like.
I mean is it okay to have a SteeringWheel for driving, BoltsAndNuts on wheels for pit crew and all sorts of other funny interfaces without having an instance that represents a car as a whole?
DI or no DI, it's fine to have an instance that represents a car as a whole, but that instance isn't something I want to know about directly if I don't have to. Give me a car abstraction so I don't have to care if it runs on gas, diesel, or electricity when I use it. That's only something I should care about when I'm building it or maintaining it. It's nice if the code that uses car doesn't have to know or care how it works. "I don't know. I don't want to know."