-
Notifications
You must be signed in to change notification settings - Fork 621
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Basic interfaces #387
base: master
Are you sure you want to change the base?
Basic interfaces #387
Conversation
Of note another project that wraps streadway/amqp has a pretty solid set of interfaces: https://github.com/NeowayLabs/wabbit/blob/master/amqp.go. Perhaps they could be of use. Generally ++ on prototyping this, it could be VERY useful. |
I like this. I think the biggest hurdle to adopting this is this client's philosophy of keeping the API backwards-compatible for consumers of @streadway @gerhard any thoughts on at least adopting interfaces? |
I think it's finally time to add some interfaces because it's a frequent enough request to reduce the typing needed to properly write testable producers/consumers. wabbit does a pretty good job of wrapping this library already but we could also do something similar. I can think of two ways of slicing the interfaces, one on the protocol message class, and another on the separation of concerns. I initially chose neither for this package to give the author that uses this package the freedom to shape their own integration. This PR slices the interfaces a little bit too coarse grained on both the message class and use case to hand write mocks for testings. Also, when interface types include |
Adding interfaces is a backwards compatible change, so #312 doesn't yet apply. |
@streadway Any news about this topic? Need any help? |
Without some more concrete examples, it's hard for me to measure whether these interfaces would be along the right boundaries. For example, putting all the notify methods in one interface doesn't seem like the right separation. Do you have some examples of how this change makes some of your application code simpler? "Accept interfaces, return structs" is something that I've found to work well in Go and this change moves away from that. Is this something that needs to be within the As for the interface separation, I think it'd make more sense to define smaller interfaces so applications can compose them into the types they need. For example:
In a
I think the right abstraction for interfaces would rather be the application specific adapter to the broker, rather than on the implementation of the adapter. Something like this:
Then I would exercise the adapter implementation through the normal integration/end-to-end tests, since that's where the most confidence would be gained because the adapter code depends on prior broker state to be correct. WDYT? |
I'd combine all topology management ( What is grouped into the Having interfaces mostly built around the protocol shouldn't make it significantly harder to build application-specific interfaces on top. So I'd go with something like type Closer interface {
Close() error
}
type Publisher interface {
Publish(exchange, key string, mandatory, immediate bool, msg amqp.Publishing) error
}
type Consumer interface {
Consume(queue, consumer string, autoAck, exclusive, noLocal, noWait bool, args amqp.Table) (<-chan amqp.Delivery, error)
Cancel(consumer string, noWait bool) error
}
type Acknowledger interface {
Ack(tag uint64, multiple bool) error
Nack(tag uint64, multiple bool, requeue bool) error
Qos(prefetchCount, prefetchSize int, global bool) error
}
type NotifyPublisher interface {
NotifyPublish(confirm chan amqp.Confirmation) chan amqp.Confirmation
}
type NotifyCanceller interface {
NotifyCancel(c chan string) chan string
}
type NotifyCloser interface {
NotifyClose(c chan *amqp.Error) chan *amqp.Error
}
type NotifyFlower interface {
NotifyFlow(c chan bool) chan bool
}
type NotifyReturner interface {
NotifyReturn(c chan amqp.Return) chan amqp.Return
}
type NotifyBlocker interface {
NotifyBlocked(receiver chan amqp.Blocking) chan amqp.Blocking
}
type PublishConfirmer interface {
Confirm(noWait bool) error
// PublishedDeliveryTag() uint64
}
type ExchangeDeclarer interface {
ExchangeDeclare(name, kind string, durable, autoDelete, internal, noWait bool, args amqp.Table) error
}
type ExchangeBinder interface {
ExchangeBind(destination, key, source string, noWait bool, args amqp.Table) error
}
type ExchangeDeleter interface {
ExchangeDelete(name string, ifUnused, noWait bool) error
}
type ExchangeUnbinder interface {
ExchangeUnbind(destination, key, source string, noWait bool, args amqp.Table) error
}
type QueueDeclarer interface {
QueueDeclare(name string, durable, autoDelete, exclusive, noWait bool, args amqp.Table) (amqp.Queue, error)
}
type QueueBinder interface {
QueueBind(name, key, exchange string, noWait bool, args amqp.Table) error
}
type QueueDeleter interface {
QueueDelete(name string, ifUnused, ifEmpty, noWait bool) (int, error)
}
type QueuePurger interface {
QueuePurge(name string, noWait bool) (int, error)
}
type QueueUnbinder interface {
QueueUnbind(name, key, exchange string, args amqp.Table) error
}
type Connection interface {
Closer
Channel() (*amqp.Channel, error)
}
type Transactions interface {
Tx() error
TxCommit() error
TxRollback() error
}
type PollingConsumer interface {
Get(queue string, autoAck bool) (msg amqp.Delivery, ok bool, err error)
}
type Channel interface {
Publisher
Consumer
Acknowledger
PublishConfirmer
ExchangeDeclarer
ExchangeBinder
QueueDeclarer
QueueBinder
PollingConsumer
Closer
} I can see how fine-grained interfaces around notifications would be useful, and possibly publishing/consumption but not most other channel operations. |
@krzysztof-gzocha should we update this PR to use the above interfaces? I don't mean to imply that my suggestion is superior but we have to settle on something. Interface design is a matter of opinion :) I'd like to keep the momentum in this PR while @streadway has some cycles to dedicate to this client ❤️ |
It's easier to add methods to interfaces, than it is to remove them. The complete method list as @michaelklishin proposes does not seem to fit the strengths of the Go type system where interface composition is simple. It doesn't make sense for me to share a type for distinctly different purposes like Publishing and Consuming, or even ExchangeDeclare as part of application lifecycle and ExchangeDelete as part of topology administration. This is where it'd be good to get an answer to the question, ideally with an example to "what is the purpose of these package interfaces". In #383 it's the smallest set of interfaces for mocking, and I don't see how a large Channel interface would help ease the effort in mocking compared to an application defined interface definition. |
#389 should be merged before this because it will add a new set of context based methods. |
I just find small interfaces like I would happily implement interfaces as suggested by @michaelklishin. About #389: yes, it would be helpful to first merge every PR that is changing methods definition. |
My concern is that every symbol exported must have a reason to support it, and I don't yet see a reason to export the large |
Channel struct just have many responsibilities and you could simply do a lot with it. We are already returning large It would work even without big |
I guess this sums up how I think about structs vs interfaces. Structs describe what something can do, and Interfaces describe what part of that is used. This is why "accept interfaces, return structs" makes sense - you declare what you intend to use with interfaces, and you satisfy that with structs. Interfaces in Go don't describe what something can do, which is why it makes more sense to declare interfaces in the consuming packages rather than the providing packages. The one example of returning interfaces I can think of is for factories that can vary its implementation based on its input like |
So in your opinion it would be better to not add any interface at all and make developers implement them on their own each time they need them? How about just the small interfaces? |
I think the smaller interfaces, with an example of how they are composed (like the This sounds like it's a step towards the goal of saving time when writing tests that use this client. |
This post also applies some of the SOLID design principles to Go, including Interface Segregation: https://dave.cheney.net/2016/08/20/solid-go-design
|
I discussed this with @streadway on a Hangout and we settled on something like this (@streadway, feel free to edit some more): type Closer interface {
Close() error
}
type Publisher interface {
Publish(exchange, key string, mandatory, immediate bool, msg amqp.Publishing) error
}
type Consumer interface {
Consume(queue, consumer string, autoAck, exclusive, noLocal, noWait bool, args amqp.Table) (<-chan amqp.Delivery, error)
Cancel(consumer string, noWait bool) error
}
type Acknowledger interface {
Ack(tag uint64, multiple bool) error
Nack(tag uint64, multiple bool, requeue bool) error
Qos(prefetchCount, prefetchSize int, global bool) error
}
type NotifyPublisher interface {
NotifyPublish(confirm chan amqp.Confirmation) chan amqp.Confirmation
}
type NotifyCanceller interface {
NotifyCancel(c chan string) chan string
}
type NotifyCloser interface {
NotifyClose(c chan *amqp.Error) chan *amqp.Error
}
type NotifyFlower interface {
NotifyFlow(c chan bool) chan bool
}
type NotifyReturner interface {
NotifyReturn(c chan amqp.Return) chan amqp.Return
}
type NotifyBlocker interface {
NotifyBlocked(receiver chan amqp.Blocking) chan amqp.Blocking
}
type PublishConfirmer interface {
Confirm(noWait bool) error
// PublishedDeliveryTag() uint64
}
type ExchangeDeclarer interface {
ExchangeDeclare(name, kind string, durable, autoDelete, internal, noWait bool, args amqp.Table) error
}
type ExchangeBinder interface {
ExchangeBind(destination, key, source string, noWait bool, args amqp.Table) error
}
type ExchangeDeleter interface {
ExchangeDelete(name string, ifUnused, noWait bool) error
}
type ExchangeUnbinder interface {
ExchangeUnbind(destination, key, source string, noWait bool, args amqp.Table) error
}
type QueueDeclarer interface {
QueueDeclare(name string, durable, autoDelete, exclusive, noWait bool, args amqp.Table) (amqp.Queue, error)
}
type QueueBinder interface {
QueueBind(name, key, exchange string, noWait bool, args amqp.Table) error
}
type QueueDeleter interface {
QueueDelete(name string, ifUnused, ifEmpty, noWait bool) (int, error)
}
type QueuePurger interface {
QueuePurge(name string, noWait bool) (int, error)
}
type QueueUnbinder interface {
QueueUnbind(name, key, exchange string, args amqp.Table) error
}
type Connection interface {
Closer
Channel() (*amqp.Channel, error)
}
type Transactions interface {
Tx() error
TxCommit() error
TxRollback() error
}
type PollingConsumer interface {
Get(queue string, autoAck bool) (msg amqp.Delivery, ok bool, err error)
}
type Channel interface {
Publisher
Consumer
Acknowledger
PublishConfirmer
ExchangeDeclarer
ExchangeBinder
QueueDeclarer
QueueBinder
PollingConsumer
Closer
} And it doesn't have to wait for #389. @krzysztof-gzocha WDYT? |
I like it. Good job! Will soon try to implement this in this PR. |
@michaelklishin
|
Let's use
These interfaces are for Liskov substitution (input parameters with implementations provided outside the library) rather than interface separation (input parameters for the application) so they should remain in
Let's answer #387 (comment) with some examples before discussing changing the I still would like to understand the interest to mock infrastructure and I value examples to understand better. I can imagine mocking your application's interfaces, but I can't example where real confidence is gained by mocking this library as opposed to using an integration test against a real server. It's kind of like providing a mock MySQL adapter rather than providing a mock to your Repository pattern. From my experience and advice from others it's usually an application level abstraction missing that makes testing obvious rather than incompletely mocking/simulating the infrastructure. |
Thank you. I will start with those smaller interfaces. |
May I suggest |
In Go, imported package names are required to be referenced. When using From an application point of view, it's possible to rename package names during import, so when using Let's not use generic package names like |
@streadway makes sense. |
7265c50
to
7866e74
Compare
24319a8
to
2192f2c
Compare
879d319
to
e351b11
Compare
@michaelklishin 2nd version is ready for CR |
"connection.blocked" server capability key is true. | ||
|
||
*/ | ||
NotifyBlocked(receiver chan amqp.Blocking) chan amqp.Blocking |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FTR, a single channel of notifications is used for both connection.blocked
and connection.unblocked
. See #75 for details.
@michaelklishin Any progress? |
Is this still being worked on? I use this library heavily and would love making unit test easier. |
+1 |
Hey folks, I'm posting this on behalf of the core team. As you have noticed, this client hasn't seen a lot of activity recently. Because this client has a long tradition of "no breaking public API changes", certain We would like to thank @streadway Team RabbitMQ has adopted a "hard fork" of this client What do we mean by "hard fork" and what does it mean for you? The entire history of the project What does change is that this new fork will accept reasonable breaking API changes according If your PR hasn't been accepted or reviewed, you are welcome to re-submit it for Note that it is a high season for holidays in some parts of the world, so we may be slower Thank you for using RabbitMQ and contributing to this client. On behalf of the RabbitMQ core team, |
Short description
This PR aims to demonstrate the idea in issue #383 and to add interfaces for basic structures like
amqp.Channel
andamqp.Connection
, so it developers that are using this library won't have to write them on their own when mocking channel or connection in tests.What's inside
ChannelInterface
, which consists of several other smaller interfaces like (NotifyChannel
,TxChannel
,ConsumeOnlyChannel
,TxConsumeOnlyChannel
,PublishOnlyChannel
,TxPublishOnlyChannel
andServerConfigureChannel
)ConnectionInterface
, which consists of one smaller interfaceNotifyConnection
Is it finished?
Nope, just sharing the idea to collect all the comments about naming or which methods should go to which interface.
Any other important struct is missing its interface?