How to merge two functions with same conditions?

Viewed 358

I quickly wrote the below class for this question.

I'm looking for a way to merge addFruit() with removeFruit() to reduce the code.

They both use identical conditions but just different function call at the end.

My Code :

#include <iostream>
#include <string>
#include <vector>

class MyClass
{
public:
    void addFruit(const std::string &str, int count)
    {
        if (str == "apples")
            addToVec(apples, count);
        else if (str == "oranges")
            addToVec(oranges, count);
        else if (str == "lemons")
            addToVec(lemons, count);
        else if (str == "melons")
            addToVec(melons, count);
        else if (str == "bananas")
            addToVec(bananas, count);
        else
            std::cout << "Unknown Fruit : " << str << '\n';
    }
    void removeFruit(const std::string &str)
    {
        if (str == "apples")
            removeFromVec(apples);
        else if (str == "oranges")
            removeFromVec(oranges);
        else if (str == "lemons")
            removeFromVec(lemons);
        else if (str == "melons")
            removeFromVec(melons);
        else if (str == "bananas")
            removeFromVec(bananas);
        else
            std::cout << "Unknown Fruit : " << str << '\n';
    }
private:
    void addToVec(std::vector<int> &vec, int count)
    {
        vec.push_back(count);
    }
    void removeFromVec(std::vector<int> &vec)
    {
        vec.pop_back();
    }
    std::vector<int> apples;
    std::vector<int> oranges;
    std::vector<int> lemons;
    std::vector<int> melons;
    std::vector<int> bananas;
};

Any clever way to nicely merge the two functions so I can reduce the code?

9 Answers

Make an additional function e.g. determineTargetVector(const std::string &str) which returns the corresponding vector, where you want to insert/remove an element, so you have no redundant conditions. Also its nice to have only a single reponsibility for each function.

For example:

std::vector<int> *determineTargetVector(const std::string &str)
{
    if (str == "apples")
        return &apples;
    else if (str == "oranges")
        return &oranges;
    else if (str == "lemons")
        .
        .
        .
    else
        //something invalid, to check for in superior function
        return nullptr;
}

The easiest solution might be to use a std::map for those vectors:

std::map<std::string,std::vector<int>> fruitVecs;

The key values of the map would be "apples", "oranges", "bananas" etc.

Thus you can easily access the corresponding vector for any operation through the map.

A code that selects the vector to be used and then perform the action can be used :

class MyClass
{
public:
    void addFruit(const std::string &str, int count)
    {
        auto vec = selectVector(str);
        if(vec != nullptr)
            addToVec(*vec, count);
        else
            std::cout << "Unknown Fruit : " << str << '\n';
    }
    void removeFruit(const std::string &str)
    {
        auto vec = selectVector(str);
        if(vec != nullptr)
            removeFromVec(*vec);
        else
            std::cout << "Unknown Fruit : " << str << '\n';
    }
private:

    std::vector<int> *selectVector(const std::string &str)
    {
        if (str == "apples")
            return &apples;
        else if (str == "oranges")
            return &oranges;
        else if (str == "lemons")
            return &lemons;
        else if (str == "melons")
            return &melons;
        else if (str == "bananas")
            return &bananas;
        else
            return nullptr;
    }

    void addToVec(std::vector<int> &vec, int count)
    {
        vec.push_back(count);
    }
    void removeFromVec(std::vector<int> &vec)
    {
        vec.pop_back();
    }
    std::vector<int> apples;
    std::vector<int> oranges;
    std::vector<int> lemons;
    std::vector<int> melons;
    std::vector<int> bananas;
};

What about below solution. That will also allow you add/remove known fruits easily by adding/removing line in constructor.

#include <iostream>
#include <string>
#include <vector>
#include <map>

class MyClass
{
public:
    MyClass()
    {
        allowedFruits["apples"] = {};
        allowedFruits["oranges"] = {};
        allowedFruits["lemons"] = {};
        allowedFruits["melons"] = {};
        allowedFruits["bananas"] = {};
    }
    void addFruit(const std::string &str, int count)
    {
        auto it = allowedFruits.find(str);
        if(it != MyClass::allowedFruits.end()){
            it->second.push_back(count);
        }
        else {
            std::cout << "Unknown Fruit : " << str << '\n';
        }
    }
    void removeFruit(const std::string &str)
    {
        auto it = allowedFruits.find(str);
        if(it != allowedFruits.end()){
            // my be some check here
            it->second.pop_back();
        }
        else {
            std::cout << "Unknown Fruit : " << str << '\n';
        }
    }
private:
    std::map<std::string,std::vector<int>> allowedFruits;
};

Without changing the interface you could do it like this:

std::vector<int>& pickVector(std::string str) {
    // put all the switch here and return a reference to the correct vector
}

void addFruit(const std::string &str, int count)
{
   addToVec(pickVector(str),count);
}

I would go a little functionnal by passing the function to apply.

#include <iostream>
#include <string>
#include <vector>
#include <functional>

class MyClass
{
public:
    void addFruit(const std::string &str, int count)
    {
        searchAndApplyHelper(str, std::bind(&MyClass::addToVec, *this, std::placeholders::_1, count));
    }
    void removeFruit(const std::string &str)
    {
        searchAndApplyHelper(str, std::bind(&MyClass::removeFromVec, *this, std::placeholders::_1));
    }

private:

    template <class Func>
    void searchAndApplyHelper(const std::string str, Func f)
    {
        if (str == "apples")
            f(apples);
        else if (str == "oranges")
            f(oranges);
        else if (str == "lemons")
            f(lemons);
        else if (str == "melons")
            f(melons);
        else if (str == "bananas")
            f(bananas);
        else
            std::cout << "Unknown Fruit : " << str << '\n';
    }

