Photo by Mark Duffel on Unsplash

The Clean Coder Golden Rules

by

When writing professional code, it is crucial to adhere to best practices, especially when transitioning from different programming languages or working under time constraints. This article aims to serve as a comprehensive guide to ensure that your applications are not only secure but also easily maintainable by future developers.

This article will be periodically updated, so it is advisable to bookmark it for future reference. Without further delay, let's delve into the essential guidelines.

1. Encapsulate Primitive Types and Strings

One fundamental rule in clean code development is to avoid the direct use of primitive types. Instead, embrace the concept of encapsulation by using custom types or objects.

By doing so, you can enforce validation rules that not only prevent potential issues but also ensure data integrity. This practice aligns with Rule #3 of Object Calisthenics by Jeff Bay.

Never place blind trust in user inputs, data from APIs, databases, or any form of external input without first converting it into trusted objects or custom types. Always employ factories and constructors when handling raw, untrusted data to ensure that you have control over its validation and integrity.

For instance, when dealing with custom strings, consider the following approach:

For custom numeric types, like floats, you can implement a constructor with additional validation checks:

2. Utilize a Proper Software Architecture

Regardless of whether you opt for Clean Architecture or prefer a simpler pattern such as Model-View-Controller (MVC), always employ a well-defined architecture that promotes code maintainability.

Avoid relying solely on the default file structure provided by your framework, as this can lead to complications when you need to adapt to changing frameworks or perform upgrades.

An architecture that encourages dependency injection and repository patterns is often the best choice for ensuring the longevity of your code, which may need to remain viable for several years.

3. Grasping the Essence: What, How, and Why

Before you even consider delving into the code, it's absolutely crucial to have a complete grasp of the problem and its solution. One must comprehend what the software is designed to accomplish, the manner in which it achieves these goals, and fundamentally, the reasons behind the functionalities—both in terms of what it does and how it accomplishes these tasks.

Let's start with the 'what'—arguably the simplest aspect. Set aside the source code for a moment and direct your attention to the endpoints. Investigate what the software is capable of, its requirements, and its outputs. Test it thoroughly, exploring each endpoint and every conceivable use case. Put yourself in the user’s shoes. It’s quite surprising how many developers never experience their own creations from a user’s perspective, even briefly. The most effective way to understand the user is to become one.

In instances where the software is still in the conceptual stage, jot down the intended purpose for each endpoint. Outline what it should accomplish, determine the necessary inputs from the user, and what you plan to deliver in return. When I begin coding, my initial step is to articulate the purpose of my method or class. I don't start with code; instead, I describe the mission in straightforward English.

Next, we venture into the 'how.' If the software is already in existence, immerse yourself in the source code, from the top down. Understand the flow of information, its manipulation, storage, and the APIs in play. This stage is undoubtedly challenging and may raise numerous questions, especially if the code is technically complex. Don’t fret—just note down any uncertainties and ideas for the time being.

If you're initiating the coding process, translate these method missions into sub-steps, yet still in plain English. Sketch out the steps you believe are necessary to fulfill the mission. Then, start coding, tackling each step methodically. Your code should mirror the step descriptions seamlessly, making comments redundant. The intent of the code should be self-evident, unmistakably clear.

The final piece of the puzzle is the 'why.' This involves understanding the rationale behind the software’s actions and the reasons for its particular development approach. Review your notes. You might have observed redundant information requests, missing authorizations in routes, or code segments that are paradoxical or unnecessarily complex. Don’t hesitate to reach out to previous developers or product managers to clarify any doubts.

Remember, programming is fundamentally about automation. However, before you embark on coding, it’s imperative to gain a deep understanding of your product, the underlying technology, the problem at hand, and how your solution is intended to function. Start by manually solving the problem to fully appreciate not only what needs to be done but also the best way to achieve it. Be the user.

Diving into automation without fully understanding the process can lead to substantial challenges, squandering both time and effort. When confronted with unfamiliar concepts, it’s worthwhile to invest a few moments in educational resources—perhaps a quick tutorial on YouTube or a chat with an AI assistant—to bolster your comprehension.

4. KISS, YAGNI and DRY

When discussing clean code, we can't ignore these three essential principles. They work together to stress the importance of avoiding overcomplication and unnecessary repetition.

"Keep It Simple Stupid" (KISS): Complexity should be avoided. If your code needs an elaborate explanation or numerous comments, it's a sign that something's amiss. Clean code should be easy to read and understand.

"You Ain't Gonna Need It" (YAGNI): Write code only when it's needed for the current requirements. Future-proofing should rely on sound design, not on adding features that aren't currently in use. Don't try to solve problems that don't exist by adding methods and fields that aren't used.

"Don't Repeat Yourself" (DRY): Avoid writing the same code multiple times. When you notice duplication, consider converting it into a helper method or a separate class, depending on the situation.

