Refactor “if” statements - functional programming style

Tuesday, November 22, 2011

Have you ever seen code that look like this:
public string GetStatusDescription(Model model)
{
    if(model.HasProblemReports)
    {
        return "Errors";
    }

    if(model.SystemState.WorkingMode == WorkingMode.NotManaged)
    {
        return "Manual";
    }

    if(model.SystemState.IsInitializing)
    {
        return "Initialize";
    }

    if(!model.SystemState.InService)
    {
        return "Not in service";
    }

    if(model.SystemState.WorkingMode == WorkingMode.Paused)
    {
        return "Paused";
    }

    if(model.Storage.Objects.Any(obj => obj.IsMoving))
    {
       return "Movement in storage";
    }

    return string.Empty;
}

I know I have – yesterday, I’ve changed the variable names and removed a bit of plumbing code but in essence this is a real function I had to work with.

The problem with this kind of code that it start out simple but quickly becomes hard to read and maintain. Because the ordering of the “if” statements matters as much as the condition itself.

One solution is to use the well known Chain of Responsibility pattern. although it would probably work in this case, I’ve noticed that developers shy from using it in code similar to this example. It’s as if they feel that they “waste” good code by creating a”whole class for two lines of code”. Another reason not to use CoR is that in this case I don’t want to encapsulate the logic of this method in a lot of classes – I just want to be able to easily understand what the method does and add/remove/update it easily.

I literally felt pain writing each new “if” statement until I could take it no more and so I’ve decided to refactor it.

First I’ve created a class to hold each condition:

class ModelToMessage
{
    private Func<Model, bool> _predicate;

    public ModelToMessage(Func<model ,="" bool=""> predicate, string message)
    {
        _predicate = predicate;

        Message = message;
    }

    public bool IsValid(Model model)
    {
        return _predicate(model);
    }

    public string Message{get; private set;}
}

Using this new class I was able to declare the following list and refactor my method:

private List<modeltomessage> _messageCreators = new List<modeltomessage>
    {
        new ModelToMessage(model => model.HasProblemReports, "Errors"),
        new ModelToMessage(model => model.SystemState.WorkingMode == WorkingMode.NotManaged, "Manual"),
        new ModelToMessage(model => model.SystemState.IsInitializing, "Initialize"),
        new ModelToMessage(model => !model.SystemState.InService, "Not in service"),
        new ModelToMessage(model => model.SystemState.WorkingMode == WorkingMode.Paused, "Paused"),
        new ModelToMessage(model => model.Storage.Objects.Any(obj => obj.IsMoving), "Movement in storage"),
        new ModelToMessage(model => true, string.Empty),
    };

public string GetStatusDescription(Model model)
{
   var messageCreator = _messageCreators.First(mc => mc.IsValid(model));

    return messageCreator.Message;
}

The last item on the list is the “default” item that is always true and return an empty string.

I really like the fact that it’s easy to understand which message comes before which message and it’s easy to add new conditions and change their priority.


Happy coding…

13 comments

  1. "The problem with this kind of code that it start out simple but quickly becomes hard to read and maintain. Because the ordering of the “if” statements matters as much as the condition itself."


    You did not remove these problems. The code was as simple as it possibly could be. Now there is an additional layer added on top of it that has nothing to do with the business rule. It is just artificial complexity.

    I was surprised that you found the first code hard to read and to maintain. I think it is as clear and as concise as code gets. There is no noise in there at all, just business rules encoded in C#.

    ReplyDelete
  2. Having had to maintain code with endless ifs, the CoR pattern is welcome relief. The "if" code really is not all that readable once you get more than a page. Not to mention all the boiler plate code. Extend this example to 50 clauses and I think you'll find CoR is the better route.

    ReplyDelete
  3. Feels like over engineering to me. I like the code just don't like the reason for doing it.

    ReplyDelete
  4. Here an alternative solution:

    return    model.HasProblemReports ? "Errors" :    model.SystemState.WorkingMode == WorkingMode.NotManaged ? "Manual" :    model.SystemState.IsInitializing ? "Initialize" :    !model.SystemState.InService ? "Not in service" :    string.Empty;
    C# has this style already built-in.

    ReplyDelete
  5. Hi  Mike,

    I really like your code, and I haven't seen a simple case demonstrating this pattern before - so I really appreciate it.

    What I like, is that the important part - as you say - is the ordering of the rules - the chain of responsibility.

    In the CoR pattern, I can see all the rules specified in one place - on subsequent lines. I am certain this conveys intent a lot quicker. 

    If I was stepping into this code as a maintenance developer, it would also force me to focus on the ordering - the pattern explains that the order is important of the rules. With the sequence of if statements, it is so easy to add or change conditions, and not even realise how important the order is. You make one scenario work - the one you're trying to fix - but then you kill all the others.

    A big thumbs up, mate. Great job.

    ReplyDelete
  6. Tobi,

    I feel your code is poor at clarifying intent and would cause many-a-developer to want to punch you.

    But.....I'm just an idiot haha 

    ReplyDelete
  7. I'd like to thank you for your thoughts - you all post good reason to use (or not to use) this refactoring I suggested.

    We had similar discussion on my team and  it seems there is no "right" or "wrong" here. In the specific example in this post the "if" clauses were getting out of hand we had more then 10 of those and bugs were starting to creep in - refactoring solved that problem and now it's easier to add more "business rules". I agree that every new developer seeing that code has to think a few min. before understanding what it does - but it takes less time than the previous two page long method riddled with "if" statements.

    ReplyDelete
  8. Actually,

    Maybe I've changed my mind about this one... You could actually format it so each rule lived on a new line. Then it wouldn't look so bad.

    Told you I was an idiot - sorry tobi

    ReplyDelete
  9. If you just reformatted your original code as:

    if(model.HasProblemReports)   return "Errors";if(model.SystemState.WorkingMode == WorkingMode.NotManaged) return "Manual";if(model.SystemState.IsInitializing) return "Initialize";if(!model.SystemState.InService)     return "Not in service";

    You would have pretty much accomplished everything your revised code does.

    ReplyDelete
  10. How about sth like this
    Class Foo
    {
    int age;
    DateTime dateTime; 
    MyType mytype;
    }

    if(mytype.Type==...)
      return Func1(); //do some job on age
    if(mytype.Type==...)
     return Func2(); //do some job on dateTime
    ....

    ReplyDelete
  11. Would be simpler and cleaner IMO to map predicates to messages using a Dictionary:

    Dictionary,string>>

    The call would then be:

    string message = _messageMap.First( x => x.Key( model ) ).Value;

    ReplyDelete
  12. It's another way to implement it, and I suggest you use a Dictionary if you find it more readable. I've used a List in my solution since I'm not really using the Dictionary functionality

    ReplyDelete

Newer Older
Related Posts Plugin for WordPress, Blogger...