←back to thread

257 points pmig | 1 comments | | HN request time: 0.215s | source
Show context
epolanski ◴[] No.43096608[source]
Very ignorant about Go, is dependency injection not a thing there?

E.g. I like declaring interfaces in other languages that model some service (e.g. the database), focus on the API, and then be able to provide different implementations that may use completely different technologies behind them.

I know that some people don't see the point, but I'll make an example. At my primary client the database layer is abstracted and provided at runtime (or compile time, depends) with the repository pattern. How's that useful? Developers can use a filesystem-based database when working, and don't need to setup docker images that provide databases, anything they do is saved on their machine's filesystem. E2Es can run against an in-memory database, which makes it very simple to parallelise tons of different tests as there are no race conditions. And yes, the saved formats are the same and can be imported in the other databases, which is very useful for debugging (you can dump the prod database data you need and run it against your implementation).

There's many other situations where this is useful, one of the core products I work on interfaces with different printing systems. Developers don't have printers at home and my clients have different printing networks. Again, abstracting all of it through DI makes for a very sane and scalable experience.

I don't see how can this be achieved as easily without DI. It's a very nice tool for many use cases.

replies(6): >>43096619 #>>43096645 #>>43096738 #>>43096799 #>>43096998 #>>43097708 #
dgunay ◴[] No.43096738[source]
DI in its most fundamental form is common in Go via interfaces. People just declare that arguments to their functions must implement some interface, and the caller provides something that satisfies it.

"DI" in the sense of having a more complex purpose-built DI container is less common. They exist, but I don't see them used a lot.

If your application code has a ton of dependencies, the direct injection/low magic style favored by Go can get kind of boilerplate-y. I'm not going to comment on which style is better.

replies(1): >>43101437 #
CharlieDigital ◴[] No.43101437[source]
.NET's DI has constructor injection so I suppose this is similar; you just write your constructor saying "I need an `IDataProvider`" and it can come from DI or direct.

Question I have for Go though. Without DI, what is the idiomatic way of supplying one of n implementations? An if-else block? Like if I had 5 possible implementations (AzureBlobStore, AwsBlobStore, GcpBlobStore, CFBlobStore, FSBlobStore) and I need to supply 10 dependent services with access to `IBlobStore` what would that code look like?

In .NET, it would look like:

    // Resolve the concreteImpl in DI
    IBlobStore concreteImpl = switch (config.BlobStoreType) {
        case "Azure": new AzureBlobStore();
        case "Aws": new AwsBlobStore();
        default: new FSBlobStore();
    };

    // Register
    services.AddSingleton<IBlobStore>(concreteImpl)
    services.AddScoped<Svc1>();
    services.AddScoped<Svc2>();

    // Services just use primary constructor and receive impl
    public class Svc1(IBlobStore store) { }
    public class Svc2(IBlobStore store) { }

    // But I can also manually supply (e.g. unit testing)
    var mockBlobStore = new MockBlobStore();
    var svc2 = new Svc2(mockBlobStore);
replies(1): >>43109679 #
1. dgunay ◴[] No.43109679[source]
Yeah it basically is just an if-else or switch block.

    type BlobStore interface { /* methods */ }

    // later, when initializing stuff
    var foo BlobStore
    switch {
        case isAzure:
            foo = AzureBlobStore{}
        default:
            foo = FSBlobStore{}
    }
    
    svc1 := NewSvc1(foo)

    // Constructors are just functions and are totally 
    // convention/arbitrary. If the fields are public you 
    // can instantiate the struct directly.
    svc2 := Svc2{ BlobStore: foo }