views:

182

answers:

8

There's a really obvious refactoring opportunity in this (working) code.

bool Translations::compatibleNICodes(const Rule& rule, 
                                     const std::vector<std::string>& nicodes)
{
    bool included = false;

    // Loop through the ni codes.
    for(std::vector<std::string>::const_iterator iter = nicodes.begin();
        iter != nicodes.end();
        ++iter)
    {
        // Match against the ni codes of the rule
        if(rule.get_ni1() == *iter)
        {
            // If there's a match, check if it's flagged include or exclude
            const std::string flag = rule.get_op1();
            // If include, code is included unless a later rule excludes it
            if(flag == "INCLUDE"){ included = true; }
            // If exclude, code is specifically excluded
            else if(flag == "EXCLUDE"){ return false; }
        }
        if(rule.get_ni2() == *iter)
        {
            const std::string flag = rule.get_op2();
            if(flag == "INCLUDE"){ included = true; }
            else if(flag == "EXCLUDE"){ return false; }
        }
        if(rule.get_ni3() == *iter)
        {
            const std::string flag = rule.get_op3();
            if(flag == "INCLUDE"){ included = true; }
            else if(flag == "EXCLUDE"){ return false; }
        }
        if(rule.get_ni4() == *iter)
        {
            const std::string flag = rule.get_op4();
            if(flag == "INCLUDE"){ included = true; }
            else if(flag == "EXCLUDE"){ return false; }
        }
        if(rule.get_ni5() == *iter)
        {
            const std::string flag = rule.get_op5();
            if(flag == "INCLUDE"){ included = true; }
            else if(flag == "EXCLUDE"){ return false; }
        }
    }
    return included;
}

I want to turn it to something like:

bool Translations::compatibleNICodes(const Rule& rule, 
                                     const std::vector<std::string>& nicodes)
{
    bool included = false;

    // Loop through the ni codes.
    for(std::vector<std::string>::const_iterator iter = nicodes.begin();
        iter != nicodes.end();
        ++iter)
    {
        // Match against the ni codes of the rule
        included |= matchNICode(rule.get_ni1(), rule.get_op1);
        included |= matchNICode(rule.get_ni2(), rule.get_op2);
        included |= matchNICode(rule.get_ni3(), rule.get_op3);
        included |= matchNICode(rule.get_ni4(), rule.get_op4);
        included |= matchNICode(rule.get_ni5(), rule.get_op5);
    }
    return included;
}

bool Translations::matchNICode(const std::string& ni, 
                               const std::string& op)
{
    if(ni == *iter)
    {
        if(op == "INCLUDE"){ return true; }
        else if(op == "EXCLUDE"){ /*Return worse than false*/ }
    }
    return false;
}

The problem is that I can't get around the problem that I want to exit early if it's an exclude statement.

Note that I can't change the structure of the Rule class.

Any advice?

+2  A: 

Obviously the code would be much cleaner and simpler, if you could iterate through the ni and op members of Rule in a loop. If you can't refactor Rule, maybe you could create a wrapper around it to achieve this goal.

If you have a single method with such code, I wouldn't bother though. IMO this would only pay off if you can eliminate the duplicated code in several similar methods.

