How to keep your code elegant, readable and maintainable

·

7 min read

In the world of software development, simplicity and elegance are highly valued qualities in code. Writing elegant code requires thoughtful design and attention to detail. In this article, we will explore some key principles and practices for achieving this.

Note, before we go on, I do understand that larger organisations have their own internal guidelines for writing clean code. If you're one of those individuals who work for one of those organisations, you should know that this article is not a replacement for those guidelines but rather a general idea of how a clean code base should be written.

Here is a list of all the points we will be discussing in this article:

  1. Use Descriptive Names

  2. Keep Functions and Methods Small and Focused

  3. Avoid nested if statements

  4. Avoid undescriptive chaining conditions in if statements

  5. Write Readable and Self-Documenting Code

  6. Eliminate Code Duplication (kinda)

  7. Properly structure the project directories

  8. Write Unit Tests

  1. Use Descriptive Names: Choosing clear and descriptive names for variables, functions, and classes is essential for writing elegant code. Names should accurately reflect the purpose and functionality of variables and functions. Avoid cryptic abbreviations or acronyms that may confuse other developers. Intention-revealing names make the code self-explanatory and reduce the need for comments. For an in-depth discussion, guidelines and examples on variable naming, read my article The Art of Naming Variables: Avoiding ambiguity and Promoting Clarity.

  2. Keep Functions and Methods Small and Focused: One of the fundamental principles of writing simple and elegant code is to keep functions and methods small and focused. Aim for functions that perform a single task and have a clear purpose. Breaking down complex logic into smaller, more manageable pieces. Below is an example of taking user input and running a sorting algorithm:

     def sort_array(arr):
         if arr:
             if len(arr) > 1:
                 algorithm = input("Enter the sorting algorithm (bubble, insertion, quick): ")
                 match algorithm:
                     case "bubble":
                         # bubble sort logic
                     case "insertion":
                         # insertion sort logic
                     case "quick":
                         # quick sort logic
                     case _:
                         print("Invalid sorting algorithm.")
             else:
                 print("The array has only one element.")
         else:
             print("The array is empty.")
    

    Although the above does work, it's doing too many things, such as array validation, getting user input, writing out each sorting logic inside each of the switch cases and using string values for each switch case. All these things make the function complex and difficult to debug. Instead, we should break down this larger function into smaller ones where each function will have its own responsibility:

     class SortEnum(Enum):
         BUBBLE = "bubble"
         INSERTION = "insertion"
         QUICK = "quick"
    
     def bubble_sort(arr):
         # Bubble sort implementation
         pass
    
     def insertion_sort(arr):
         # Insertion sort implementation
         pass
    
     def quick_sort(arr):
         # Quicksort implementation
         pass
    
     def validate_array(arr):
         if not arr:
             print("The array is empty.")
             return False
         if len(arr) <= 1:
             print("The array has only one element.")
             return False
         return True
    
     def get_sorting_algorithm():
         algorithm = input("Enter the sorting algorithm (bubble, insertion, quick): ")
         return algorithm
    
     def sort_array(arr):
         if validate_array(arr):
             algorithm = get_sorting_algorithm()
             match algorithm:
                 case SortEnum.BUBBLE:
                     bubble_sort(arr)
                 case SortEnum.INSERTION:
                     insertion_sort(arr)
                 case SortEnum.QUICK:
                     quick_sort(arr)
                 case _:
                     print("Invalid sorting algorithm.")
    

    Although separating the function into multiple functions like this increases the number of lines of code. However, it is now much easier to extend functionality and debug any issues that may arise in the future.

    Note that in a real code base, the enum class and some of the functions should be in different files.

  3. Avoid nested if statements: Nested if statements can quickly lead to code complexity, making it harder to understand the logic flow and increasing the chances of introducing bugs during development or future modifications. As the number of nested if statements increases, the code becomes more convoluted, and it becomes difficult to track which condition corresponds to which block of code; this is especially true for languages such as Python that rely on indentations. This can lead to a phenomenon known as the "arrowhead anti-pattern," where code branches out into an arrowhead shape, making it challenging to follow the program's logic. For example, Suppose you are developing a software application that processes customer orders. You want to validate the order before processing it. Here's an example of deeply nested if statements:

     def process_order(order):
         if order.is_valid():
             if order.has_sufficient_inventory():
                 if order.has_valid_payment():
                     order.process()
                 else:
                     print("Invalid payment")
             else:
                 print("Insufficient inventory")
         else:
             print("Invalid order")
    

    In this example, as the validation steps increase or become more complex, the logic can be very hard to follow. A better approach is to use a guard clause or early exit strategy to avoid excessive nesting. Here's an improved version using guard clauses:

     def process_order(order):
         if not order.is_valid():
             print("Invalid order")
             return
    
         if not order.has_sufficient_inventory():
             print("Insufficient inventory")
             return
    
         if not order.has_valid_payment():
             print("Invalid payment")
             return
    
         order.process()
    
  4. Avoid undescriptive chaining conditions in if statements: When conditions are chained together with logical operators such as and or or, the code can quickly become difficult to comprehend. The below example needs to do 3 checks before the user can edit it:

     # check user can edit
     if user.role == RoleEnum.ADMIN and 
        job.status == JobStatusEnum.OPEN and 
        job.type == JobTypeEnum.CONTRACT:
             pass
    

    The above is what is typically seen in a code base, although there is a comment telling the reader what this check is for, we can instead assign a variable to the condition:

     can_edit = user.role == RoleEnum.ADMIN and 
        job.status == JobStatusEnum.OPEN and 
        job.type == JobTypeEnum.CONTRACT
    
     if can_edit:
         pass
    

    This makes the code much easier to comprehend at a glance.

  5. Write Readable and Self-Documenting Code: Strive to write code that is self-explanatory and easy to understand by implementing the above points. Well-designed code should read like a narrative, making the logic and flow clear to other developers without the use of comments. Use consistent formatting, indentation, and spacing to enhance readability. In saying this, do use comments where needed, such as classes or functions that require domain knowledge.

  6. Eliminate Code Duplication (kinda): Code duplication not only increases the chances of errors but also makes code harder to read and maintain. Eliminating duplication through code reuse and abstractions. Identify repeated patterns or functionality and extract them into reusable functions, modules, or classes. What I am referring to here is the "Don't repeat yourself" (DRY) principle. But one does need to understand how to use DRY properly rather than using it everywhere. Hence the "Avoid hasty abstraction" (AHA) programming principle is gaining popularity. It remains the developers that it is ok to not DRY everything in the codebase, but rather use DRY where appropriate; this article summarises this principle.

  7. Properly structure the project directories: A well-designed project directory provides several benefits. Firstly, it improves code maintainability by creating a logical separation of different modules. Secondly, a clear folder fosters a better understanding of the project's architecture and reduces the risk of code duplication. Team members can quickly find what they need and grasp the project's overall layout. Lastly, a structured folder hierarchy simplifies version control management, as it allows developers to track changes systematically. For a deeper discussion and examples of folder structures, have a read of my articles Express.js: The dangers of unopinionated frameworks and best practices for building web applications and Here is how you write scalable and maintainable React components for projects of any size.

  8. Write Unit Tests: Unit testing is a very important part of the software development lifecycle. Writing tests not only verify the correctness of the code but also act as living documentation and provide confidence during refactoring or modifying code. It is important that we not only test the "happy" path but more importantly, the "un-happy" scenarios. We as developers are usually very lenient when it comes to testing, so we need to test all scenarios the users could face. But in saying this, there is a balance to writing the appropriate amount of tests, you don't want to overdo it as this will result in increased dev time for writing and maintaining the tests.

In conclusion, writing simple and elegant code is a goal worth pursuing in software development. By embracing these principles and strive for simplicity and elegance in your codebase to reap the long-term benefits.