    void addToVec(std::vector<int> &vec, int count)
    {
        vec.push_back(count);
    }
    void removeFromVec(std::vector<int> &vec)
    {
        vec.pop_back();
    }
    std::vector<int> apples;
    std::vector<int> oranges;
    std::vector<int> lemons;
    std::vector<int> melons;
    std::vector<int> bananas;
};

I did it with a template but you could use std::function as well.

I would do this:

class MyClass
{
public:
    void addFruit(const std::string &str, int count)
    {
        doToFruit(str, [&](std::vector<int> &vec){ addToVec(vec, count); });
    }
    void removeFruit(const std::string &str)
    {
        doToFruit(str, [&](std::vector<int> &vec){ removeFromVec(vec); });
    }
private:
    template<typename Callable>
    void doToFruit(const std::string &str, const Callable &func)
    {
        std::pair<const char*, std::vector<int>&> fruits[] = {
            {"apple", apples}, {"oranges", oranges}, {"lemons", lemons},
            {"melons", melons}, {"bananas", bananas}
        };
        for (auto i : fruits)
            if (str == i.first)
                return func(i.second);
        std::cout << "Unknown Fruit : " << str << '\n';
    }
    void addToVec(std::vector<int> &vec, int count)
    {
        vec.push_back(count);
    }
    void removeFromVec(std::vector<int> &vec)
    {
        vec.pop_back();
    }
    std::vector<int> apples;
    std::vector<int> oranges;
    std::vector<int> lemons;
    std::vector<int> melons;
    std::vector<int> bananas;
};

You can use pointer to members if you want better performance:

class MyClass
{
public:
    void addFruit(const std::string &str, int count)
    {
        doToFruit(str, [&](std::vector<int> &vec){ addToVec(vec, count); });
    }
    void removeFruit(const std::string &str)
    {
        doToFruit(str, [&](std::vector<int> &vec){ removeFromVec(vec); });
    }
private:
    template<typename Callable>
    void doToFruit(const std::string &str, const Callable &func)
    {
        auto iFound = fruits.find(str);
        if (iFound != fruits.end())
            return func(this->*(iFound->second));
        std::cout << "Unknown Fruit : " << str << '\n';
    }
    void addToVec(std::vector<int> &vec, int count)
    {
        vec.push_back(count);
    }
    void removeFromVec(std::vector<int> &vec)
    {
        vec.pop_back();
    }
    std::vector<int> apples;
    std::vector<int> oranges;
    std::vector<int> lemons;
    std::vector<int> melons;
    std::vector<int> bananas;
    static std::unordered_map<std::string, std::vector<int> MyClass::*> fruits;
};

std::unordered_map<std::string, std::vector<int> MyClass::*> MyClass::fruits = {
    {"apple", &MyClass::apples}, {"oranges", &MyClass::oranges},
    {"lemons", &MyClass::lemons}, {"melons", &MyClass::melons},
    {"bananas", &MyClass::bananas}
};

Using C++17 you could use optional arguments:

void addRemoveFruit(const std::string &str, std::optional<int> count = std::nullopt)
{
    if (str == "apples")
        addRemoveVec(apples, count);
    else if (str == "oranges")
        addRemoveVec(oranges, count);
    else if (str == "lemons")
        addRemoveVec(lemons, count);
    else if (str == "melons")
        addRemoveVec(melons, count);
    else if (str == "bananas")
        addRemoveVec(bananas, count);
    else
        std::cout << "Unknown Fruit : " << str << '\n';
}

and:

void addRemoveVec(std::vector<int> &vec, std::optional<int> count = std::nullopt)
{
    if(count.has_value()) {
        vec.push_back(count.value());
    } else {
        vec.pop_back();
    }
}

This way existing calls to addFruit/removeFruit only need to be changed to addRemoveFruit without having to change the parameters passed.

To stick with your pattern of multiple distinct vectors, I'd recommend using an internal enum that decides the method, while the choice of the vector can be done in one place:

class MyClass
{
public:
    void addFruit(const std::string &str, int count)
    {
        addOrRemoveFruit(str, count, Method::ADD);
    }
    void removeFruit(const std::string &str)
    {
        addOrRemoveFruit(str, 0, Method::REMOVE);
    }

private:
    enum class Method
    {
        ADD,
        REMOVE
    };

    void addOrRemoveFruit(const std::string &str, int count, Method method)
    {
        if (str == "apples")
            addOrRemoveFruitImpl(apples, count, method);
        else if (str == "oranges")
            addOrRemoveFruitImpl(oranges, count, method);
        else if (str == "lemons")
            addOrRemoveFruitImpl(lemons, count, method);
        else if (str == "melons")
            addOrRemoveFruitImpl(melons, count, method);
        else if (str == "bananas")
            addOrRemoveFruitImpl(bananas, count, method);
        else
            std::cout << "Unknown Fruit : " << str << '\n';
    }

    void addOrRemoveFruitImpl(std::vector<int> &vec, int count, Method method)
    {
        if (method == Method::ADD)
             vec.push_back(count);
        else
             vec.pop_back();
    }

    std::vector<int> apples;
    std::vector<int> oranges;
    std::vector<int> lemons;
    std::vector<int> melons;
    std::vector<int> bananas;
};

However even better would be to use a map:

class MyClass
{
public:
    void addFruit(const std::string &str, int count)
    {
        fruits[str].push_back(count);
    }
    void removeFruit(const std::string &str)
    {
        if (fruits.count(str) > 0)
            fruits[str].pop_back();
    }

private:
    std::unordered_map<std::string, std::vector<int>> fruits;
};
Related