Home > database >  How to refactor a function so that it works for custom slice types holding different but similar str
How to refactor a function so that it works for custom slice types holding different but similar str

Time:09-23

I have two different types of activity structs which both have a StartTime. For both of these activity types I have defined a type for a slice of activities. For both of these activity types I want to perform the same processing of their start times. I currently have to have 2 copies of the function, differing only in parameter type, as I cant work out how to get a single function to accept either type.

I've tried adding an interface above the activity types based on the StartTime, but it didnt help due to the custom slice types.

How can I factor out this function so that it can process either activity type?

Here is an example of the problem, with the 2 virtually identical functions: https://play.golang.org/p/pFg8yJwW2Hl

CodePudding user response:

You can create a Starter interface that your two (or more) types implement:

type Starter interface {
    Start() time.Time
}

func (a activityA) Start() time.Time { return a.StartTime }
func (b activityB) Start() time.Time { return b.StartTime }

Then use the reflect package to introspect your function parameter:

  • ensuring the argument is a slice type
  • and each element of the slice implements Starter

the downside of this is that any errors are only caught at runtime, not compile time.


func genericStartTimeActivity(v interface{}) (counter int, err error) {

    if reflect.TypeOf(v).Kind() != reflect.Slice {
        err = fmt.Errorf("argument must be slice type, got type '%T'", v)
        return
    }

    s := reflect.ValueOf(v)
    // since we know v is a slice:
    // - s.Len() gets slice length
    // - s.Index(i) gets a particular value

    for i := 0; i < s.Len(); i   {

        activity, ok := s.Index(i).Interface().(Starter)

        if !ok {
            err = fmt.Errorf("argument's slice index %d does not implement '%s' interface", i, reflect.TypeOf((*Starter)(nil)).Elem().Name()) // pay no attention to the man behind the curtain
            return
        }

        counter  = activity.Start().Day()
    }
    return
}

https://play.golang.org/p/v8dXSPPLk1l

CodePudding user response:

An alternative to interfaces and reflect is to use a functional style.

func lookupSomethingToDoWithStartTimesFromFunction(n int, getStartTime func(i int) time.Time) int {
    counter := 0
    for i := 0; i < n; i   {
        counter  = getStartTime(i).Day()  // do something with the start time
    }
    return counter
}

...
lookupSomethingToDoWithStartTimesFromFunction(len(AActivities), func(i int) time.Time { return AActivities[i].StartTime }),
lookupSomethingToDoWithStartTimesFromFunction(len(BActivities), func(i int) time.Time { return BActivities[i].StartTime }),

https://play.golang.org/p/X95AAkwUVX3

CodePudding user response:

Make your function declare what it needs by using an interface. If your function needs to know only the start time of many different things, make all of those things able to say what their start time is using a method. Using your original example (playground):

package main

import (
    "fmt"
    "time"
)

type activityA struct {
    Name                    string
    StartTime               time.Time
    UniqueStuffForActivityA int
}

func (a activityA) StartedAt() time.Time {
    return a.StartTime
}

type activityB struct {
    ActivityName            string
    StartTime               time.Time
    UniqueStuffForActivityB string
}

func (a activityB) StartedAt() time.Time {
    return a.StartTime
}

type Starter interface {
    StartedAt() time.Time
}

func main() {
    AActivities := []activityA{
        activityA{"Act1", time.Now(), 5},
        activityA{"Act2", time.Now(), 99},
        activityA{"Act3", time.Now(), 23},
    }
    BActivities := []activityB{
        activityB{"Act1", time.Now(), "foo"},
        activityB{"Act2", time.Now(), "bar"},
        activityB{"Act3", time.Now(), "badger"},
    }

    AActivitiesStarter := make([]Starter, len(AActivities))
    for i := range AActivities {
        AActivitiesStarter[i] = AActivities[i]
    }
    BActivitiesStarter := make([]Starter, len(BActivities))
    for i := range AActivities {
        BActivitiesStarter[i] = BActivities[i]
    }

    fmt.Printf("A: [%d]\nB: [%d]\n",
        lookupSomethingToDoWithStartTimes(AActivitiesStarter),
        lookupSomethingToDoWithStartTimes(BActivitiesStarter),
    )

}

func lookupSomethingToDoWithStartTimes(activities []Starter) int {
    counter := 0
    for _, activity := range activities {
        counter  = activity.StartedAt().Day() // do something with the start time
    }
    return counter
}

The loop to convert from a slice of []activityX to a []Starter could be removed by just using a []Starter in the first place, depending on what else you might do with the slices. This loop may be faster or slower than using reflection on a []interface{}, you'd need to Benchmark your case to find out, but it's more readable.

You can also avoid converting entirely if you allow your collection objets to yield Starters with a channel as in this example but you must ensure you read the channels to completion or you will leak goroutines.

  • Related