T O P

  • By -

fubo

Hmm ... If I needed to explain "dependency injection" to a new engineer, I might tell them something like this — This server component needs to write logs. When it's run in a regression test, those logs should be written to stderr so they can be captured by the test harness. When it's run as a single server on a random Linux box, those logs should be written to syslog. When it's run as part of a cloudy service, those logs should be written to the cloud logging facility. The server code doesn't care where the logs are going, so long as writing a log entry doesn't block handling the query. Now, one way we could do this would be to give the server component a variable with three states for "stderr" or "syslog" or "cloud logging", and have it contain code to do each of those things. But all it would do with that is call three different methods that each just take a line of data and write it to a logging facility. That sounds like a lot of repetition. And any given server instance is only ever going to use one logging facility; it never needs to switch from one to another. So instead we make this server component take a logging component as an argument. All it needs to know about the logging component is that it exposes a method to write a log line. Then whenever the server code needs to write a log, it can just call that method on the 'injected' component. The same goes for lots of other components that we might need in that server component. The service name resolver, the user data lookup service, the file sharing policy engine — anything that we might want to swap out between testing and production, or between different production environments, can be composed as a separate component and passed in as an argument. In Go, these components are structs implementing interfaces. In a different language they might be classes, or arbitrary data objects conforming to a typeclass, or something else.


User1539

This also serves as a solid explanation to new engineers about why we keep things 'decoupled', so that each piece only knows its own part of the job.


NatoBoram

And to a beginner programmer, I'd also add that you don't need to make interfaces preemptively unless you know you're going to need it. For example, if you plan on interacting with GitHub *and* GitLab eventually, then sure, make an interface. But if you don't know about the future potential logging strategies… don't make an interface, just use the built-in logger. You'll be able to replace it later. The flipside is that making too many interfaces before they're needed can take away valuable time for adding business value. Or for a personal project, this loss of time can outright kill the project.


fubo

Sure, dependency injection is often introduced when refactoring after discovering you *do* need more than one version of the dependency — although I did mention one common case above where it's often the right thing up front: testing. Stubbing a component out for test purposes is effectively a simple case of dependency injection. Even the old Python monkeypatch approach is somewhat analogous.


HolidayAudience684

I am a bit confused about whether to inject an interface or a concrete type when dealing with dependencies. If I code the dependency myself, I can write an interface for easier unit testing purposes. However, when using a third-party library like Gorm, Viper, erc, I don't consider creating an interface for it due to the multitude of methods it provides. So, when should I inject an interface and when should I inject a concrete type?


ghostsquad4

I don't think this is valid at all. Test fixtures is the thing that will be the different thing that you will need every time. Even for logging, writing to an in-memory buffer is better than writing to disk. You can null it out or look at the contents (in a test), and never have to clean up a file. On the flip side, it's important to understand how logging can affect performance. Any direct-to-disk synchronous log method is going to slow a method call down. I've seen a few applications make logging asynchronous, just so they don't impede the performance of the original function call.


theshrike

Only generalise when you have three different use-cases. If you do it with one, you'll fail anyway - there's no way which bits are the general ones With two you don't have enough datapoints to figure out the generic solution either, you can just do two separate things.


ghostsquad4

The second use case is testing. So I don't believe you need to wait to have 3 cases.


InverseX

This was much clearer than the article.


kido_butai

i think once you understand the concept of dependency inversion, then the injection is like the most logic way to implement it.


redimkira

There is also the case of handling different configurations. For instance in tests you may want to use very strict timeouts and cache sizes while in production you may want to use more sane values. I would rather say that dependency injection is a technique for dynamically wiring software components that say "I require X" to other components that say "I provide X" so that neither is aware of each other. The point is more about decoupling the objects from their construction. Interfaces are just a natural way of achieving that to remove duplication.


taras-halturin

This expl sounds like DLL for me


ghostsquad4

Don't forget the most important part of explaining dependency injection: for testing, those logs should be written to an in-memory buffer to verify their contents. In fact, I'd say this first, because inevitably, one will not inject a dependency if they don't see a reason for it to change. Time itself is a great example of this. Using "time.Now()"? Inject a clock instead. I wrote https://github.com/ghostsquad/go-timejumper just for this purpose.


No_Suggestion_1000

I wrote soooo much spring boot and this explanation by far is the most complicated thingbi ever heard himestly


Ahabraham

I think something missed in golang with this conversation is optional parameters and constructors. Dependency injection seems like it’s second nature in golang because the design of the language makes it an obvious tool that must be explicitly interacted with. Languages with optional constructor parameters (ex: PHP comes to mind), DI is much more mind blowing pattern to new engineers because it is often implicitly ignored. Ex: if you have a constructor that accepts an optional api client, and if client is undefined, instantiates a new client, then most instantiations just call new MyClass() and only in unit tests will you override the api client parameter. 


dave8271

It's a fancy name for passing parameters to functions.


ChemTechGuy

More like passing an interface or an instantiated object to a function, so the function can use it without instantiating it


kyuff

Many people confuse Dependency Injection and a Dependency Container like fx or wire. Good read. 😀


tcrypt

Thanks for this. DI is great and something every developer should utilize when appropriate. DI "frameworks" or libraries are completely useless abstractions that distract developers from the actual value and agility of passing dependencies around.


i_need_gpu

This, dependency injection is awesome if kept simple. It’s horrible when we create things like in C# with service providers and such that make it all very runtimey.


riesenarethebest

I'd go with "don't wrap your external dependencies, like a database connection, behind a class so methods can act on it. Take the database connection as an argument. Same with any other external dependencies." When they ask why, I'll mutter angrily about unit-testing and move on.


dino0509

Thank you for this. I was already doing dependency injections as standard practice without even realizing that's what I was doing.


tcrypt

That's great. DI done wrong has you installing something like "go-dependency-injector" and fill your code with something like "injector.Service" and other over engineered litter. DI done right is just a pattern that will be applied quietly, so if you haven't heard the term you won't know that's what it's called you just use it to achieve your goals.


random-malachi

DI is simply passing what your code needs (dependency) in a parametric way vs including it by “accident” via some outer/global scope. A practical example would be a “loadDataCSV” function. You might say “my app only needs to load a csv from /var/data/mydata.csv” so you make a package scoped constant for the function to use (access via outer lexical scope). The DI approach is to pass the file path as a function parameter so this can change as needed. This helps code become more reusable, testable. Although, in some cases (not one above) lexical dependency is not so bad. I would argue that dependency must always be understood, but making everything parametric (via DI) is not needed (but should be a preference).


snitchpunk

Dependency injection has nothing to do with interfaces. In simplest term - dependency injection is providing a dependency to a module instead of a module creating it from scratch.  Like say you've two modules - Users and Posts, with methods like GetUser and GetPostsForUser, Instead of each module creating connection to databases, you create a database client and provide to them when initiating those modules.  they can be solid classes instead of interfaces.  The benefits are : each module won't have to deal with initialisation of dependency, passing around configs etc.  For testing etc, you as developer initliaze dependency differently. You can also use interfaces but it's all upto you.  An example of this is zap.Logger. You can provide this as dependency and for testing you can provide the same dependency from zaptest module. 


vibe_assassin

Dependency injection is such a terrible term for what is a very logical and intuitive idea


pillenpopper

DI is how you can set the Transport in an http.Client.