0
\$\begingroup\$

I have implemented clean architecture for my app, and I have a few questions.

Typically, pure DI is argued for over a Service Locator pattern, because it is very explicit and more testable.

However, I like the idea of having an object that contains all of my services or all of my repositories (a DI container), and I can just inject that one single object instead of injecting the various different services/repositories for ease of development.

But I'm not sure if this still follows SOLID principles.

For example, here is a typical DI:

// Database

type Database interface {
    Insert(...)
    Delete(...)
}

// User object

type User struct {}

// User Service

type UserService interface {
    Create(user User)
}

func NewUserService(userRepo UserRepository) UserService {
    return &userService{userRepo: userRepo}
}

type userService struct {
    userRepo UserRepository
}

func (s *userService) Create(user User) {
    s.userRepo.Create(user)
}

// User Repository

type UserRepository interface {
    Create(user User)
}

func NewUserRepository(db Database) UserRepository {
    return &userRepository{db: db}
}

type userRepository struct {
    db Database
}

func (r *userRepository) Create(user User) {
    r.db.Insert(user)
}

main() {
    // Create db
    db := database.New(...)

    // Create repo by injecting db
    userRepo := NewUserRepository(db)

    // Create service by injecting repo
    userService := NewUserService(userRepo)
}

As you see in the example above, that is following a pure DI approach.

The problem I have with this is the arguments can get to over 30 things being injected in a larger application.

So I came up with a different pattern which is to basically inject a single ServiceFactory or RepositoryFactory, and those will contain pointers to all of the other more granular service/repository classes.

So the new code can be changed to something like this:

// User Service

type UserService interface {
    Create(user User)
}

func NewUserService(repos RepositoryFactory) UserService {
    return &userService{repos: repos}
}

type userService struct {
    repos RepositoryFactory
}

func (s *userService) Create(user User) {
    s.repos.UserRepository.Create(user)
}

// Repository factory

type RepositoryFactory struct {
    UserRepository UserRepository
}

func NewRepositoryFactory(db Database) RepositoryFactory {
    return RepositoryFactory{
        UserRepository: NewUserRepository(db),
    }
}

main() {
    // Create db
    db := database.New(...)

    // Create repo factory
    repos := NewRepositoryFactory(db)

    // Create services by injecting repo factory
    userService := NewUserService(repos)
    fooService := NewFooService(repos)
    barService := NewBarService(repos)
    bazService := NewBazService(repos)
}

Does this approach still confirm to SOLID principles? It makes development much quicker and still uses DI, so I don't see why it couldn't still be tested because I could just mock the repository factory class in the same way I'd mock the individual repository classes...

Would you recommend this approach, why/why not? Thanks.

\$\endgroup\$
  • \$\begingroup\$ Hmm, what you wrote to me doesn't look like go at all. That's the real problem. You're using terminology and patterns that are used in traditional OO languages, but golang doesn't really do traditional inheritance. It's more about ducktype interfacing and composition. Just KISS (Keep it simple). Create a struct for the service, assign the DB to a field in stat struct, and use it where you need to. Once you start doing some stuff with that service, you'll automatically see what should be abstracted/moved to a separate package and what shouldn't \$\endgroup\$ – Elias Van Ootegem Apr 2 '18 at 13:43
  • \$\begingroup\$ @EliasVanOotegem Thanks for the response. Have you read this or this? What are your thoughts on it? I've modified my structure to be more in line with those two articles. That is more or less what I was trying to achieve here, though the examples in the article are much better than my first jab at it here. \$\endgroup\$ – Lansana Apr 2 '18 at 13:47
  • \$\begingroup\$ I've seen the second one before. There's a lot in there that I actually find myself disagreeing with, and that's after having tried a couple of things he suggests. They key thing is that different languages require different approaches. That, and I can't get past the fact that the code in the domain.go file (second article) isn't even thread-safe. Just makes it stupendously hard for me to take it as serious as perhaps I should. Then again: I've been writing almost nothing but go for over 1.5 years professionally... should be worth something, too \$\endgroup\$ – Elias Van Ootegem Apr 2 '18 at 14:02
  • \$\begingroup\$ Mind if I ask what kind of architecture you use for your web services that need to connect to external databases, API's, etc.? P.S. I am going more with the approach in the first article now. It allows me to set up an internal API in the domain layer that depends on nothing else, and I can use the duck typing to conform to that API in my mysql layer, HTTP layer, etc. Then I use dependency injection to add everything together in main.go. \$\endgroup\$ – Lansana Apr 2 '18 at 14:12
  • \$\begingroup\$ In terms of architecture, I'm not sure what you mean, but it's a pretty standard microservices deal, containerised on AWS. Connecting to DB's is done by the service through a database package, that takes its config from environment vars (parsed by a config package, passed through via main package). API's are imported packages from other services (gRPC style), or custom built packages depending on what you're dealing with. Could be third party package if it exists... Most packages are self-contained, so the main doesn't really inject anything other than context + config values \$\endgroup\$ – Elias Van Ootegem Apr 2 '18 at 15:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.