Keep Methods Small and Simple

Summary

Keep methods small and simple.

Details

Small things are easier to understand than big things. Methods are no different.

One way to measure the size of a method is via the number of lines of code it contains.

As a guide methods should not usually be longer than 7 lines in length. This is not a hard rule - just a guide of when to feel uncomfortable with a method’s size.

Another way to gauge the size of a method is to see how many possible paths there are through it. The Cyclomatic complexity of a method gives a measure of this - it will increase as the amount of conditional logic and number of loops grows.

As a guide, methods should not usually have a complexity above 5. Again, this is not a hard rule, just a guide of when to feel uncomfortable.

Your code will naturally contain some methods that are larger than others - some concepts are inherently more complex than others and the implementation will not become simpler if broken down further or expressed in a different way.

But most large methods can be made smaller in one of three ways :

  • Refactoring into a number of smaller methods
  • Re-expressing the logic
  • Using appropriate language features

Splitting a Method into Smaller Concerns

Many large methods have smaller methods within them trying to find a way out.

We can make our code easier to maintain by freeing them.

Bad

  1. protected static Map<String, String> getHttpHeaders(HttpServletRequest request) {
  2. Map<String, String> httpHeaders = new HashMap<String, String>();
  3. if (request == null || request.getHeaderNames() == null) {
  4. return httpHeaders;
  5. }
  6. Enumeration names = request.getHeaderNames();
  7. while (names.hasMoreElements()) {
  8. String name = (String)names.nextElement();
  9. String value = request.getHeader(name);
  10. httpHeaders.put(name.toLowerCase(), value);
  11. }
  12. return httpHeaders;
  13. }

Better

  1. protected static Map<String, String> getHttpHeaders(HttpServletRequest request) {
  2. if ( isInValidHeader(request) ) {
  3. return Collections.emptyMap();
  4. }
  5. return extractHeaders(request);
  6. }
  7. private static boolean isInValidHeader(HttpServletRequest request) {
  8. return (request == null || request.getHeaderNames() == null);
  9. }
  10. private static Map<String, String> extractHeaders(HttpServletRequest request) {
  11. Map<String, String> httpHeaders = new HashMap<String, String>();
  12. for ( String name : Collections.list(request.getHeaderNames()) ) {
  13. httpHeaders.put(name.toLowerCase(), request.getHeader(name));
  14. }
  15. return httpHeaders;
  16. }

Re-expressing logic

Terrible

  1. public boolean isFnardy(String item) {
  2. if (item.equals("AAA")) {
  3. return true;
  4. } else if (item.equals("ABA")) {
  5. return true;
  6. } else if (item.equals("CC")) {
  7. return true;
  8. } else if (item.equals("FWR")) {
  9. return true;
  10. } else {
  11. return false;
  12. }
  13. }

This can be easily re-expressed with less noise as :

Better

  1. public boolean isFnardy(String item) {
  2. return item.equals("AAA")
  3. || item.equals("ABA")
  4. || item.equals("CC")
  5. || item.equals("FWR");
  6. }

Or with a move to a more declarative style :

  1. private final static Set<String> FNARDY_STRINGS
  2. = ImmutableSet.of("AAA",
  3. "ABA",
  4. "CC",
  5. "FWR");
  6. public boolean isFnardy(String item) {
  7. return FNARDY_STRINGS.contains(item);
  8. }

Neither of the above changes alter the structure of our program or even affect the signature of the method. Both still reduce both line count and complexity while increasing readability.

Simplifying things with a series of higher impact changes that extract a model of our domain is, however, often the best approach.

It is difficult to guess what this model might look like for our contrived example, but is likely that this conditional logic could be replaced with polymorphism.

  1. enum ADomainConcept {
  2. AAA(true),
  3. ABA(true),
  4. CC(true),
  5. FWR(true),
  6. OTHER(false),
  7. ANDANOTHER(false);
  8. private final boolean isFnardy;
  9. private ADomainConcept(boolean isFnardy) {
  10. this.isFnardy = isFnardy;
  11. }
  12. boolean isFnardy() {
  13. return isFnardy;
  14. }
  15. }

Using Appropriate Language Features

Methods are sometimes bloated by boilerplate that solves common programming problems. The need for some of this boilerplate has been removed by new language features.

Some of these new features aren’t all that new, but code is still written without them:

  • Java 5 Generics removes the need for ugly casts
  • The Java 5 for-each-loop can replace code using iterators and indexed loops
  • The Java 7 try-with-resources can replace complex try, catch finally blocks
  • The Java 7 multi-catch can replace repeated catch blocks
  • Java 8 lambda expressions can replace anonymous class boilerplate