DEV Community

Anusha Renangi
Anusha Renangi

Posted on

Software Design Principles Every Developer Should Know: SOLID, DRY, KISS, and YAGNI

When we write code, our first goal is usually to make it work.

But what happens when requirements change?

Initially, our code might work perfectly. But as the application grows, even a small change can become difficult if the code is poorly structured.

So, writing code that works is important, but writing code that is easy to understand and maintain is equally important.

While learning software design, I came across a few principles that help us write better code.

I have already written about the SOLID principles with real-life examples.

You can read that article here:

SOLID Principles Explained Through Real-Life Examples

In this article, I'll cover three more principles:

  1. DRY: Don't Repeat Yourself
  2. KISS: Keep It Simple, Stupid
  3. YAGNI: You Aren't Gonna Need It

1. DRY Principle (Don't Repeat Yourself)

DRY stands for Don't Repeat Yourself.

The idea is to avoid writing the same logic in multiple places.

The Problem: Repeating the Same Logic

Suppose we are developing an online shopping application and need to calculate the final price after applying a discount.

price = 1000
discount = price * 0.10
final_price = price - discount

print(final_price)
Enter fullscreen mode Exit fullscreen mode

Now, suppose we need the same calculation on another page. We write the same logic again.

If the discount changes, we must update both places. If we forget one, the application might calculate different prices.

Applying DRY

Instead, we can create a reusable function.

def calculate_final_price(price):
    discount = price * 0.10
    return price - discount


print(calculate_final_price(1000))
print(calculate_final_price(2000))
Enter fullscreen mode Exit fullscreen mode

Now, the calculation is written in one place and can be reused wherever needed.

Use Cases of DRY

1. Reusing Common Logic

Suppose we need to check whether a person is an adult in different parts of an application.

Instead of writing the same condition repeatedly, we can create a reusable function.

def is_adult(age):
    return age >= 18


print(is_adult(22))
print(is_adult(16))
Enter fullscreen mode Exit fullscreen mode

Now, we can call is_adult() wherever we need to check a person's age.

2. Sharing Common Functionality Through Inheritance

Suppose we have two types of employees: Developers and Testers.

Both have a name and need to display it. However, developers write code, while testers test applications.

Instead of repeating the common functionality, we can put it in a parent class.

class Employee:
    def __init__(self, name):
        self.name = name

    def display_name(self):
        print(f"Employee: {self.name}")


class Developer(Employee):
    def write_code(self):
        print(f"{self.name} is writing code")


class Tester(Employee):
    def test_application(self):
        print(f"{self.name} is testing the application")


developer = Developer("Anu")
tester = Tester("Ravi")

developer.display_name()
developer.write_code()

tester.display_name()
tester.test_application()
Enter fullscreen mode Exit fullscreen mode

Both classes reuse display_name() from the Employee class, while each class has its own functionality.

3. Using Constants for Repeated Values

Suppose we use the same tax rate in multiple calculations.

Instead of hardcoding the value everywhere, we can define a constant.

TAX_RATE = 0.18

price = 1000
tax = price * TAX_RATE

print(tax)
Enter fullscreen mode Exit fullscreen mode

If the tax rate changes, we can update it in one place.

4. Creating Reusable Modules

Suppose multiple Python files need to calculate the area of a circle.

Instead of writing the same function in every file, we can create a separate module.

In math_utils.py:

import math

def circle_area(radius):
    return math.pi * radius ** 2
Enter fullscreen mode Exit fullscreen mode

In another file:

from math_utils import circle_area

print(circle_area(5))
Enter fullscreen mode Exit fullscreen mode

Now, we can reuse the same function in different files.

Benefits of DRY

  • Less duplicated code: We avoid repeating the same logic.
  • Easier maintenance: We can update shared logic in one place.
  • Consistent behavior: The same logic works across different parts of the application.
  • Easier testing: We can test reusable functions independently.

Cons

  • Over-abstraction: Combining unrelated logic can make code difficult to understand.
  • Unwanted dependencies: Changes to shared code can affect multiple parts of the application.

We should avoid repeating the same logic, but we don't need to combine code just because it looks similar.


2. KISS Principle (Keep It Simple, Stupid)

KISS stands for Keep It Simple, Stupid.

The idea is to avoid making code more complicated than necessary.

The Problem

Suppose we want to check whether a number is even.

def is_even(number):
    if number % 2 == 0:
        return True
    else:
        return False
Enter fullscreen mode Exit fullscreen mode

But do we really need an if-else statement here?

Applying KISS

We can simplify it:

def is_even(number):
    return number % 2 == 0
Enter fullscreen mode Exit fullscreen mode

Both functions produce the same result, but the second one is simpler and easier to read.

How to Apply KISS

  • Break large problems into smaller functions.
  • Use clear variable and function names.
  • Avoid unnecessary complexity.
  • Choose straightforward solutions when they meet the requirements.

Benefits of KISS

  • Easier to read and understand.
  • Easier to debug and maintain.
  • Less unnecessary complexity.
  • Faster code reviews.

Cons

Oversimplifying code can make it difficult to handle important requirements or edge cases.

Remember, shorter code is not always simpler code.


3. YAGNI Principle (You Aren't Gonna Need It)

YAGNI stands for You Aren't Gonna Need It.

The idea is to build what is required now instead of implementing features just because we might need them in the future.

The Problem: Building Unnecessary Features

Suppose we are developing a payment system that currently supports only credit card payments.

While developing it, we think:

"What if we need PayPal or cryptocurrency payments in the future?"

So, we start implementing support for those payment methods even though nobody has requested them.

This takes extra time and makes the code more complicated.

Applying YAGNI

For now, we should implement only the required credit card payment functionality.

If the requirements change later, we can add the necessary features then.

How to Apply YAGNI

  • Understand the current requirements.
  • Focus on features that are actually needed.
  • Avoid implementing hypothetical features.
  • Add functionality when there is a real requirement for it.

Benefits of YAGNI

  • Faster development.
  • Less unnecessary code.
  • Reduced development effort.
  • Better focus on current requirements.

Cons

  • Future requirements may require refactoring.
  • New features may take longer if the existing design needs changes.

YAGNI doesn't mean ignoring future maintainability. It means avoiding unnecessary implementation before it is needed.

The goal is to write code that solves the current problem and is easy to understand and maintain.

Top comments (0)