samber / do

⚙️ A dependency injection toolkit based on Go 1.18+ Generics.
https://pkg.go.dev/github.com/samber/do
MIT License
1.71k stars 67 forks source link

Service register best practices #78

Closed iyaozhen closed 2 months ago

iyaozhen commented 2 months ago

I have been using do for a while. But I have a confused about service register

a typical service:

type Service struct {
    meegoMaterial *material.MeegoMaterial[material.MeegoMeta] `do:""`
    docMaterial   *material.DocMaterial[material.DocMeta]     `do:""`
    anyMaterial   *material.AnyMaterial[any]                  `do:""`
}

func NewService() (*Service, error) {
    return di.InvokeStruct[Service]()
}

And I add service.Register() function in main.go.

func Register() {
    di.Provide(material.NewMeegoMaterial)
    di.Provide(material.NewAnyMaterial)
    di.Provide(material.NewDocMaterial)

        di.Provide(report.NewService)
}

It works, But when I testing report:

func init() {
    service.Register()
}

func TestService_Report(t *testing.T) {

}

Circular dependencies error:

# ***/biz/service/report
package ***/biz/service/report
    imports ***/biz/service
    imports ***/biz/service/report: import cycle not allowed in test

I register service in test init func without report now.

func init() {
    di.Provide(material.NewMeegoMaterial)
    di.Provide(material.NewDocMaterial)
    di.Provide(objectstorage.NewService)
    di.Provide(material.NewAnyMaterial)
}

But I think it is ugly, Is there a better way? all in one service.Register() maybe no need to exist?

Of course, this is not do is problem, It should be my problem with using and understanding.

samber commented 2 months ago

Yes, declaring services globally might create import cycles sometimes.

In v2-beta6, we added support for "do.Package" -> https://do.samber.dev/docs/getting-started#register-services-using-package-declaration

You can create a do.Package in ***/biz/service/ for a subset of your app.

go get -u github.com/samber/do/v2@v2.0.0-beta.7

If you currently use v1, check the migration guide: https://do.samber.dev/docs/upgrading/from-v1-x-to-v2

I currently use v2 in my own project. You can consider it almost production-ready. Some APIs might change before release.

iyaozhen commented 2 months ago

Thx, I try to do.Package. I used v2 in production for some time, it is very good.

iyaozhen commented 2 months ago

Hello samber, do.Package doesn't seem to solve my problem.

Maybe I didn't describe it in detail enough. I create a demo: https://github.com/iyaozhen/samber-do-learn/blob/main/service/car/car_test.go

image

import cycle:

package github.com/iyaozhen/samber-do-learn/service/car
    imports github.com/iyaozhen/samber-do-learn/service
    imports github.com/iyaozhen/samber-do-learn/service/car: import cycle not allowed in test

TestCar_Start_V2 It is work, but I will add lots of do.Provide.

service/register.go, Should it not exist?

I should use do.Package in car? But engine used in a other car2 service, car and car2 Lazy/Provide Duplicate?

samber commented 2 months ago

Ok, i see your issue.

Maybe you could move your tests to a dedicated package? 🤔

I don't know the complexity of your app, but you can also have a do.Package in 2 sub-packages (here: service/engine/ and service/car/) and import them in tests: do.New(engine.Pkg, car.Pkg).

I don't know if this issue is related to DI, since you would have the same problem in traditional programming.

Did you try with interfaces? Using do.InvokeAs might prevent some troubles.

iyaozhen commented 2 months ago

Thank you for your patient answer. There's no particular solution for me.

move your tests to a dedicated package

not golang customary norm

2 sub-packages

car.Package

do.Package(
    do.Lazy(engine.NewEngine),
    do.Lazy(NewCar),
)

airplane.Package

do.Package(
    do.Lazy(engine.NewEngine),
    do.Lazy(NewAirplane),
)

https://github.com/iyaozhen/samber-do-learn/blob/main/service/car/car_test.go

panic: DI: service `*/service/engine.Engine` has already been declared

interface

interface is a good way. But it will make the project more complex. In general, there is only one implementation of an interface.

This is not do is problem, the same problem in traditional programming.

I'll close this issue first, If there is a good solution, I will come back to update.