While striving to eliminate redundancy, be careful not to create overly generic functions with long return values or unnecessary complexity. Refer to the "Strike the Right Balance in Verbosity" rule to understand when and how to use DRY effectively.

To implement these principles effectively, consider adopting a "build-measure-learn" (BML) feedback loop. This iterative approach suggests writing new code only in response to actual project needs.

In the coding process, it's normal to write something and realize shortly after that it needs revision. Embrace this natural part of coding. The act of writing code is straightforward; the real challenge lies in making the logic clear and dependable. Avoid trying to predict the requirements of your next method or task; instead, proceed step by step.

Continuously assess whether the logic you're following is the most straightforward way to achieve the desired outcome. Only when you've confirmed that your automation works, performs efficiently, and maintains readability should you consider splitting the code into multiple methods or functions. This incremental approach ensures that your code evolves in a structured and reliable manner.

5. Strike the Right Balance in Verbosity

Clean code isn't about being terse. I'd choose a code with five methods that I can easily understand over a shorter, two-method version that leaves me scratching my head. The key here is finding the right level of verbosity. You want your code to be verbose enough for the next developer to grasp, but not so verbose that it becomes a puzzle.

The approach can be broken down into two steps. First, go all out with verbosity in the initial stages of your code. Dump your pseudo-code and establish the logical flow without holding back. Then, after you've laid out the groundwork, go back and see if any parts of the code can be broken out into auxiliary methods or merged with other sections.

Only move code into auxiliary methods if the main method exceeds 100~150 lines or if that functionality is needed elsewhere. Resist the urge to overcomplicate or embellish unnecessarily. If another class ends up needing this auxiliary method, then you can consider promoting it to a standalone helper function. Be patient and responsive—wait for the actual need to arise.

6. Use Conditionals Strategically

Avoid enclosing your entire code within a single massive if block. The primary flow of your code should be clearly evident from the start, with minimal nesting. Conditionals should be reserved for specific scenarios where the standard flow doesn't suffice, and you need to incorporate additional steps.

If is fine, else is evil. Instead of relying heavily on if-else constructs, embrace alternatives like early returns or switch statements. else statements can lead to convoluted and difficult-to-follow code, which can become a maintenance nightmare. Remember that code is read more often than it is written.

7. Maintaining Code Cohesion

Every developer brings their unique coding style to the table. However, having three different ways to accomplish the same task scattered throughout your codebase or a mix of PascalCase and snake_case in different classes can lead to chaos.

Ensuring code cohesion is a fundamental practice. While there may be diverse coding styles in play, it's prudent to adhere to the prevailing style and conventions set by previous developers. However, this doesn't mean blindly perpetuating errors. If you encounter a mistake, rectify it in the new code before revisiting and correcting the same issue in the existing codebase, thus maintaining a sense of consistency.

Another aspect of cohesion often overlooked involves the organization of methods. In most programming languages, auxiliary methods should precede the main method. This convention harks back to the early days of computing when procedural programming was the norm.

While there may be instances where you need to reorganize the natural order of methods, like when creating classes where the constructor should be the first method, such reordering should be undertaken solely to enhance readability or adhere to well-established conventions.

8. The Three-Step Code Verification

The process of triple-checking your code is an invaluable practice that ensures its quality and reduces common errors. Let's break it down into three distinct phases:

1) Initial Review: Before making any changes to the code, the first check is a mental one. Read the code aloud and immerse yourself in the methods and logic. The goal here is to gain a clear understanding of what the code does, how it accomplishes its task, and why it's structured in a particular way.

2) Ongoing Validation: The second check occurs after you've made your initial code adjustments. Instead of waiting until the entire refactoring or coding process is complete, perform this check after each sub-task is finished. Once again, read the code aloud, reinforcing your grasp of the code's purpose and how it all fits together. If everything aligns as intended, proceed with your commit. Repeat this process as you work through different parts of your code.

3) Pre-Pull Request Assurance: Upon completing the entire task and preparing to submit a pull request, take a brief five-minute break. Before seeking feedback from your colleagues, invest time in reviewing your own work. Address common errors such as grammatical mistakes, deviations from coding conventions, or overlooking linting and error checking. These errors are often avoidable and can be indicative of fatigue. By addressing them yourself, you save your potentially tired and overworked colleagues from dealing with avoidable issues.

9. Limit Your Code Sessions

Avoid the temptation to code for extended hours without taking breaks. While it may seem like you're being highly productive, the reality is that prolonged coding sessions often lead to overlooking crucial details, necessitating extensive code revisions later on.

Consider adopting time management techniques like the Pomodoro method. This approach allows you to maintain a sharp focus and a clear mind throughout your work. When you encounter challenges, resist the urge to spend excessive time (more than 45 minutes) on a single problem. Instead, transition to the next task or seek assistance from a colleague.

