Our Approach to Clean Code

Grace Geng
Grace Geng
August 22, 2022

At Harled we're all about moving quickly, failing fast and learning to deliver the best tech to our clients. However, moving this fast can have negative side effects if not done correctly. This post covers how we approach software development with a focus on clean code and keeping a sustainable pace of development.

Why Does it Matter?

As much as we want to be agile and flexible all the time, focusing only on quantity without quality can result in:

  1. A worse product
    • Rails applications lend themselves to a Monolithic architecture - left unchecked, modules in your app can become highly coupled
    • Code changes can now unexpectedly impact other pieces of code, leading to regressions and bugs
    • Product quality takes a hit, and time now needs to be invested in bug-fixes rather than improving the product
  2. Slowing down the team
    • Developers tripping on their own code leads to low confidence that new code can be shipped safely (especially with the absence of testing)
    • Complicated dependencies mean more time is spent understanding the code and impacts of code changes instead of feature work
  3. Team quality taking a hit
    • New hires will have even more difficulty ramping up in an unclear codebase. Understanding just enough to implement a ticket isn't an option if modules are highly coupled.
    • The effects of point 1 and 2, take a huge toll on developer experience: happy coders are productive coders!

Scout's Rule

TLDR: leave code behind better than you found it. When you find code that is difficult to understand, use the opportunity to refactor it into something clearer.

This rule was coined by Uncle Bob, and comes from a rule Scouts have regarding camping: leave the campground cleaner than you found it. There's no need to clean the entire camping area, but the mess shouldn't be left to be cleaned up at some future date.

Why Does it Work?

  • With each feature shipped, code should be easier to work with rather than become an obstacle
  • Code that is touched more often is cleaned more often, those areas are the places where clean code is most valuable
  • Contrast continuous cleanup with an alternative: shutting down all client work in order to tackle technical debt (note: this may be necessary sometimes). Your codebase improves while delivering value to customers
  • Having a scout mindset as a team means that everyone gets better at both recognizing and refactoring code that could become a problem. Refactoring becomes a team skill that constantly gets better.

How to be a Great Scout?

Great, now I know the benefits of the scout rule, but what do I even look out for? Writing clean code is a skill to build over time with practice and guidance from mentors, but as a start, check out some common anti-patterns to look out for later in this page.

Testing

Refactoring is made much easier by having a great test suite to rely on to avoid regressions. The state of your test suite is a part of your codebase quality, so effective scouts update the tests when:

  • new functionality is added
  • old functionality is changed
  • when a bug is discovered

Writing effective, clean tests is another responsibility of code contributors.

Caveats

While the scout rule is widely recognized as an effective way to combat technical debt it is not a silver bullet.

  • There is a danger of discovering more and more spots in codebase to fix. Developers need to use their own judgement to decide how much of an investment is reasonable given the benefits of the refactor. The code should still be left improved in some way, but some parts may need to be left for the future.

    Tech debt that is discovered but not addressed should be documented and added in the backlog as dedicated work

  • You may want to avoid refactoring fragile code that doesn't have solid regression tests since you can't be sure your refactors didn't cause new bugs.

    In this case perhaps the scout thing to do is to write some tests so that someone in the future feels empowered to refactor!

Test Driven Development

In TDD the tests for a feature are written before the feature code itself. The feature is broken down into small, testable chunks, and each micro-feature is tested first, then coded up.

You can follow the principle of Red, Green, Refactor:

  • Red: the first step to feature development is to design a test and make sure it fails. Write tests that validate the input and output specified, tests that handle edge cases, invalid input etc.
  • Green: Write the minimal amount of code to satisfy the test.
  • Refactor: Revise your code. Make it more robust, maintainable, and easy to understand, and run your test again to make sure it's still green. Continue to refactor and run your test until you are satisfied with the code.

These steps are repeated for part of the feature.

Why Does it Work?

Let's start with the most obvious benefit: Test coverage

  • With TDD every feature gets tested before it is even written, so there should be 100% coverage of new features.
  • Following TDD means your code coverage will never decrease, a great motivator to get all members of the team to stay on top of testing.

TDD can improve your code design

  • By developing your feature in small testable chunks, TDD encourages modular programming, preventing large classes and methods for higher cohesion
  • Tests are easier to write with few dependencies and mock data, so TDD discourages your modules from becoming coupled to other modules
  • Developers are kept focused on writing the minimal code to satisfy the test requirements. This helps keep development focused and prevent extra, unnecessary code from being written

