Main Software Design Principles You Should Know

Main Software Design Principles You Should Know
As a senior software engineer, you often encounter complex systems where clean and maintainable code is crucial. Software design principles serve as guidelines to help developers create code that is efficient, reusable, and easy to understand. In this article, we’ll dive into some of the main software design principles, explain them in simple words, and provide code examples in JavaScript to make these concepts clear.
1. DRY: Don’t Repeat Yourself
What it means: Avoid duplicating code by consolidating logic into reusable functions or modules.
Simple Words:
Imagine writing the same paragraph over and over again in a book — it’s a waste of time and energy. Instead, just reference it once. In programming, this saves effort and makes future updates easier.
Example:
Bad Code:
function calculateAreaOfSquare(side) {
return side * side;
}
function calculateAreaOfRectangle(length, width) {
return length * width;
}
Better Code:
function calculateArea(shape, ...dimensions) {
if (shape === 'square') {
return dimensions[0] * dimensions[0];
} else if (shape === 'rectangle') {
return dimensions[0] * dimensions[1];
}
}
By generalizing the logic into a single function, updates only need to be made in one place.
2. KISS: Keep It Simple, Stupid
What it means: Strive for simplicity in your code. Avoid unnecessary complexity.
Simple Words:
Think of a maze versus a straight road. A straight road is easier to navigate and quicker to debug. Complex code creates more opportunities for bugs and confusion.
Example:
Bad Code:
function isEven(number) {
if (number % 2 === 0) {
return true;
} else {
return false;
}
}
Simpler Code:
function isEven(number) {
return number % 2 === 0;
}
The simpler code is easier to read, maintain, and debug.
3. SOLID Principles
The SOLID principles are five guidelines to create robust and maintainable object-oriented systems. Let’s simplify and break them down.
3.1 Single Responsibility Principle (SRP):
A class or function should have only one job.
Simple Words:
If you’re cooking and cleaning simultaneously, you’re bound to mess up one. Focus on one task per function or class.
Example:
Bad Code:
class UserManager {
addUser(user) {
// Add user to database
}
sendWelcomeEmail(user) {
// Send email logic
}
}
Better Code:
class UserManager {
addUser(user) {
// Add user to database
}
}
class EmailService {
sendWelcomeEmail(user) {
// Send email logic
}
}
Each class has one responsibility, making the code easier to extend and test.
3.2 Open/Closed Principle (OCP):
Software entities should be open for extension but closed for modification.
Simple Words:
You should be able to add new features without changing existing code (to avoid breaking things).
Example:
Bad Code:
function getDiscount(type, price) {
if (type === 'student') {
return price * 0.8;
} else if (type === 'senior') {
return price * 0.7;
}
}
Better Code:
class Discount {
calculate(price) {
return price;
}
}
class StudentDiscount extends Discount {
calculate(price) {
return price * 0.8;
}
}
class SeniorDiscount extends Discount {
calculate(price) {
return price * 0.7;
}
}
You can add new discount types by creating new classes without modifying existing ones.
3.3 Liskov Substitution Principle (LSP):
Derived classes should be substitutable for their base classes.
Simple Words:
If something works for a parent class, it should work the same way for a child class without breaking.
Example:
Bad Code:
class Bird {
fly() {
console.log("Flying");
}
}
class Penguin extends Bird {
fly() {
throw new Error("Penguins can't fly");
}
}
Better Code:
class Bird {
move() {
console.log("Moving");
}
}
class Penguin extends Bird {
move() {
console.log("Swimming");
}
}
By rethinking the behavior as “move” instead of “fly,” we avoid breaking functionality.
3.4 Interface Segregation Principle (ISP):
A class should not be forced to implement methods it does not use.
Simple Words:
Don’t give a carpenter a toolbox full of plumbing tools — stick to what’s needed.
Example:
Bad Code:
class Animal {
eat() {}
fly() {}
}
class Dog extends Animal {
fly() {
throw new Error("Dogs can't fly");
}
}
Better Code:
class Animal {
eat() {}
}
class FlyingAnimal extends Animal {
fly() {}
}
class Dog extends Animal {}
class Bird extends FlyingAnimal {}
3.5 Dependency Inversion Principle (DIP):
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Simple Words:
Don’t hard-code dependencies; let them be flexible and interchangeable.
Example:
Bad Code:
class EmailService {
sendEmail(message) {
console.log(`Sending email: ${message}`);
}
}
class Notification {
constructor() {
this.emailService = new EmailService();
}
notify(message) {
this.emailService.sendEmail(message);
}
}
Better Code:
class EmailService {
send(message) {
console.log(`Sending email: ${message}`);
}
}
class Notification {
constructor(service) {
this.service = service;
}
notify(message) {
this.service.send(message);
}
}
const emailService = new EmailService();
const notification = new Notification(emailService);
notification.notify("Hello!");
This approach makes it easy to replace EmailService
with another service, like SMSService
, without changing the Notification
class.
4. YAGNI: You Aren’t Gonna Need It
What it means: Don’t add features or functionality until you actually need them.
Simple Words:
Don’t pack a winter coat for a summer vacation — you’ll just carry unnecessary weight.
Example:
Bad Code:
function calculate(x, y, operation) {
if (operation === 'add') {
return x + y;
} else if (operation === 'subtract') {
return x - y;
} else if (operation === 'multiply') {
return x * y;
} else if (operation === 'divide') {
return x / y;
} else if (operation === 'power') { // Not needed yet
return Math.pow(x, y);
}
}
Better Code:
function calculate(x, y, operation) {
if (operation === 'add') {
return x + y;
} else if (operation === 'subtract') {
return x - y;
} else if (operation === 'multiply') {
return x * y;
} else if (operation === 'divide') {
return x / y;
}
}
Conclusion
Design principles are not strict rules but guidelines to help you write cleaner, more maintainable code. Start with small improvements — refactor functions to reduce duplication, simplify complex logic, and make your code modular. Over time, these principles will become second nature, leading to better software for both developers and users.
Happy coding! 🚀