Essential coding principles for better software

Aug 28, 2023

In the vast universe of programming, there are principles that, although not commandments, provide great guidance for those who aspire to write clean, maintainable, and efficient code. These principles not only represent best practices but are also the fruits of years of experience and learning from the developer community. Below, we’ll delve deeper into some of these principles.

DRY (Don’t Repeat Yourself)

Repetition is the downfall of the programmer.

This statement encapsulates the essence of the DRY principle - Don’t Repeat Yourself. It aims to reduce duplicities in our code as repetition leads to errors, inefficiencies, and difficulties in software creation.

Motivation

  • Maintenance: If logic is repeated in multiple places, any change or correction to that logic has to be made in all those places. This not only consumes more time but also increases the chance of errors.
  • Efficiency: Code reusability leads to smaller programs, as redundancies are eliminated.
  • Readability: Code without repetition is easier to read and understand; repetitions can distract and confuse the reader.

Common violations of the DRY principle include practices like repeatedly copying and pasting code, implementing functions or methods that execute very similar or identical operations, and designing classes with notably similar structures.

Example


    # Incorrect approach
    def calculate_rectangle_area(width, height):
        return width * height
    
    def calculate_circle_area(radius):
        return 3.14 * radius * radius
    
    # Using DRY Principle
    class Shape:
        def area(self):
            pass
    
    class Rectangle(Shape):
        def __init__(self, width, height):
            self.width = width
            self.height = height
    
        def area(self):
            return self.width * self.height
    
    class Circle(Shape):
        def __init__(self, radius):
            self.radius = radius
    
        def area(self):
            return 3.14 * self.radius * self.radius
            

Practical Tip

Abstracting to avoid repetition is crucial, but you wouldn’t want to overcomplicate matters. Before abstracting, question whether the abstraction muddles the code or if it remains comprehensible. At times, a tad repetition is preferable if it aids clarity.

KISS (Keep It Simple, Stupid)

Simple is better than complex, and complex is better than complicated.

This is a variant of the philosophy behind KISS, a principle that champions simplicity in design and code over intricate and convoluted solutions. Simplicity is a desirable aim in software development, leading to cleaner code that’s easy to understand and maintain.

Motivation

  • Ease of Maintenance: Simple codes are more straightforward to maintain. Corrections, enhancements, or extensions can be undertaken with less effort and a reduced risk of errors.
  • Readability: Simple code is more effortless to read and comprehend, enabling other developers or even oneself in the future to get acquainted with it swiftly.
  • Reduced Error Potential: Lower complexity typically equates to fewer error chances. Simple solutions often have fewer edge cases and exceptions to consider.

The key to implementing KISS is constant reflection and self-evaluation. Every time you’re about to write or design something, ask yourself: “Is there a simpler way to do this?”.

Example

Suppose you want to filter even numbers from a list:


    # Overboard Approach
    def rectangle_area(width, height):
        return width * height

    def circle_area(radius):
        return 3.14159 * radius * radius

    # And so on for each shape...
    

A simpler and scalable solution uses classes and polymorphism:


    # Using the KISS principle
    class Shape:
        def area(self):
            pass

    class Rectangle(Shape):
        def __init__(self, width, height):
            self.width = width
            self.height = height

        def area(self):
            return self.width * self.height

    class Circle(Shape):
        def __init__(self, radius):
            self.radius = radius

        def area(self):
            return 3.14159 * self.radius * self.radius
            

Practical Tip

With this class-based approach, you have a cleaner structure that allows adding new shapes without needing to craft a new function for each one. It’s a design simplification rather than a code reduction. Practical Tip

Don’t be enticed by technical elegance or “smart” solutions. Often, the most direct and simple method is superior. When facing a problem, before diving into the code, take a moment to ponder and plan. Remember that what seems clear and straightforward to you now might not be for someone else (or even yourself) in the future.

YAGNI (You Aren’t Gonna Need It)

The best code is the code you never write.

YAGNI, or You Aren’t Gonna Need It, is a principle that encourages developers to only implement functionality when it is necessary. It warns against over-engineering or adding features based on speculation about future needs.

Motivation

  • Simplicity: By avoiding premature optimization or unnecessary features, you ensure your codebase remains clean and to the point.
  • Efficiency: Writing only what’s required means less code to test, debug, and maintain, which saves both time and resources.
  • Reduction in waste: Unneeded features or over-complicated solutions not only bloat the software but may also never get used, making them a waste of effort.

Many developers fall into the trap of anticipating every potential future requirement, leading to bloated and over-engineered solutions. YAGNI suggests that you should only add functionality when there’s a definitive requirement for it.

Example