TDD can help your team understand the code better

  • Tests naturally document exactly what your tests should do; how they react in perfect and imperfect conditions.
  • Just taking a look at a well built test suite for a feature should give you teammates a solid understanding of features they didn't write themselves.
  • When writing the tests, you need to think about what the feature should actually do first, before getting wrapped up in implementation details

Pair Programming and TDD

This method can help ensure that tests aren't written to accommodate code, and leverage benefits of pair programming while letting both parties work in parallel

  • One developer writes the test, the other writes the code to satisfy the test
  • Both developers inspect the code and work on any refactoring
  • For the next test, the roles are reversed

Caveats

  • Tests can't catch all your bugs! Your tests can also contain bugs, or miss edge cases.
  • TDD may not serve its purpose if:
    • tests are not run frequently
    • too many tests are written at once
    • tests written are too large
  • TDD lends itself to unit testing, but integration and end-to-end tests are crucial to make sure your code delivers value to your users at the end of the day
  • Some types of development work may not be suited to TDD

Common Anti-patterns

Coupling

Coupling describes how much one module of the codebase relies on another. When two modules are "highly coupled" it means that the implementation of one module relies heavily on the implementation of another. This means when you change one module, you likely need to change another, and if you don't realize two modules are coupled, code changes could lead to mistakes.

  • "You can touch your friends, and you can touch your privates. But you can't touch your friends privates."
    • This is an old design saying illustrating coupling. Modules that rely on knowledge of other module's private implementation details are likely highly coupled.
  • Look for classes that "know a lot about each other", for example if class keeps referencing data in another class, it may mean reconstructing which class should hold that data.
  • Generally your interfaces should be unidirectional so look out for circular dependencies ie. Module1 calls some function in Module2 and Module2 calls some function in Module1.
  • Write some test cases for your modules. The mocks and fixtures needed from supporting modules can illustrate which dependencies your modules have. The testing phase can help surface exactly how much your class needs from other classes to function.

Code Duplication

The concept of DRY (Don't Repeat Yourself) is a great principle to follow when coding. Duplicate code means any future changes also need to be duplicated: this can be easy to forget when the code is revisited in the future.

  • Try abstracting repeated code into reusable functions, or creating a utility class.
  • If there is shared information that is duplicated in each place it is used (such as Status enum), consider consolidating duplicate constants into one place, then importing them where they need to be used.

A caveat to this principle: sometimes too much abstraction can be a problem.

  • Abstraction can create more overhead for future changes
  • The flow of logic can become harder to understand if code is broken up in different places
  • Sometimes it is unclear if two modules that use the same code will remain the same or if their shared functionality is a temporary coincidence.

Sometimes the benefits of abstraction don't outweigh the costs, each developer should use their own judgement when it comes to refactoring duplicate code (or any refactoring).

  • The Rule of 3 popularized by Martin Fowler can be a good metric to follow to keep code DRY but avoid unnecessary refactors
    • When you repeat something for the first time, cringe at repeating it, but don't refactor yet. Only refactor when the code is repeated for the third time.
  • A great article about how one time use extraction can hurt your codebase: https://www.cloudbees.com/blog/when-to-be-concerned-about-concerns

Large Methods and Large Classes

  • Smaller methods and smaller classes are easier to understand and test
  • Smaller methods can encourage reuse and avoid duplication
  • Keeping both methods and classes small keeps them focused (better cohesion)

How to take apart a large class:

  • Extract each piece of your large method or class into a new method or class that has a single responsibility

Unexpected Side Effects

A side effect in programming is anything that can modify the state of your application, for example modifying the DOM, an object, or writing to the database. Side effects are not necessarily bad... but can cause problems when they aren't documented clearly

  • Example: a program has a method called validatePassword()
    • This method validates the password by returning true or false. If the password is correct it also logs the user in. In this method a side effect is logging the user in
    • The name of the method isn't clear as to what the side effects could be, another developer could use this function thinking it only validates the password and not realize it also logs the user in
    • To solve this you could rename the function, or better - extract these two pieces of logic into separate functions validatePassword() and loginUser()
  • Unexpected side effects can mean your module has low cohesion; it's focusing on more than a singular purpose.

Again this is not an exhaustive list of all the possible code that could need refactoring, just some common things to keep in mind. Please contribute to this section if you feel there is an important part missing.

Resources

Scout Rule

Cohesion and Coupling

Common Anti-patterns

Monolithic Architecture

Rule of 3

Side Effects

Test Driven Development

Technical Debt

Would you like to join us and to continue to iterate on our approach to software and delivering big results for clients? If so, take a moment to review our open positions!

About the author

Grace Geng

Grace has held a number of software development roles across a variety of companies. She is a leader in clean code and implementing systems and processes to support top notch development teams.