Search code examples
goooptestingdesign-patterns

Instantiating large number of objects from different types


Assume there is interface:

type CustomMetrics interface {
   Update() error
   Init() error
}

This interface will have many different implementations (structs), each implementation will be shipped with New method, which returns new object of this type, here is example:

type TestMetric struct {
   metric IntGauge
}

func (m *TestMetric) Init() error { 
   // registers the metric in otel metrics, basically deals with exporting it via HTTP
}

func (m *TestMetric) Update() error { 
   // Contacts another system,  parses the data and update the metric value in otel metrics
}

the Update methods are quite complex in reality. Also they are very different from one another in terms of implementation.

Following this pattern, if I want to instantiate all objects from a single function I will have to create a method similar to:

func InitAllMetrics() []CustomMetrics {
   cm := make(CustomMetrics, 0) 
   testMetric1, err := NewTestMetric()
   if err != nil {
      return err
   }
   cm = append(cm, testMetric1)
   testMetric2, err := NewTestMetric2()
   if err != nil {
      return err
   }
   cm = append(cm, testMetric2)
}

This method will become extremely big(I need to create around 50 metrics), it will be error prone to modify and difficult to test.

Are there any alternatives?


Solution

  • One would be using a table, like in tests:

    func InitAllMetrics() ([]CustomMetrics, error) {
        m := []func() (CustomMetrics, error){
            func() (CustomMetrics, error) { return NewTestMetric1() },
            func() (CustomMetrics, error) { return NewTestMetric2() },
        }
        cm := make([]CustomMetrics, len(m))
        for i, mm := range m {
            var err error
            if cm[i], err = mm(); err != nil {
                return nil, fmt.Errorf("error creating metrics %d: %w", i, err)
            }
        }
    
        return cm, nil
    }
    

    This is one line per metric, it doesn't get much shorter than that.

    Reflection is an option, but I'm not sure if it would be worth the effort.

    An alternative with slightly worse error handling would be:

    type metricsAggregator struct {
        CustomMetrics []CustomMetrics
        Err           error
    }
    
    func (m *metricsAggregator) Add(cm CustomMetrics, err error) {
        if err != nil {
            if m.Err != nil {
                m.Err = err
            }
    
            return
        }
    
        m.CustomMetrics = append(m.CustomMetrics, cm)
    }
    
    func InitAllMetrics() ([]CustomMetrics, error) {
        var ma metricsAggregator
        ma.Add(NewTestMetric1())
        ma.Add(NewTestMetric2())
    
        return ma.CustomMetrics, ma.Err
    }
    

    Or you use something like this in a loop:

    func CreateTestMetric(i int) (CustomMetrics, error) {
        switch i {
        case 1:
            return NewTestMetric1()
    
        case 2:
            return NewTestMetric2()
    
        default:
            return nil, fmt.Errorf("metric %d does not exist", i)
        }
    }
    

    You get the idea. I'm unsure if it is worth thinking too long about it, since you're writing 50 NewTestMetric functions anyway - maybe this is a concept worth thinking about.


    Edit:

    If you like the aggregator, you could initialize your metrics concurrently:

    package ...
    
    import (
        "context"
    
        "golang.org/x/sync/errgroup"
    )
    
    type metricsAggregator struct {
        wg  *errgroup.Group
        ctx context.Context
        cm  chan []CustomMetrics
    }
    
    func NewMetricsAggregator(ctx context.Context, limit int) *metricsAggregator {
        wg, ctx := errgroup.WithContext(ctx)
        wg.SetLimit(limit)
    
        cm := make(chan []CustomMetrics, 1)
        cm <- nil
    
        return &metricsAggregator{wg: wg, ctx: ctx, cm: cm}
    }
    
    func (m *metricsAggregator) Result() ([]CustomMetrics, error) {
        if err := m.wg.Wait(); err != nil {
            return nil, err
        }
    
        return <-m.cm, nil
    }
    
    func AddMetric[T CustomMetrics](m *metricsAggregator, newTestMetric func() (T, error)) {
        m.wg.Go(func() error {
            select {
            case <-m.ctx.Done():
                return m.ctx.Err()
    
            default:
            }
    
            var testMetric CustomMetrics
            var err error
            if testMetric, err = newTestMetric(); err != nil {
                return err
            }
    
            m.cm <- append(<-m.cm, testMetric)
    
            return nil
        })
    }
    
    func InitAllMetrics5(ctx context.Context) ([]CustomMetrics, error) {
        ma := NewMetricsAggregator(ctx, 10)
        AddMetric(ma, NewTestMetric1)
        AddMetric(ma, NewTestMetric2)
    
        return ma.Result()
    }