..., what is the purpose of IoC container?
The purpose of an IoC container is technically unrelated to the existence of interfaces. You could have a functional though barebones IoC container that only uses concrete types, and thus relies on inheritance for its type variation. It's not great, but it can get the basic job done.
If it is a bad practice to use an interface if only one class will implement it, ...
I have been searching for the answer if do we need to use an interface if only one class will implement it, majority, if not all of the answers are "No, you should not!".
The interface advice you've been given is... imperfect.
It is factually correct that if there is definitively never ore than one implementation, an interface isn't being leveraged for its full potential.
However, there are some major caveats to claiming that you know that there is definitively no other possible implementation for this interface. There are so many caveats, that in my experience as a developer I now default to disbelieving any claim by any developer that things will definitely never change. Almost always, such a claim is a matter of developer naiveté/arrogance/obliviousness/oversimplification.
- Well-developed test suites tend to inherently fill that need by creating a mocked version of the interface.
With proper code coverage, pretty much all components in your software will be tested, and therefore its dependencies will be mocked. If your interface is being used as a dependency by a class, then that class' unit tests will be mocking your interface, which means that you now have a second implementation of that interface (the real implementation + the mocked one).
- You cannot tell the future. Requirements change, or may have been misinterpreted, or your codebase may become ripe for an upgrade due to its popularity and longevity.
This is in bold because it is by far the biggest flaw I see developers make. You assume that what is true today will be true for eternity, which is simply not the case. Either because you might be wrong today, or because tomorrow will be different from today.
While there may be only one implementation today, nothing is stopping the future from coming with new requirements that add additional variations into the mix.
For example, my software might use DateTime.Now
for all its clock-based needs today. But maybe tomorrow there's a better clock-based library, and I'd want to use that. If I abstract DateTime.Now
into an IClock
based library today, even though it's the only implementation I currently think I'll use, I will reap the benefits tomorrow when I can simply write a new IClock
implementation for the better library.
Without that IClock
interface, I'm stuck having to manually rewrite every reference or dependency on DateTime.Now
in my codebase. At best, it's an annoying menial task. At worst, if I forget a few, that's going to be the source of some majorly annoying bugs.
With that IClock
interface, I simply have to write a new class which implements that IClock
interface using the fancy new library I wrote, and I have to change 1 line in my IOC container registration where it says <IClock,DateTimeClock>
to <IClock,NewLibraryClock>
.
Note also that this work did not require me to figure out what my old DateTime
-related code did, how it worked, nor did I need to change any of that code and risk breaking it. I was able to simply write a new thing and plug it in. No fuss, no muss.
For example, IEmail will only be injected with Gmail class always. But when you add Outlook class, Yahoo class, etc., correct me if I'm wrong, but I don't think IoC container will handle it easily.
You're conflating two different uses for interfaces.
For the IoC example, we're talking about how dependencies can change over time. For example, your application uses Gmail servers to send out its notification today, but tomorrow Microsoft drops their prices and you want to start sending out your email notifications using Outlook servers. This is the kind of scenario where you'd be changing your IoC registration from <IEmailSender, GmailSender>
to <IEmailSender, OutlookSender>
.
But what you're talking about here is runtime variability, i.e. you want your application to use Gmail
or Outlook
dynamically, based on e.g. the user's email address. You're right that no IoC container will handle this kind of choose-your-email-implementation easily, because this is not the kind of scenario that an IoC container is designed for.
IoC containers work with global registrations, i.e. one definitive concrete type assigned to a more abstracted type (interface). When you have multiple concrete types that you want to use, you'll find that your IoC container is not great at choosing between different concrete types based on some arbitrary logic. It's simply not what an IoC container was designed to do.
This second scenario is much more suited to a factory which decides which IEmail
implementation to use. This requires some business logic, which is something that your IoC container can provide out of the box.
When you get to this stage, you create the factory, and it is the factory which gets registered to the IoC container. This is a very different scenario from the one your question is focusing on.
Note: there are ways to force this factory logic into an IoC container directly, but for any non-trivial example it's in my opinion an allround bad idea to do so and much better implemented using an explicitly designed factory class.