Péter Török
True. What I was wondering is if there was a way to refactor so that the if statements could be refactored to a sub-routine.
deworde
@deworde, the early exit makes it difficult indeed :-( Of course you could code around it, but the result would be more complicated and less understandable then the current code IMHO.
Péter Török
A: 

At first, I would rethink methods get_ni1 ... as get_ni(1). Similarly for get_opN as get_op(N). get_ni and get_op then can do different things according to a switch-case. This way the list of if can be put into a for loop. When exclude appears, you can break. The problem is that breaking from nested loops is not possible in C++. But you can use another bool so that when you break from inner for, at the end you check this bool (initially false) and if it is true, you break from outer for too. And finally included is returned.

Now if you want the whole

 const std::string flag = rule.get_op5();
  if(flag == "INCLUDE"){ included = true; }
  else if(flag == "EXCLUDE"){ return false; }

as a subroutine, you can make it return boolean value and check for it. The caller "decides" if break/return or not. Of course the caller must pass i also (if looping over i for get_op changed as described before).

But unless you use that thing in other functions too, the prev "refactoring" with for and the change of get_ni and get_op make according to me not necessary to turn that code into a subroutine.

EDIT

An example (semi-pseudocoded)

totalexit = false;
for( ...iter... )
{
   for( i = 1; i <= 5; i++ ) {
      if (rule.get_ni(i) == *iter) {
         // ...
         if (flag == "INCLUDE") { included = true; }
         else (flag == "EXCLUDE") { included = false;
            totalexit = true; break; }
         // you can return directly from here indeed
         // but this show a general way of breaking from nested loops
      }
   }
   if (totalexit) break;
}
return included;

EDIT Added a comment in the code: return false can be used from the inner loop in this approach, even if you make that piece a function (see my comment); but in case you need to do something before returning, this solution could be useful.

Final Edit

The code you've added and requirement (no change to Rule) made me think about another possible solution I've described in the comment to the question. Basically it is an analysis of what is returned to the caller of compatibleNICodes. It is true iff all the calls to matchNICode are true, false otherwise. In the false case, it is matchNICode that "wants" to return false, but this is not essential, since false is false, and true is true.

So, if you put in And all the values returned by matchNICode, and makes this one return true or false (instead of "more than false"), you can write something like

if ( !(matchNICode(...1) && matchNICode(...2) && ...) ) return false;

and you can also change the final return included into return true since if the end of the loop is reached for real, included must be true (otherwise in your idea a sudden return from inside matchNICode should have happened).

ShinTakezou
As stated, CAN'T CHANGE RULE.
deworde
can I vote myself up? I find this -1 really stupid, expecially since basically I am saying the same thing of an answer that's got +1 and say no word at all about refactorying the if with a subroutines. I've already said that: no matter the techinicalities, Q/A sites are frequented by the same kind of voters.
ShinTakezou
+1 This is the best solution. And when people say CAN'T, I say bullsh*t. This is code we are talking about - nothing in this world is easier to change.
anon
@deworde you stated it __AFTER__ I've posted my code. I've started writing __BEFORE__ the first answer appeared (and I did not push on load new answers); now I won't change my answers, which contains already the possible solution even in the case you do not want/can't use the inner for loop: the subs return a bool value and the caller decide according to it if return or not. BTW the returned value is exactly included:`included = asub(flag); if(!included)return included;` no other ways, unless you use jumps (ugly); and anyway is not a so good refactoring at all
ShinTakezou
My downvote is because that your answer ignores my question.Yes, refactoring the Rule class to use an int would be useful, and in fact, I could keep the "else if(flag == "EXCLUDE") { included = false" given your example.So it's a very *good* answer to a completely different question.
deworde
@Neil, true from the technical point of view, but often false due to sociological reasons (team rules / politics if you will).
Péter Török
@Shin, I understand your frustration, but still see no point in ranting about the local rules. That's just a waste of your own energy IMHO. SO members have the right to up/downvote, even if their reasoning is invalid to you (or even without bothering to explain). I have got many unexplained / unjustified downvotes myself - it didn't rfeel right, but there is no point arguing. Better write it down as random loss, and move on to write an even better next answer :-)
Péter Török
@ShinI checked the revision, it was specified as part of the original question.
deworde
I removed the downvote, as it caused an issue.
deworde
@Péter Török project chiefs or whoever should plan a rewriting of Rule instead of letting programmers become mad to refactor their code based on poor implementation of other people classes...
ShinTakezou
@deworde no wish to appear rude, I've just stressed something that can happen when people can add informations here (in the question) or there (in the comments) while one is still writing an answer based on the First Provided Informations, and something voters/askers should be aware of and consider before evaluation of answers. (I think it is in general good idea to vote when the question is "cold" and not while it is "hot")
ShinTakezou
@Shin, I agree that this is how it should go in an ideal world :-) Sometimes the real is far from the ideal though :-(
Péter Török
@Péter Török re:ranting."Hot" Qs are subject to change and this can change Voters perception of a good/bad A. Good Vs(opposed to bad one who downvote since dislike e.g.)can vote you down even because the Q changed since you've written your A and of course vote is subjective.This is ok, but when two As saying the same thing get two opposite votes,it looks disappointing.Q/A sites should register when I start writing my A,rather than when I post it, so that Vs can evaluate As according to changes too.Same for Comments:deworde C seems antecedent to mine,but when I started it,no Cs appeared at all!
ShinTakezou
@Péter Török see it happened again, when started prev comments, your about ideal world was not there! :-D Anyway I do not get upset for these things, life has more hard moments than these and I am used to this happenings... just I can't help writing and noting these "traps" and oddnesses;) sorry if it seems sometimes annoying or rude, I am not "smooth" in Q/A sites when "defending" my As or ideas :-)
ShinTakezou
@Shin: I think the OP's downvote was fully justified. You wrote an answer entirely based on an assumption that the question clearly specifies is invalid. That means your answer doesn't solve the problem.
jalf
@jalf : you can say it was not an exact answer to the exact question (exiting from a function made from the if statement), but __at the beginning__ the Q __had not__ specified he can't modify Rule at all. My only assumption was that __refactoring is about making better code__, not worst. I've added a comment to the Q to refactor it another way, avoiding the "nested returning" problem, iff my analysis is not wrong of course.
ShinTakezou
@Shin: The very first revision of the question **did** contain the following statement: "Note that I can't change the structure of the Rule class". Maybe you didn't notice it, which is fair enough, but it was there, and it was there to begin with.
jalf
@deworde ... or I missed Rule cant be changed for real?! Whoa it could be.. anyway even my original answer gave a solution without Rule modification: "Now if you want the whole ...."; I _cited_ it rather than proposing real working code, but I suppose it's not a problem for programmers once I say "you put it into a function that returns true and false, and decide accoding to this returned value if you return suddely or continue looping", or similar words. Of course, the "of course caller must pass i" referred to Rule mods, which you could have skipped, or suggest Rule coders to change it!
ShinTakezou
@jalf it could be, but you all then missed the fact that my answer answered in that case too so being not totally OT or whatever, read prev comment to deworde: Now if you want the whole... until "of course caller must pass i" referred ... I did not proposed actual code for a dissatis-factoring answer, but there's a suggestion that brings to something like deworde matchNICode funcs, to be mixed with my final edit
ShinTakezou
ShinTakezou
+2  A: 

One possible refactoring is the below, but I'm not sure if it is worth the trouble

#define NI_CLAUSE(ID) \
        if(rule.get_ni ## ID() == *iter) \
        { \
            const std::string flag = rule.get_op ## ID(); \
            if(flag == "INCLUDE"){ included = true; } \
            else if(flag == "EXCLUDE"){ return false; } \
        }

bool Translations::compatibleNICodes(const Rule& rule, 
                                     const std::vector<std::string>& nicodes)
{
    bool included = false;

    // Loop through the ni codes.
    for(std::vector<std::string>::const_iterator iter = nicodes.begin();
        iter != nicodes.end();
        ++iter)
    {
        NI_CLAUSE(1)
        NI_CLAUSE(2)
        NI_CLAUSE(3)
        NI_CLAUSE(4)
        NI_CLAUSE(5)
    }
    return included;
}
Nick D
Best option so far.
deworde
I would undef that macro after usage, just as a precaution
daramarak
it's good but it is not refactoring, the code ends to be the same.
ShinTakezou
+3  A: 
bool Translations::compatibleNICodes(const Rule& rule,
                                     const std::vector<std::string>& nicodes)
{
    bool included = false;

    struct
    {
      RULE_GET_NI get_ni;
      RULE_GET_OP get_op;
    } method_tbl[] =
    {
      { &Rule::get_ni1, &Rule::get_op1 },
      { &Rule::get_ni2, &Rule::get_op2 },
      { &Rule::get_ni3, &Rule::get_op3 },
      { &Rule::get_ni4, &Rule::get_op4 },
      { &Rule::get_ni5, &Rule::get_op5 },
    };
    // Loop through the ni codes.
    for(std::vector<std::string>::const_iterator iter = nicodes.begin();
        iter != nicodes.end();
        ++iter)
    {
        for(size_t n = 0; n < 5 /* I am lazy here */; ++n)
        {
            if((rule.*(method_tbl[n].get_ni))() == *iter)
            {
                // If there's a match, check if the rule is include or exclude
                const std::string flag = (rule.*(method_tbl[n].get_op))();
                // If include, code is included unless a later rule excludes it
                if(flag == "INCLUDE"){ included = true; }
                // If exclude, code is specifically excluded
                else if(flag == "EXCLUDE"){ return false; }
            }
        }
    }
    return included;
}

The answer was edited to include only final version.

BTW this problem is fun, just give me some more time and I come up with stl algorithm and functor...

Tomek
I like this (+1), allows for both the ideas scattered (for loop and "hiding" of the ugly get_ni/opN in Rule problem)
ShinTakezou
I wasn't looking for something this complex really, but I do like the final version. Can you do an edit that just includes that?
deworde
+3  A: 

I assume that the get_niX() or get_opX() methods have some kind of side effect; otherwise, as soon as you get a true, you would want to exit.

If the thing returned from matchNICode() is really worse than false, it may be an exception. In this case, it is quite simple:

bool Translations::compatibleNICodes(const Rule& rule, 
                                     const std::vector<std::string>& nicodes)
{
    bool included = false;

    try
    {
      // Loop through the ni codes.
      for(std::vector<std::string>::const_iterator iter = nicodes.begin();
          iter != nicodes.end();
          ++iter)
      {
        // Match against the ni codes of the rule
        included |= matchNICode(rule.get_ni1(), rule.get_op1);
        included |= matchNICode(rule.get_ni2(), rule.get_op2);
        included |= matchNICode(rule.get_ni3(), rule.get_op3);
        included |= matchNICode(rule.get_ni4(), rule.get_op4);
        included |= matchNICode(rule.get_ni5(), rule.get_op5);
      }
      return included;
    }
    catch (WorseThanFalseException& ex)
    {
      return false; // Or whatever you have to do and return
    }
}

bool Translations::matchNICode(const std::string& ni, 
                               const std::string& op)
{
    if(ni == *iter)
    {
        if(op == "INCLUDE"){ return true; }
        else if(op == "EXCLUDE"){ throw WorseThanFalseException(); } // Whatever this is
    }
    return false;
}
Gorpik
A: 

This is promised algorithms based solution and it is hardcore... And they are saying STL is here to simplify our programs (this is waaaay longer then the other solution I proposed).

struct FUNCTOR : std::unary_function<bool, std::string const &>
{
  public:
    FUNCTOR(Rule const &r) : included(false), rule(r)
    {
    }
    // returns true if exluded
    bool operator()(std::string const &s)
    {
      static METHODS methods[] =
      {
        { &Rule::get_ni1, &Rule::get_op1 },
        { &Rule::get_ni2, &Rule::get_op2 },
        { &Rule::get_ni3, &Rule::get_op3 },
        { &Rule::get_ni4, &Rule::get_op4 },
        { &Rule::get_ni5, &Rule::get_op5 },
      };
      return(std::find_if(&methods[0], &methods[5], FUNCTOR2(rule, s, included)) != &methods[5]);
    }
    operator bool()
    {
      return(included);
    }
  private:
    struct METHODS
    {
      std::string (Rule::*get_ni)() const;
      std::string (Rule::*get_op)() const;
    };
    struct FUNCTOR2 : std::unary_function<bool, METHODS>
    {
      public:
        FUNCTOR2(Rule const &r, std::string const &s, bool &incl) : rule(r), str(s), included(incl)
        {
        }
        // return true if exluded
        bool operator()(METHODS m)
        {
          if((rule.*m.get_ni)() == str)
          {
            // If there's a match, check if the rule is include or exclude
            const std::string flag = (rule.*m.get_op)();
            // If include, code is included unless a later rule excludes it
            if(flag == "INCLUDE")
              included = true;
            // If exclude, code is specifically excluded
            else if(flag == "EXCLUDE")
            {
              included = false;
              return(true);
            }
          }
          return(false);
        }
      private:
        Rule const &rule;
        std::string const &str;
        bool &included;
    };
    Rule const &rule;
    bool included;
};

bool Translations::compatibleNICodes(const Rule& rule,
                                     const std::vector<std::string>& nicodes)
{
  FUNCTOR f(rule);
  std::find_if(nicodes.begin(), nicodes.end(), f);
  return(f);
}
Tomek
+1  A: 

You could get around by creating some kind of tribool class and use lazy evaluation.

class TriState
{
public:
  TriState(): mState(KO) {}

  bool isValid() const { return mState != FATAL; }

  bool ok() const { return mState == OK; }

  void update(std::string const& value,
              std::string const& reference,
              std::string const& action)
  {
    if (mState == FATAL) return;

    if (value == reference)
    {
      if (action == "INCLUDE") mState = OK;
      else if (action == "EXCLUDE") mState = FATAL;
    }
  }

private:
  typedef enum { OK, KO, FATAL } State_t;
  State_t mState;
};

Then you can use the loop as such:

TriState state;

for (const_iterator it = nicodes.begin(), end = nicodes.end();
     it != end && state.isValid(); ++it)
{
   state.update(*it, rule.get_ni1(), rule.get_op1);
   state.update(*it, rule.get_ni2(), rule.get_op2);
   state.update(*it, rule.get_ni3(), rule.get_op3);
   state.update(*it, rule.get_ni4(), rule.get_op4);
   state.update(*it, rule.get_ni5(), rule.get_op5);
}

return state.ok();

Now, if the operation on rule have some kind of side effect that should be avoided, you use a wrapper to get lazy evaluation.

class Wrapper
{
public:
  Wrapper(Rule const& rule): mRule(rule) {}

  std::string const& get_ni(size_t i) const { switch(i) { ... } }
  std::string const& get_action(size_t i) const { switch(i) { ... } }

private:
  Rule const& mRule;
};

Refactor update:

void update(std::string const& value, Wrapper wrapper, size_t index)
{
  if (mState == FATAL) return;

  if (value == wrapper.get_ni(index))
  {
    if (wrapper.get_action(index) == "INCLUDE") mState = OK;
    else if (wrapper.get_action(index) == "EXCLUDE") mState = FATAL;
  }
}

Use a double loop:

TriState state;
Wrapper wrapper(rule);

for (const_iterator it = nicodes.begin(), end = nicodes.end();
     it != end && state.isValid(); ++it)
{
  for (size_t index = 1; index != 6 && state.isValid(); ++index)
    state.update(*it, wrapper, index);
}

return state.ok();

Guideline: Wrap what you can't modify! (look at the Adaptor family of Patterns)

Matthieu M.
A: 

Using a in/out parameter is a straightforward and efficient way to get two return values:

Plus I presume you need lazy evaluation of rule.get_opN()? To do that you'll need to use a pointer-to-member-function.

bool Translations::compatibleNICodes(const Rule& rule, 
                                     const std::vector<std::string>& nicodes)
{
    bool included = false;

    // Loop through the ni codes.
    for(std::vector<std::string>::const_iterator iter = nicodes.begin();
        iter != nicodes.end();
        ++iter)
    {
        // Match against the ni codes of the rule
        if (!matchNICode(rule.get_ni1(), rule, &Rule::get_op1, included)) return false;
        if (!matchNICode(rule.get_ni2(), rule, &Rule::get_op2, included)) return false;
        if (!matchNICode(rule.get_ni3(), rule, &Rule::get_op3, included)) return false;
        if (!matchNICode(rule.get_ni4(), rule, &Rule::get_op4, included)) return false;
        if (!matchNICode(rule.get_ni5(), rule, &Rule::get_op5, included)) return false;
    }
    return included;
}

bool Translations::matchNICode(const std::string& ni, const Rule& rule, 
                               std::string (Rule::* const opfn)(), bool& included)
{
    if (ni == *iter) {
        const std::string& op = (rule.*opfn)();
        if (op == "INCLUDE") {
            included = true;
        }
        else if (op == "EXCLUDE") {
            return false;
        }
    }
    return true;
}
Ben Voigt