Backward Compatibility: Practice

Welcome back! Today, we'll master what we learned about backward compatibility in practice. Prepare to apply all the knowledge to practical tasks, but first, let's look at two examples and analyze them.

Task 1: Enhancing a Complex Data Processing Function with Flexible Function Use

Let's say that initially, we have a complex data processing function designed to operate on a slice of maps map[string]interface{}, applying a transformation that converts all string values within the map to uppercase. Here's the initial version:

package main

import (
    "fmt"
    "strings"
)

type DataProcessor struct{}

func (d DataProcessor) ProcessData(items []map[string]interface{}) {
    processedItems := make([]map[string]interface{}, 0)
    for _, item := range items {
        processedItem := make(map[string]interface{})
        for k, v := range item {
            if strVal, ok := v.(string); ok {
                processedItem[k] = strings.ToUpper(strVal)
            } else {
                processedItem[k] = v
            }
        }
        processedItems = append(processedItems, processedItem)
    }
    for i := 0; i < min(3, len(processedItems)); i++ {
        fmt.Println("Processed Item:", processedItems[i])
    }
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

We intend to expand this functionality by adding capabilities to filter the items based on a condition and to allow for custom transformations. The aim is to retain backward compatibility while introducing these enhancements. Here's the updated approach using flexible function signatures and closures:

package main

import (
    "fmt"
    "strings"
)

type DataProcessor struct{}

/*
    List of data items to be processed,
    Function to transform each item,
    Function to determine if the item meets a condition
*/
func (d DataProcessor) ProcessData(
        items []map[string]interface{},
        transform func(map[string]interface{}) map[string]interface{},
        condition func(map[string]interface{}) bool) {
    if condition == nil {
        condition = func(item map[string]interface{}) bool { return true }
    }

    if transform == nil {
        transform = func(item map[string]interface{}) map[string]interface{} {
            transformed := make(map[string]interface{})
            for k, v := range item {
                if strVal, ok := v.(string); ok {
                    transformed[k] = strings.ToUpper(strVal)
                } else {
                    transformed[k] = v
                }
            }
            return transformed
        }
    }

    processedItems := make([]map[string]interface{}, 0)
    for _, item := range items {
        if condition(item) {
            processedItems = append(processedItems, transform(item))
        }
    }

    for i := 0; i < min(3, len(processedItems)); i++ {
        fmt.Println("Processed Item:", processedItems[i])
    }
}

// Usage examples:
func main() {
    data := []map[string]interface{}{
        {"name": "apple", "quantity": 10},
        {"name": "orange", "quantity": 5},
    }

    processor := DataProcessor{}

    // Default behavior - convert string values to uppercase
    processor.ProcessData(data, nil, nil)

    // Custom filter - select items with a quantity greater than 5
    processor.ProcessData(data, nil, func(item map[string]interface{}) bool {
        return item["quantity"].(int) > 5
    })

    // Custom transformation - convert names to uppercase and multiply the quantity by 2
    processor.ProcessData(data, func(item map[string]interface{}) map[string]interface{} {
        transformed := make(map[string]interface{})
        for k, v := range item {
            if k == "name" {
                transformed[k] = strings.ToUpper(v.(string))
            } else if k == "quantity" {
                transformed[k] = v.(int) * 2
            } else {
                transformed[k] = v
            }
        }
        return transformed
    }, nil)
}

In this evolved version, we've introduced flexible function signatures that allow users to provide transformation and filtering functions as needed. The default behavior processes all items, converting string values to uppercase, ensuring that the original functionality's behavior is maintained for existing code paths. This robust enhancement strategy facilitates adding new features to a function with significant complexity while preserving backward compatibility, showcasing an advanced application of evolving software capabilities responsively and responsibly.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal