Home > Net >  Golang : create an interface to abstract a method which may have have variable argument
Golang : create an interface to abstract a method which may have have variable argument

Time:10-25

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.
  •  Tags:  
  • go
  • Related