10. Embrace Collaboration and Seek Guidance

No matter your level of expertise, there will come a time when you find yourself uncertain about the clarity of your code or the suitability of a particular solution. It's important to understand that having doubts is not a sign of weakness; rather, it signifies your willingness to step outside your comfort zone and enhance your skills.

Remember that your colleagues on the team are not adversaries; they share the same ultimate goal as you do - achieving success as a team. When you encounter questions that don't have readily available answers, reach out to your peers for their input. Encourage them to provide feedback on your thought process. In essence, be a team player.

Avoid making unilateral decisions when deviating from the group's initial plan. Instead, openly communicate the challenges you're facing and the potential solutions you're considering. Maintain an open mind, be receptive to different viewpoints. It's highly probable that you'll receive valuable feedback that can make your code more readable or even improve its performance.

11. Test and Monitor

In software development, one undeniable truth prevails: things break. However, it's far more advantageous for them to break during the initial stages of development rather than in front of your users. Therefore, incorporating unit and integration tests is not merely recommended; it's an absolute necessity. If possible, extend your testing suite to include end-to-end tests for comprehensive coverage.

The time invested in creating robust tests pays off manifold. It ensures code consistency and mitigates the need for extended debugging sessions. This approach is a win-win scenario from every perspective.

Vigilance doesn't end with code deployment. Once your application is in production, maintain a watchful eye. Monitor response times, scrutinize error logs, and analyze access patterns.

Recognize that your application may eventually encounter misuse or exhibit unforeseen behavior. Waiting for users to report issues is an inadequate approach to user experience and can jeopardize your client base. Proactive monitoring is the key to preemptively addressing potential problems and ensuring a seamless user experience.

12. Add Informative Logs and Responses

When it comes to debugging applications, it often involves asking users how to recreate issues and sprinkling in a fair share of temporary "console.log" and "var_dump" statements. But with so many print functions, it's all too easy for someone to forget them in the code, inadvertently exposing sensitive data.

To avoid this predicament, consider using logs to record useful errors and providing users with concise yet informative messages. For instance, if your application's database goes offline, delivering default error messages like "Error connecting to server on socket X" isn't very user-friendly and potentially reveal information about your database to attackers.

Instead, opt for a message like "DatabaseConnectionError." It's user-friendly, letting API consumers know it's not an issue with their payload. Plus, it's easy to translate on the user interface without revealing too many unnecessary details. You definitely want to steer clear of displaying a full-blown stack trace when errors occur. That's like serving a feast to potential attackers.

This tip comes in particularly handy when working with factories within a for loop, for example. You might use a "continue" to skip an iteration when something goes wrong without logging why. Then a user reports having X items, but the application only shows half. You can still use "continue" to keep the flow going, but make sure you log the error with enough information for you or the operations team to easily figure out what's happening.

13. Shift Away from an Exception-Driven Mindset

If you come from a background in web development-focused languages, you may be accustomed to using try-catch and exceptions extensively. However, newer languages tend to be designed for concurrent and multi-threaded programming, thus the use of panics and exceptions is discouraged for several reasons:

a) Predictable Control Flow: Panics and exceptions can lead to non-deterministic program behavior in multi-threaded environments, making it hard to understand and reason about program execution.

b) Resource Management: Avoiding panics helps ensure proper resource cleanup, preventing resource leaks like memory or network connections.

c) Concurrency Safety: Panics can disrupt the safe and explicit communication model between co-routines, potentially introducing unexpected behavior.

d) Code Readability and Debugging: Panics can make code harder to read and debug, especially in multi-threaded contexts.

e) Performance: Exception handling can introduce overhead in time and memory usage, which can be problematic in performance-sensitive systems.

f) Philosophy of Error Handling: Modern languages promote explicit error handling as a core principle, treating errors as values rather than exceptional events. This approach enhances code clarity and maintainability.

It is advisable to handle errors gracefully using "if err != nil" statements, which may appear verbose but align with the idiomatic way of error handling in modern languages. With practice, you will become accustomed to this approach and benefit from its clarity and maintainability.

14. Caution with Type Assertions

When dealing with raw data, it's often necessary to assert its data type and subsequently transform it into a trusted format. Always begin by clearly labeling the raw data with a prefix that signifies its nature.

Furthermore, ensure that you validate these type assertions before proceeding to further operations. This practice enhances code clarity and minimizes the risk of unexpected data mismatches or type-related errors.

The approach to checking for the existence of a key and the type of its associated value can vary depending on the programming language in use. In our example, we are utilizing Go, a language that inherently supports multiple return values from a single function. As a result, verifying both the existence of a key and the type of its value is pretty straightforward.

Comments