DEV Community

Zac Jiang
Zac Jiang

Posted on • Updated on

How to keep your code elegant, readable and maintainable

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

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

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.")
Enter fullscreen mode Exit fullscreen mode

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.")

Enter fullscreen mode Exit fullscreen mode

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.

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")
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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.

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.

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.

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.

Top comments (0)