0

In trying to separate interface from implementation, I ran into a circular dependency problem.


There is a best practice in the world of Java: consume interfaces instead of concrete classes. That is, instead of declaring variables of type ArrayList<int>, use the type List<int> instead. That way the migration from ArrayList to your own implementation of List will be painless.

I have decided to write an event queue/bus in golang for fun. Consumers are able to subscribe to certain topics and publish events within certain topics.

I have decided to split the interface and implementation in case I'll have to write adapters to already existing solutions, like RabbitMQ, in the future.

First I defined the interface:

package eventQueue

type EventQueue interface {
    Subscribe(topic string, handlerFunc HandlerFunc)
    Publish(topic string, event interface{})
}

type HandlerFunc func(event interface{})

And, within the internal package, I have written the basic implementation:

package internal

import (
    "eventQueue"
    "sync"
)

type EventQueueImpl struct {
    handlerFuncsByTopic map[string][]eventQueue.HandlerFunc
    mutex sync.RWMutex
}

// Implements the interface...

Back to the interface - I have written a New() method, which returns the default implementation (at the moment there is, of course, only one):

func New() EventQueue {
    return &internal.EventQueueImpl{} // EventQueueImpl uses pointer receivers
}

And there it is. EventQueueImpl imports HandlerFunc from eventQueue, and the eventQueue package imports EventQueueImpl.

How to change this design to circumvent circular dependency while preserving the interface-implementation separation?

Is dependency injection a fitting solution?

2 Answers 2

2

What is the reason behind putting the constructor in the interface and not in the implementation ?

Your constructor returns an implementation not the interface, and interfaces should never be instantiated, only their concrete implementation should be.

1

Comparing to Java, you should see that New() function as the constructor of your struct. And, like the constructor in Java, that function should be with the implementation, not with the interface.

Your New() function is implementation specific; if internal.EventQueueImpl needs some internal property that needs to be set or initialized before it can be used, New() is the place to set it - but some random other implementation of your EventQueue interface won't need those same properties, meaning New() is doing things to make internal.EventQueueImpl work, so it has no business being defined inside the eventQueue package.

Another sign that New() shouldn't be part of the eventQueue package is that, if you were to add a second implementation of eventQueue.EventQueue, what would you do? Create eventQueue.New2()? Change the signature of New() to accept some sort of toggle? If you do that, how will you handle the configuration of all your different implementations?

So, as f222 said, to fix this, all you need to do is move New() from eventQueue to internal and all will be good.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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