I wrote some code that create "humans". Humans have birthday every 100 ms, and you can subscribe to the event like this:
pers1 := new(Human)
pers1.Init("John")
pers1.Subscribe(func(h Human) { fmt.Printf("Observer1 : %s", h.String()); return })
pers1.Subscribe(func(h Human) { fmt.Printf("Observer2 : %s", h.String()); return })
time.Sleep(3 * time.Second)
Output is the following
HUMAN John is born // by init
HUMAN John is now followed by 0x4901a0 // by subscribe
There is now 1 observers
HUMAN John is now followed by 0x490300 // by subscribe
There is now 2 observers
[T 0100ms]
HUMAN John has its birthday // after 100ms : birthday happens
Observer1 : HUMAN : John is 1 // callback
Observer2 : HUMAN : John is 1 // callback
// ... continue for 3 seconds
Detailed code is here, but the problem is not there https://goplay.tools/snippet/7qsZ1itcqrS
My question is the following:
I would like to create an interface Producer corresponding to things producing events I can subscribe on.
You can subscribe to:
- Human that have birthday
- Humidity sensors that can detect a change in humidity
- Mail servers that got a mail...
In my example, the callback function has as argument : a Human. The one whose age changed...
In the same manner, a given event for a humidity sensor would expected the sensor struct.
My question is
- is there a sense to do such I think? ( This is a scholar question, things work without)
- if yes, how. I wasn't able to find relevant example
That would be
type Producer interface{
Subscribe( func( < something variable >) )
}
I wasn't able to get something working. Also I had difficult to find a good title to the question. Feel free to give me a better one.
CodePudding user response:
Depending on what you need, there are three options that might work for you here.
Option 1: Common Interface for Published Items
Create an interface not only for publishers that can have subscribers, but for the sort of things that those publishers can publish:
type Item interface{
Description() string
Age() int
}
type human struct{
age int
}
func (h *human) Description() string {
return "human"
}
func (h *human) Age() int {
return h.age
}
type Publisher interface{
Subscribe(func(Item))
}
type humanProducer struct{
subscribers []func(Item)
}
func (hp *humanProducer) Subscribe(f func(Item) {
hp.subscribers = append(hp.subscribers, f)
}
// Example use
func addSubscriber(p Publisher, f func(Item)) {
p.Subscribe(f)
}
func main() {
hp := &humanProducer{}
addSubscriber(p, func(i Item) {
fmt.Printf("Got a %s that is %d years old.\n", i.Description(), i.Age())
})
}
You can now set up other types of things to be published by having them implement the Item
interface. The Description
and Age
methods here are just examples - you could add whatever methods there you need.
Pros
- Avoids reflection.
- Avoids type parameters; works in versions before Go 1.18.
- A subscriber can receive multiple kinds of items.
- A publisher can publish multiple kinds of items.
Cons
- Published items can't just be anything - you have to define a pre-determined set of functionality that all kinds of published items must have.
- Published items are hidden behind an interface, so you can only use the functionality exposed in the
Item
interface unless you start casting or using reflection.
Option 2: Interface using Type Parameters
Add type parameters to the interface itself:
type human struct{
age int
}
type Publisher[T any] interface{
Subscribe(func(T))
}
type humanProducer struct{
subscribers []func(*human)
}
func (hp *humanProducer) Subscribe(f func(*human) {
hp.subscribers = append(hp.subscribers, f)
}
// Example use
func addSubscriber[T any](p Publisher[T], f func(T)) {
p.Subscribe(f)
}
func main() {
hp := &humanProducer{}
addSubscriber[*human](p, func(h *human) {
fmt.Printf("Got a human that is %d years old.\n", h.age)
})
}
Pros
- Avoids reflection.
- No restrictions on the sorts of things that can be published.
- Published items aren't hidden behind an interface.
Cons
- Publishers can only publish one certain kind of item.
- Subscribers can only receive one certain kind of item.
- Any use of the
Publisher
interface requires use of type parameters. Only works in Go 1.18 or later.
Option 3: Reflection/Casting
Allow publishers to publish anything and use reflection or casting in subscribers to sort out what kind of thing was published:
type human struct{
age int
}
type Publisher interface{
Subscribe(func(any))
}
type humanProducer struct{
subscribers []func(any)
}
func (hp *humanProducer) Subscribe(f func(any) {
hp.subscribers = append(hp.subscribers, f)
}
// Example use
func addSubscriber(p Publisher, f func(any)) {
p.Subscribe(f)
}
func main() {
hp := &humanProducer{}
addSubscriber(p, func(i any) {
if h, ok := any.(*human); ok {
fmt.Printf("Got a human that is %d years old.\n", h.age)
}
})
}
If using Go pre-1.18, replace any
with interface{}
. This option is sort of the same thing as option 1, except with an empty Item
interface.
Pros
- Avoids type parameters; works in versions before Go 1.18.
- No restrictions on the sorts of things that can be published.
- Published items aren't hidden behind an interface.
- A subscriber can receive multiple kinds of items.
- A publisher can publish multiple kinds of items.
Cons
- Requires reflection or casting, which is slow, awkward, and less safe.
- Subscribers will have to do extra work to figure out what kind of item they received.