Go - Separate external calls from our main logic

发布时间 2023-09-21 11:15:49作者: ZhangZhihuiAAA

Original implementation:

type SingleItem struct {
    Field    string  `json:"field"`
    Hour     int     `json:"hour"`
    Minute   int     `json:"minute"`
    ItemCode string  `json:"item_code"`
    Price    float64 `json:"price"`
    Quantity int     `json:"qty"`
}

type RawItems struct {
    Items            []SingleItem `json:"items"`
    TotalRecordCount int          `json:"total_record_count"`
    Start            int          `json:"start"`
}

type Calculator struct{}

func (c *Calculator) SomeComplexAggregationFunction(startDate, endDate time.Time, field string) (float64, error) {
    convertedStartTime := startDate.Format("2006-02-01")
    convertedEndTime := startDate.Format("2006-02-01")
    rawResp, err := http.Get(fmt.Sprintf("http://example-data-server/api/data-archive/v1/retail?field=%v&start-date=%v&end-date=%v", field, convertedStartTime, convertedEndTime))
    if err != nil {
        return 0.0, err
    }
    if rawResp.StatusCode != http.StatusOK {
        return 0.0, fmt.Errorf("unexpected status code")
    }
    raw, err := io.ReadAll(rawResp.Body)
    if err != nil {
        return 0.0, err
    }
    var items RawItems
    err = json.Unmarshal(raw, &items)
    if err != nil {
        return 0, err
    }
    // Pretend this is some complex calculation
    summer := 0.0
    for k, v := range items.Items {
        fmt.Printf("processing current item: %v", k)
        summer = float64(v.Quantity)*v.Price + summer
    }
    return summer, nil
}

 

Refactored implementation:

type V1InternalEndpoint struct{}

func (e *V1InternalEndpoint) Retrieve(startDate, endDate time.Time, field string) ([]SingleItem, error) {
    convertedStartTime := startDate.Format("2006-02-01")
    convertedEndTime := startDate.Format("2006-02-01")
    rawResp, err := http.Get(fmt.Sprintf("http://example-data-server/api/data-archive/v1/retail?field=%v&start-date=%v&end-date=%v", field, convertedStartTime, convertedEndTime))
    if err != nil {
        return []SingleItem{}, err
    }
    if rawResp.StatusCode != http.StatusOK {
        return []SingleItem{}, fmt.Errorf("unexpected status code")
    }
    raw, err := io.ReadAll(rawResp.Body)
    if err != nil {
        return []SingleItem{}, err
    }
    var items RawItems
    err = json.Unmarshal(raw, &items)
    if err != nil {
        return []SingleItem{}, err
    }
    return items.Items, nil
}

type DataRetriever interface {
    Retrieve(startDate, endDate time.Time, field string) ([]SingleItem, error)
}

type Calculator struct {
    d DataRetriever
}

func (c *Calculator) SomeComplexAggregationFunction(startDate, endDate time.Time, field string) (float64, error) {
    items, err := c.d.Retrieve(startDate, endDate, field)
    if err != nil {
        return 0, err
    }
    // Pretend this is some complex calculation
    summer := 0.0
    for k, v := range items {
        fmt.Printf("processing current item: %v", k)
        summer = float64(v.Quantity)*v.Price + summer
    }
    return summer, nil
}

Notice how much simpler the function becomes as we move the external call out of the function where we implement the logic which would contain our main logic. The call to retrieve data could be a real piece of code that actually does external calls and retrieve data via JSON or thrift, or GRPC protocols. It could also be a piece of code that provides fake data that we could test our application against.

 

Seeing that our code now relies on DataRetriever interface, we would need to build our mock against it and ensure that it follows that function signature.

type FakeDataRetriever struct{}

func (f *FakeDataRetriever) Retrieve(startDate, endDate time.Time, field string) ([]SingleItem, error) {
    if field == "receipts" {
        return []SingleItem{SingleItem{Price: 1.1, Quantity: 2}}, nil
    }
    return []SingleItem{}, fmt.Errorf("no data available")
}

We can specify what kind of data that might be returned from the external calls:
• Maybe an array of 1,000 items could be returned
• Maybe include invalid data 

You can probably extend this concept further, such as database calls; application calls to queue systems such as Kafka and Nats; or application calls to caches such as Redis, and so on. All of this can be mocked and have unit tests be run against the
logic that we write up—it is just that it takes a bit of effort to maintain such mocking code.