2

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, 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.

  • 1
    Isn't the whole point of having a DI container to manage the instantiation of large object graphs like the one you are describing, so that you don't have to do it by hand? – Robert Harvey Mar 7 '18 at 18:08
  • @RobertHarvey I would think so, but to be honest I've never implemented a DI container before, which is why I'm asking if this is a good way to go about it (just read about it online earlier). I would just like to not have my classes have to inject like 30 different arguments if I can just have them all located in one object and inject that instead. But at the same time want to make sure I'm not making my app un-testable later because of the shortcut. – Lansana Mar 7 '18 at 18:22
  • You won't need your "ambient context" object if you use a DI container. – Robert Harvey Mar 7 '18 at 18:39
  • 1
    I am increasingly averse to "DI" as it is currently sold - it introduces all sorts of problems and actually opposes dependency inversion in many cases (e.g. where a component needs information from the request context you are forced to use clumsy factories or create leaky interfaces when constructor arguments would be more elegant). DI containers are a symptom, not a solution IMO. I increasingly prefer the idea of having the entry point (e.g. the controller) construct its own object graph at request time. This logic can be abstracted into a factory if and when it is needed elsewhere. – Ant P Mar 7 '18 at 18:46
  • 1
    @AntP: All a DI container does is work out your class's constructor dependencies and recursively constructs them for you. You can control object lifetime, when dependencies get constructed, etc. via configuration. What's not to like? – Robert Harvey Mar 7 '18 at 19:08
4

If you inject a "ServiceFactory", which contains all the dependencies for all parts of your code, into all parts of your code, then you are using dependency injection, but not dependency inversion.

Dependency inversion follows the rules of "tell, don't ask". It inverts the normal "I have a dependency, so I'll go looking for it" into "you have a dependency, here it is". So if you supply some large object that contains everything to a type, function or whatever, you are forcing it back into "I have a dependency, so I'll go looking for it in this big object".

And since the D of SOLID relates to dependency inversion, then yes you are violating it.

BUT having dozens of parameters for lots of functions will quickly lead to hard to read code. SOLID is a good set of principles, but readability is king. So if it simplifies your code to use such a collection object, then you should consider using it and SOLID be damned.

Having said all that, one of the keys to using IoC and having a container is that you hand responsibility for that plumbing of dependencies over to the container. It then removes the need for you to write lots of unreadable code and it respects the "tell, don't ask" principle by ensuring each function and type gets only what it needs. So it would be worth looking at IoC containers for go to see if one of those suits you better.

| improve this answer | |
  • Thanks a lot for this answer, it's very helpful. One thing I may have forgot to mention is that this "ServiceFactory" of mine will contain the service classes all already instantiated in the main app function, and the whole object with it's services will be injected into things that need the service factory. So I wouldn't be creating the services on the fly in this case, like some IoC implementations I read about in Java where you get a new object, like this: var presenter = IoC.Resolve<CustomerPresenter>();. In my case, it would be more like this: IoC.CustomerPresenter.DoSomething();. – Lansana Mar 7 '18 at 19:35
  • ...so do you think IoC would still be a good implementation for my use case, given the above requirement? That's just the idiomatic way to write Go code. I guess I could make it like IoC.NewCustomerPresenter()... but yeah. – Lansana Mar 7 '18 at 19:35
3

Although you can still mock your RepositoryFactory for tests, it makes it hard to have more than a single dependency per type in actual code.

For example, say that I have a large codebase with may services, each has its dependencies as part of the RepositoryFactory.

But also consider, I have two UserServices one uses a DB as in your code, but the other uses a repo which connects to a rest service.

Now I need to make two RepositoryFactories.

At the moment you have hardcoded the UserRepository type which is returned, (breaking the Dependency Inversion Principle) so I actually, I will need to make an IRepositoryFactory interface, a IUserRepository interface and two concrete RepositoryFactorys,

One of which probably doesn't implement all the other repository types. breaking the LSP

Also the IRepositoryFactory with its large collection of various repositories which have no relation to each other will break the Interface Segregation Principle.

Your RepositoryFactory might work on a small scale, but the more complex your needs become the more it will start to look like a DI Container.

Now, you can take the view that 'whatever you do is ok as long as it works for you!'. But people invented the principles to sum up hard won experience. You might not see the need for it and maybe you will invent a new and better way by going off piste.

But its not something to be recommended until its been proved by hard use.

| improve this answer | |
  • Could the "duplicate" RepositoryFactory be an example of a Facade Service, in that there are different variations of the same factory or whatever? blog.ploeh.dk/2010/02/02/RefactoringtoAggregateServices – Lansana Mar 7 '18 at 20:03
  • hmm no, i dont think so, the facade wraps groups of things and hides them. In this case they are all on a single level and all exposed – Ewan Mar 7 '18 at 20:08

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.