Loading...

Long Functions Are Always A Code Smell

Long Functions Are Always A Code Smell

This article is in response to fellow MVP Alex Shelga’s recent article Long functions in dataverse plugins – is it still ” code smell”?.  I’ll start with the fact that there is plenty of room for personal preference and there is no magic equation that can be applied to code that can ultimately define code as good or great or bad.  Alex shared his opinion, and here I’ll share mine.  I’ll tell you right now, they will differ (which shouldn’t be a surprise if you’ve read the title).  It is my hope that no one feels that I’m “attacking” Alex (especially Alex), but that everyone can see this as what it is intended to be, a healthy juxtaposition of ideas.

Alex’s Argument

Before I go into my reasons for why long functions are always a code smell, I’ll list the two reasons Alex sees plugins as different and summarize his arguments for why that matters:

  1. Plugins are inherently stateless
  2. Often developed to provide a piece of very specific business logic

This, he says, “seems to render object-oriented approach somewhat useless in the plugins (other than, maybe, for “structuring”)”.  He then dives into this further and seems to imply that OO code is slower and more complicated, and is primarily used to allow for reusability, and if it’s not making the code more reusable, there is no reason to utilize it.  His final point then is that it doesn’t matter to the performance of the system or to unit testing if the code is longer, and in his personal preference, he finds a longer function more readable: “I’d often prefer longer code in such cases since I don’t have to jump back and forth when reading it / debugging it”.  (If this is you, make sure you memorize the Navigate Forward and Navigate Backward commands in your IDE (View.NavigateBackword Ctrl+- and View.NavigateBackword Ctrl+Shift+- in Visual Studio and Alt+Left Arrow and Alt+Right Arrow in VSCode) then you need to spend the next 10 minutes diving into functions to see what they are doing, and then backing out of them using the navigation shortcut keys.  It could change your life. Scout’s honor)

My Argument

There are no facts that he presents that are wrong, plugin logic is inherently stateless, and doesn’t lend itself to loads of reusability.  I also can’t argue if his personal choice of readability is for long functions is right or wrong. But what I can do is argue why I see shorter functions as more readable, as well as other reasons that I have that shorter functions are better for the health and maintainability of the plugin project.

Why (I find) shorter functions are more readable

If you were to pick up a 300 page book with the title “Execute”, that you’ve never read before, with no cover art or introduction, or chapters or table of contents, or synopsis on the back page, but given 60 seconds to examine it and tell someone what it was about, you’d be pretty hard pressed to give an accurate definition.  But, if the book had a table of contents with these chapter names:

  1. Start at the Beginning
  2. Create a Vision
  3. Share the Vision
  4. Create the Company
  5. Invest in Others
  6. Invite Others to Invest
  7. Grow/Multiply

You could guess fairly confidently it’s a book about starting and growing a business.  If you were only interested in the details of how to get additional investors in a business, you might start at chapter 6.  If however the chapter names were as follows:

  1. Prewar
  2. Early Victories
  3. Atrocities Beyond Belief
  4. Final Battles
  5. Capture
  6. The Trial
  7. The Verdict
  8. Final Words

You could guess that the book is about a soldier/general that committed war crimes and was executed.  If you were only interested in learning if the individual had any remorse for their acts, you might start reading at chapter 8.  So not only do these chapter titles allow you to get a very quick understanding of what the book is about, they also allow you to skip large sections of the book when attempting to find a very narrow topic.  The same is true for code and long functions.  If a function is longer than your screen is tall, the first time you look at it, you will have no idea about what it does beyond the reach of your screen without scrolling and reading.  You’d have to read the entire function to determine what it does. This means that if you’re looking for a bug, you’ll need to read and understand half (on average) of the lines in the function before you could find where the bug is.  But, if the function is 15 lines long with 8 well named functions calls, you’d have a much better guess at what the entire function does and where the bug lies.  For example, given this Execute function:

public void Execute(ExtendedPluginContext context)
{
    var data = GetData(context);
    UpdateAttributes(context, data);
    CreateChildRecords(context, data);
    UpdateTarget(context, data);
}

Now these are probably some pretty poor function names, but you can immediately see that the plugin is getting data, updating some attributes, creating child records and then updating the target.  But just a small improvement in the naming would give even more details:

public void Execute2(ExtendedPluginContext context)
{
    var account = GetAccount(context);
    SetMaxCreditLimit(context, account);
    CreateAccountLogEntries(context, account);
    UpdateTargetStatus(context, account);
}

Now it’s easy to see that there is a call to get the account, which is used it to set the max credit limit and create some log entries and then update the status of the target.  If there is a bug with the status getting updated incorrectly, or the max credit limit not being set, or the log entries not having enough details, it is easy to see what function needs to be looked at first, and what functions can be ignored.  Small functions (when done well) are more efficient for understanding.

Another positives from smaller functions is the error log in the trace.  If my Execute function is 300 lines long and it has a null ref, I’ve got to look at 300 lines of code to guess where the null ref could have occurred.  But since the function name is included in the stack trace for plugins (even when the line number isn’t), if the 300 lines where split into 10 functions of 30 lines, then I’d know the function that would be causing the error and would only have a tenth of the code to analyze for null ref.  That’s huge!

My final note comes into play with nesting “ifs”.  Many times I will walk into a project with 300 line Execute functions nested 10-12 levels deep with “if” statements.  This especially causes issues when it comes to trying to line up curly braces, or when an “else” statement occurs when the matching “if” is not on the screen:

                if (bar)
                {
                    if (baz)
                    {
                        Go();
                    }
                }
                else
                {
                    Fight();
                }
            }
            else
            {
                // Wait, what is this else-ing?
                Win();
            }
        }
    }
}

Although there is nothing that says a longer function has to nest “ifs”, if your function is only 10 lines long, it limits the maximum possible number of nested “ifs”.

When Shorter Functions Help With Testing

Alex mentioned that Testing frameworks like FakeXrmEasy (and I’ll through my XrmUnitTest framework in here as well) don’t care about the length of an Execute function.  It’s a black box.  While this is true, as a test creator, the more complex the logic, the more helpful it is to test it in parts, rather than the whole.  For example, in my Execute2 function above, if there are 3 different branches of logic in GetAccount and 2 in SetMaxCreditLimit, and 4 in CreateAccountLogEntries, and 1 in UpdateTargetStatus, this results in 24 different potential dependent paths to test.  Contrast this to testing the parts separately, and only having 10 different tests with only the required setup for each specific function.  This is much more maintainable.  Personally I believe that this can be taken to the extreme as well, and trying to test 100 functions to perfection is usually not the ideal time investment as well, so I may have a couple tests of the execute function start to finish, and cherry pick some of the more complicated functions to test, rather than try to test everything. 

In Conclusion

Take time to analyze other peoples opinions and determine if you agree or disagree to the point where you are be prepared to argue why.  We are all learning and growing in our craft as developers, which requires us to continue to allow new ideas to challenge our existing conventions.  Share it, blog about it, and grow, remembering to always “Raise La Bar”.

Published on:

Learn more
.Net Dust
.Net Dust

NULL

Share post:

Related posts

Stay up to date with latest Microsoft Dynamics 365 and Power Platform news!
* Yes, I agree to the privacy policy