Suppose you’re creating a blogging platform. While a tagging system or advanced search might be useful in the future, if the current requirements only specify basic blogging features, YAGNI would advise against implementing them at the outset.


  # Over-Engineered Approach
  class Blog:
    def __init__(self):
        self.posts = []
        self.tags = []
        self.advanced_search = []

  # YAGNI Principle
  class Blog:
    def __init__(self):
        self.posts = []
        

Practical Tip

Always prioritize current, concrete requirements over speculative ones. When designing or coding, ask yourself, “Do I need this now? Is there a clear requirement for this?” If the answer is “no” or “not sure,” consider postponing that feature or functionality. Avoid the temptation to add “just in case” code. Remember, you can always add more later when it’s truly needed.

Separation of Concerns

Divide and conquer.

This age-old adage holds profound meaning in software design, and the Separation of Concerns is its standard-bearer. This principle urges us to break down a program into distinct parts, where each has a uniquely and clearly defined responsibility.

Motivation

  • Maintainability: It simplifies updates and fixes. If each software piece has a unique purpose, modifying one function shouldn’t impact other program areas.
  • Readability and Organization: By maintaining separated responsibilities, code becomes easier to read and navigate, facilitating both the onboarding of new team members and collaborative work.
  • Reusability: Autonomous individual components can be reused in different parts of the project or even in other projects.
  • Testability: It’s easier and quicker to test a specific functionality when it’s isolated from others, leading to more robust tests and faster error identification.

Always bear in mind the singularity of responsibility and think of software in terms of autonomous modules or components. If a function or class has more than one responsibility, it probably needs to be broken down into smaller pieces. It’s not just a best practice; it’s a design philosophy that should be a priority in any software project since it enhances code quality, efficiency, and scalability.

Example

Imagine a web application involving a database, a user interface, and a server:


    # Incorrect Approach
    class App:
        def get_user_data(self, user_id):
            # Fetch from database
            pass
        def render_user_page(self, user_data):
            # Render user page
            pass
        def handle_request(self, request):
            # Handle web request
            pass
    
    # Correct Approach
    class Database:
        def get_user_data(self, user_id):
            # Fetch from database
            pass
    
    class UserInterface:
        def render_user_page(self, user_data):
            # Render user page
            pass
    
    class Server:
        def handle_request(self, request, db, ui):
            user_data = db.get_user_data(request.user_id)
            ui.render_user_page(user_data)
            

Practical Tip

Imagine you’re solving a puzzle; if you mix pieces from different puzzles, it becomes extremely challenging, if not impossible, to complete the picture. Similarly, in software development, each component or function should be like a unique puzzle piece with a defined purpose and shape. Keep your components and functions focused on a single task or responsibility. By doing so, not only will it be easier for you to identify and resolve issues, but you’ll also be able to reuse, test, and understand your code much more easily. When a component or function begins to handle multiple tasks, it’s time to pause, reevaluate, and likely split it.

Law of Demeter

The Law of Demeter, often referred to as the Principle of Least Knowledge, is rooted in the idea of reducing dependencies between modules or classes in software. Essentially, it suggests that each unit should have limited knowledge about other units and should only interact with its “close friends.”

Motivation

  • Reduced Coupling: Less interdependence between classes means simpler changes and fewer chances of unintended errors.
  • Increased Maintainability: With fewer links between classes, it’s easier to make changes in the code without fear of unwanted side effects.
  • Greater Modularity: This facilitates code reuse, as classes become more autonomous.
  • Testability: Less coupled classes are easier to test due to fewer external dependencies.

The Law of Demeter is violated when navigating through multiple objects accessing chained methods, like a.b().c(), when initializing objects in a class that should be injected as dependencies, or when a class interacts with several others to retrieve data or execute functions. The benefits in terms of code clarity and structure are invaluable. As with all design principles, the key is to apply it with judgment and balance.

Example

Imagine a case where a driver wants to start a car:


    # Incorrect Approach
    class Engine:
        class SparkPlug:
            def ignite(self):
                pass
    
    class Car:
        def __init__(self):
            self.engine = Engine()
    
        def start(self):
            self.engine.SparkPlug().ignite()
    
    # Correct Approach
    class Engine:
        def start(self):
            spark_plug = SparkPlug()
            spark_plug.ignite()
    
    class SparkPlug:
        def ignite(self):
            pass
    
    class Car:
        def __init__(self):
            self.engine = Engine()
    
        def start(self):
            self.engine.start()
            

In the correct approach, Car doesn’t need to know about SparkPlug. It just knows that the engine has a start() method. The internal implementation of how the engine starts is encapsulated within the Engine class.

Practical Tip

Think of the Law of Demeter as an instruction manual for a device. If you need to perform a specific function on it, the manual directs you to a direct button or command to carry out that action. It doesn’t tell you to press a button that then takes you to another panel, where you turn a knob to then take you somewhere else to finally perform the function. In programming, this principle suggests we interact directly with the components we need, rather than chaining multiple actions through different modules.

