In software development, certain principles stand as the bedrock for writing code that is not only functional but also clean, maintainable, and efficient.
In this article, we will explore 9 such software design principles every developer should have in their toolkit.
1. Keep It Simple, Stupid (KISS)
The KISS principle advocates for simplicity in design and implementation.
Complex code is harder to understand, maintain, and debug. By keeping your code simple, you make it more readable and reduce the likelihood of errors.
Example: Consider a function that checks if a number is even:
The simplified version is easier to read and understand.
2. Don't Repeat Yourself (DRY)
The DRY principle advocates for reducing repetition in code.
When you find yourself writing similar code in multiple places, it's a sign that you should refactor your code to eliminate redundancy.
By abstracting similar code into a single location, such as a function or class, you make your code more maintainable and reduce the risk of introducing bugs when changes are needed.
Example: Imagine you're writing a program that needs to greet different types of users visiting your website.
Without applying the DRY principle, you might write something like this:
In this code, we're repeating the first two lines in each function.
Applying the DRY principle, we can refactor this to:
By creating a basic_greeting
function, we've eliminated the repetition of the common greeting lines.
📣 Become a Better Software Engineer - CodeCrafters.io
CodeCrafters is a YC backed platform that offers a unique, hands-on approach to practice complex programming challenges like building your own Redis, Git, SQLite, Bittorrent client and more from scratch.
3. You Aren't Gonna Need It (YAGNI)
YAGNI is a principle that warns against over-engineering.
Developers often anticipate future requirements and add unnecessary functionality, which increases complexity and maintenance overhead.
YAGNI advises you to implement only what you need now, not what you might need later.
Example: Suppose you're building a simple calculator app, and you're tempted to add features for advanced scientific calculations. YAGNI would suggest focusing on the basic arithmetic operations until there's a clear requirement for advanced features.
4. Encapsulate What Varies
This principle suggests isolating the parts of your code that are likely to change, such as algorithms or data sources, so that these changes can be made independently of the rest of the system.
This makes your code more flexible and easier to maintain.
Example: Consider a payment processing system that supports multiple payment methods.
Without Encapsulation, PaymentProcessor
class directly handles the logic for different payment methods. If you need to add or modify a payment method, you'll have to change this class, which can lead to bugs and make the code harder to maintain.
As new payment methods are added, this class will grow in complexity, making it difficult to manage.
After encapsulating the varying parts, we separate the code that handles different payment methods from the main payment processing logic.
Benefits of This Approach:
Flexibility: Adding a new payment method, like
BitcoinPayment
, only requires creating a new class that implements thePaymentMethod
interface. ThePaymentProcessor
class remains unchanged.Maintainability: The logic for each payment method is encapsulated in its own class, making the system easier to maintain and test. If there's a bug in the PayPal payment process, you only need to look at the
PayPalPayment
class.Reduced Coupling: The
PaymentProcessor
is now loosely coupled to the payment method implementations. It doesn’t need to know how each payment method works, just that it can call thepay
method.
5. Program to an Interface, Not an Implementation
Programming to an interface means that your code should depend on the abstraction of a behavior rather than a specific implementation of that behavior.
This allows for more flexibility in changing implementations without affecting the code that relies on them.
Example: Imagine you're building a system where different types of documents need to be printed.
Initially, you might start by creating a specific Printer
class that handles the printing process.
Here’s how the code might look if you were programming directly to an implementation:
In this implementation:
The
ReportGenerator
class directly depends on theLaserPrinter
class, a specific implementation of a printer.If you wanted to use a different type of printer (e.g.,
InkjetPrinter
), you would need to modify theReportGenerator
class.
To adhere to this principle, you can create an interface or an abstract class that represents the general concept of a Printer
.
The ReportGenerator
will then depend on this abstraction rather than a specific implementation.
6. Favor Composition Over Inheritance
This principle suggests using composition (has-a relationship) over inheritance (is-a relationship) when designing class relationships.
Inheritance can lead to rigid class hierarchies that are difficult to change. Composition allows for more flexibility by assembling behavior from smaller, interchangeable components.
Example: Instead of creating a complex inheritance hierarchy for animals, use composition to add behavior.
With this, you can easily mix and match behaviors without being tied to a rigid hierarchy.
7. Strive for Loosely Coupled Designs
Design your system so that components are loosely coupled, meaning they have little or no knowledge of each other’s inner workings.
Loose coupling reduces dependencies between different parts of your system, making it easier to modify or replace components without affecting the whole system.
Example: Consider a UserService call is tightly coupled with Database class.
After making it loosely coupled, UserService
can work with any database implementation and doesn’t need to know how to create a Database object.
8. Law of Demeter (LoD)
The Law of Demeter is often summarized as "only talk to your immediate friends."
This principle aims to minimize the dependencies between objects, reducing the risk that changes in one part of the system will ripple through other parts.
An object should only call methods of:
Itself.
Objects passed as arguments.
Objects it creates.
Its direct components.
Example: Imagine you are developing a system to simulate the operation of a car.
The car has various components, such as an engine, which in turn has components like pistons.
Here’s how the code might look if the Law of Demeter is not followed:
The Car
class is tightly coupled with both Engine
and Piston
. If the internal structure of Engine
or Piston
changes (e.g., the method fire
is renamed), you would need to modify the Car
class.
To apply the Law of Demeter, we need to reduce the knowledge that Car
has about the internal structure of Engine
and Piston
.
Instead of having the Car
directly access Piston
through Engine
, we should delegate the responsibility of interacting with Piston
to the Engine
.
Now, the Car
class only interacts with the Engine
class and knows nothing about Piston
. This reduces the coupling between Car
and the internals of Engine
.
If the Piston
class changes (e.g., method names, internal behavior), you only need to update the Engine
class, not the Car
class.
9. SOLID Principles
The SOLID principles are a set of five design principles intended to make software designs more understandable, flexible, and maintainable.
Single Responsibility Principle (SRP): A class should have only one reason to change. It should do one thing and do it well.
Open/Closed Principle (OCP): Software entities should be open for extension but closed for modification.
Liskov Substitution Principle (LSP): Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.
Interface Segregation Principle (ISP): Clients should not be forced to depend on methods they do not use. Instead of one large interface, multiple smaller interfaces are preferred.
Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions.
If you want to learn about SOILD principles in detail with code examples, checkout my previous article:
Hope you enjoyed reading this article.
If you found it valuable, hit a like ❤️ and consider subscribing for more such content every week.
If you have any questions or suggestions, leave a comment.
Checkout my Youtube channel for more in-depth content.
Follow me on LinkedIn, X and Medium to stay updated.
Checkout my GitHub repositories for free interview preparation resources.
I hope you have a lovely day!
Great Examples and clear explanation!
This is a great compilation!
Simply put, , Ashish.