Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid copy and paste when two functions are very similar?

Tags:

c++

dry

I often come across methods that have the same structure and logic, but with some differences and I don't find a proper way not to repeat myself.

For example :

void ChannelSelection::selectAlmostOkChannels(int currentInkId)
{
    bool selected = true;
    foreach (auto report, m_reports) {
        if (report.scoreByInk.find(currentInkId) != report.scoreByInk.end()) {
            auto tmpStatus = Assessment::getStatusFromScore(report.scoreByInk.value(currentInkId));
            if (tmpStatus == Assessment::Ok)
                selected = false;
            else if (tmpStatus == Assessment::NotOk)
                m_autoSelection[report.name].setSelected(currentInkId, false);
        }
    }
    m_currentSelection.insert(currentInkId, selected);
}

void ChannelSelection::selectNotOkChannels(int currentInkId)
{
    bool selected = true;
    foreach (auto report, m_reports) {
        if (report.scoreByInk.find(currentInkId) != report.scoreByInk.end()) {
            auto tmpStatus = Assessment::getStatusFromScore(report.scoreByInk.value(currentInkId));
            if (tmpStatus == Assessment::Ok || tmpStatus == Assessment::AlmostOk)
                selected = false;
        }
    }
    m_currentSelection.insert(currentInkId, selected);
}

As you can see, these 2 functions are very similar (only the inner if differs). How could I nicely remove duplication in this code?

One of the solution I thought of is using a functor, something like :

void ChannelSelection::selectChannels(int currentInkId, std::function<bool()> fn)
{
    bool selected = true;
    foreach (auto report, m_reports) {
        if (report.scoreByInk.find(currentInkId) != report.scoreByInk.end()) {
            auto tmpStatus = Assessment::getStatusFromScore(report.scoreByInk.value(currentInkId));
            selected = fn();
        }
    }
    m_currentSelection.insert(currentInkId, selected);
}

The caller gets the responsibility to implement the functor. Is there an alternative without that problem?

like image 876
R0m1 Avatar asked Sep 23 '19 13:09

R0m1


Video Answer


2 Answers

You don't have to make your parameterized selectChannels public. It can be a private implementation detail of both selectAlmostOkChannels and selectNotOkChannels, your public functions.

selectChannels can even be implemented as a function template, so that the generated code is equivalent to the hand-written "copy pasted" version, without the maintenance burden of code duplication

template<typename SelectFunction>
void ChannelSelection::selectChannels(int currentInkId, SelectFunction selectFn)
{
    bool selected = true;
    foreach (auto report, m_reports) {
        if (report.scoreByInk.find(currentInkId) != report.scoreByInk.end()) {
            auto tmpStatus = Assessment::getStatusFromScore(report.scoreByInk.value(currentInkId));
            selected = selectFn(tmpStatus);
            /* fill in */
        }
    }
    m_currentSelection.insert(currentInkId, selected);
}

void ChannelSelection::selectAlmostOkChannels(int currentInkId)
{
    selectChannels(currentInkId, [] (auto tmpStatus) -> bool {
        return /* fill in */;
    });
}

void ChannelSelection::selectNotOkChannels(int currentInkId)
{
    selectChannels(currentInkId, [] (auto tmpStatus) -> bool {
        return /* fill in */;
    });
}

You might have been taught that templates need to be in headers, but that's actually not the full story! Template definitions need to be visible where instantiated. Since your template is only used in the private implementation of your member functions, then your template definition can be in the same file that's implementing both your member functions

like image 180
KABoissonneault Avatar answered Oct 27 '22 01:10

KABoissonneault


You might merge two functions into a single one with additional conditional parameter like:

void ChannelSelection::selectChannels(int currentInkId, bool condition)
{
  bool selected = true;
  foreach (auto report, m_reports) {
    if (report.scoreByInk.find(currentInkId) != report.scoreByInk.end()) {
      auto tmpStatus = Assessment::getStatusFromScore(report.scoreByInk.value(currentInkId));
      if (condition) {
        if (tmpStatus == Assessment::Ok) {
          selected = false;
        } else if (tmpStatus == Assessment::NotOk) {
          m_autoSelection[report.name].setSelected(currentInkId, false);
        }
      } else if (tmpStatus == Assessment::Ok || tmpStatus == Assessment::AlmostOk) {
        selected = false;
      }
    }
  }
  m_currentSelection.insert(currentInkId, selected);
}

Calling it with condition == true will invoke the equivalent of selectAlmostOkChannels() function, and selectNotOkChannels() otherwise.

like image 27
vahancho Avatar answered Oct 27 '22 00:10

vahancho