Principle of Least Astonishment

Users, both developers and end-users, expect software components, functions, or modules to operate in a specific manner. The Principle of Least Astonishment argues that the behavior of any piece of software shouldn’t astonish or confuse its users. Instead, it should be clear, consistent, and predictable.

When a user interacts with a system, they already have certain expectations of how it should function based on past experiences or established conventions. An application behaving in a manner that contradicts those expectations can lead to frustration and errors.

Motivation

  • Intuitiveness: When software behaves as expected, users can interact with it more intuitively, without having to spend time and energy trying to understand why certain things happen.
  • Trust: Predictable behavior strengthens user trust in the software. If a user knows the software will consistently behave without surprises, they’re more likely to trust it for critical tasks.
  • Development Efficiency: Reducing surprises means developers spend less time addressing unexpected problems or explaining unconventional features. This allows for a smoother and more efficient development process.
  • User Adoption: Users tend to adopt and recommend software that “just works” according to their expectations. This can lead to greater customer satisfaction and broader product adoption.

Example

In many systems, clicking on the “X” in one of the top corners of a window will close it. If instead, clicking on that " X" opened a new window or deleted a file, it would counteract the user’s expectation and violate the Principle of Least Astonishment.


    # Wrong
    def saveFile(file):
        # logic to save the file
        sendEmail()
        deleteAnotherFile()

    # Right
    def saveFile(file):
        # logic to save the file
        pass
        

In the first example, besides saving the file, two other operations are carried out which would not be expected based on the function’s name. These additional actions are unexpected and could be harmful, especially if the user is unaware of them.

Practical Tip

Imagine reading a book and, out of the blue, in the middle of a detective story, a fire-breathing dragon appears. This sudden transition causes astonishment and confusion. The same applies to software development. When designing a function or component, ensure its actions are intuitive and expected. Maintain consistency and avoid “unexpected surprises” in your design. Clarity and predictability are essential for robust and reliable software.

Fail Fast

The Fail-Fast principle suggests that a system or application should identify error conditions or anomalies as soon as they occur, halt its operation, and immediately report the problem. The idea is that it’s much better to detect issues early on and address them right away, rather than allowing a system to continue operating in a potentially unstable state.

This principle is foundational in many aspects of programming, from algorithm design to the development of entire systems. By spotting and handling errors as soon as they arise, one can prevent undesirable consequences later in the process, such as inaccurate computations, data corruption, or even system failures.

Applying this principle not only aids in building more robust and reliable systems but also makes it easier to detect and fix errors during development and testing phases. This can save significant time and resources compared to rectifying problems after a product has been launched or deployed.

Moreover, by providing immediate feedback about issues, the user or developer experience is enhanced, as they can address and resolve problems promptly.

Motivation

  • Risk Prevention: Failing fast stops errors from propagating throughout the system, averting potential further damage or data corruption. A system that fails late might impact many other areas, complicating the identification and correction of the original problem.
  • System Trust: A system that promptly reports errors is perceived as transparent. Users and developers can have greater trust in a system that immediately informs them of problems rather than one that might be hiding or ignoring failures.
  • Continuous Improvement: The fail-fast principle isn’t just about error detection but also about learning from those errors. When mistakes are promptly spotted and addressed, it fosters a culture of continuous improvement, where errors are viewed as learning opportunities and not just failures to rectify.

Example

Imagine a function that carries out a division operation. Instead of letting the operation proceed with a divisor of zero (resulting in an error), it’s better to check for this condition and generate an error right away, notifying the user or developer about the problem.


    # Incorrect
    def divide_incorrect(a, b):
        if b == 0:
            return 0  # Returns a default value without signaling the error
        return a / b
    
    # Correct
    def divide(a, b):
        if b == 0:
            raise ValueError("The divisor cannot be zero.")
        return a / b
        

Practical Tip

Imagine pouring water into a strainer thinking it’s a glass; the sooner you realize the mistake, the less water you’ll waste. Similarly, in software development, when something goes awry, it’s crucial to notice and act immediately. If your code detects an abnormal condition or a potential error, don’t ignore or conceal it; make your system signal it right away. Adopt a fail-fast mindset so that your programs inform you and not the other way around. A system that fails quickly and informatively cares about its integrity and its users'.

Summary

Over the years, and with accumulated experience in software development, a series of principles and practices have been established to act as guides for producing high-quality code. These are not merely rules or standards to abide by but a distillation of collective knowledge for designing and implementing robust, maintainable, and effective software. Applying these principles reflects the developer’s commitment to producing software that not only works but is also enduring and valuable for its users. On the path to mastering programming, these principles are the beacons that guide us. However, it’s essential to remember that an overly rigid or excessive application of these can be counterproductive.

Related posts

That may interest you