Design Principles Cheatsheet
Design principles are foundational guidelines and best practices that help developers create software that is maintainable, scalable, and flexible. Here’s a cheatsheet summarizing the key design principles with examples.
1. SOLID Principles
The SOLID principles are a set of five object-oriented design principles that help developers write clear, maintainable, and scalable code.
S – Single Responsibility Principle (SRP)
- Definition: A class should have only one reason to change, i.e., it should have only one responsibility.
- Purpose: Reduces the risk of changes affecting unrelated parts of the code.
- Example:
class User {
private String name;
private String email;
// Only deals with user properties.
}
class UserRepository {
public void save(User user) {
// Logic for saving a user.
}
}
- Bad Example: If the
User
class handles both user data and persistence, it violates SRP.
O – Open/Closed Principle (OCP)
- Definition: Software entities (classes, modules, functions) should be open for extension but closed for modification.
- Purpose: Allows behavior to be extended without modifying the existing code.
- Example:
abstract class Shape {
public abstract double area();
}
class Circle extends Shape {
double radius;
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle extends Shape {
double width, height;
public double area() {
return width * height;
}
}
- Bad Example: Modifying an existing class every time a new shape is added.
L – Liskov Substitution Principle (LSP)
- Definition: Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
- Purpose: Ensures inheritance relationships are maintained correctly.
- Example:
class Bird {
public void fly() {
System.out.println("Flying");
}
}
class Sparrow extends Bird {
public void fly() {
System.out.println("Sparrow flying");
}
}
class Penguin extends Bird {
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly");
}
}
- Violation: Substituting
Penguin
whereBird
is expected would break the code. ThePenguin
class shouldn’t inherit fromBird
if it cannot fly.
I – Interface Segregation Principle (ISP)
- Definition: No client should be forced to depend on methods it does not use.
- Purpose: Avoids bloated interfaces by creating smaller, more specific interfaces.
- Example:
interface Printer {
void print();
}
interface Scanner {
void scan();
}
class MultiFunctionPrinter implements Printer, Scanner {
public void print() { }
public void scan() { }
}
class SimplePrinter implements Printer {
public void print() { }
}
- Bad Example: A class implementing both printing and scanning when it only needs one would violate ISP.
D – Dependency Inversion Principle (DIP)
- Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Purpose: Promotes loose coupling by depending on abstractions rather than concrete classes.
- Example:
interface Database {
void saveData(String data);
}
class MySQLDatabase implements Database {
public void saveData(String data) { System.out.println("Data saved to MySQL"); }
}
class App {
private Database database;
public App(Database database) {
this.database = database;
}
public void save(String data) {
database.saveData(data);
}
}
- Bad Example: Directly creating a concrete
MySQLDatabase
insideApp
.
2. DRY – Don’t Repeat Yourself
- Definition: Every piece of knowledge or logic should only exist in a single place in the system.
- Purpose: Reduces redundancy and makes the system easier to maintain.
- Example:
// Repeating the validation logic
class UserValidator {
public boolean isValid(User user) {
if (user.getName().length() > 0 && user.getEmail().contains("@")) {
return true;
}
return false;
}
}
// Bad Example: The same validation logic repeated in multiple classes.
- Fix: Extract common logic into a reusable method or class.
3. KISS – Keep It Simple, Stupid
- Definition: Simplicity should be a key goal in design, and unnecessary complexity should be avoided.
- Purpose: Simplifies development and makes the system easier to understand and maintain.
- Example:
// Complex Solution (Unnecessary)
class UserHandler {
public boolean processUserData(String name, String email) {
// Multiple layers of unnecessary abstraction and complexity
return true;
}
}
// Simple Solution
class UserProcessor {
public boolean validateUser(String name, String email) {
return name.length() > 0 && email.contains("@");
}
}
4. YAGNI – You Aren’t Gonna Need It
- Definition: Don’t add functionality until it is necessary.
- Purpose: Reduces overengineering and keeps the codebase lean.
- Example:
// Unnecessary complexity added for future features
class UserManager {
public void processUserData(User user, String action) {
if (action.equals("delete")) {
// Delete user logic
} else if (action.equals("update")) {
// Update user logic
} else {
// Other future operations
}
}
}
// Solution: Only implement what is needed now.
5. Law of Demeter – Principle of Least Knowledge
- Definition: A module should not know about the internal details of other modules. It should only communicate with its direct dependencies.
- Purpose: Reduces coupling and keeps the codebase modular.
- Example:
class Engine {
public void start() { System.out.println("Engine started"); }
}
class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void startCar() {
engine.start(); // Law of Demeter: Car knows only about Engine, not its internals.
}
}
- Bad Example: A class calls methods on objects returned by other objects, leading to tight coupling.
6. Composition over Inheritance
- Definition: Prefer using composition (has-a relationship) over inheritance (is-a relationship).
- Purpose: More flexible and reduces problems like tight coupling, deep inheritance hierarchies, and limited extensibility.
- Example:
class Engine {
public void start() { System.out.println("Engine started"); }
}
class Car {
private Engine engine; // Composition: Car has an Engine.
public Car(Engine engine) {
this.engine = engine;
}
public void startCar() {
engine.start();
}
}
- Bad Example: Using inheritance when it’s not necessary can create unnecessary dependencies and limit future flexibility.
7. Favor Polymorphism Over Conditionals
- Definition: Avoid using conditionals to select behavior based on object type. Instead, use polymorphism to achieve flexibility.
- Purpose: Reduces the use of large conditionals, improving readability and flexibility.
- Example:
abstract class Shape {
public abstract double area();
}
class Circle extends Shape {
double radius;
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle extends Shape {
double width, height;
@Override
public double area() {
return width * height;
}
}
class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.area(); // Polymorphism: No need for conditionals here.
}
}
- Bad Example: Using
if
orswitch
statements to handle different behavior for different object types.
8. Keep Low Coupling, High Cohesion
- Low Coupling: Classes or components should not depend heavily on each other.
- High Cohesion: A class or module should focus on a single responsibility or closely related responsibilities.
- Purpose: This enhances maintainability, flexibility, and testability.
9. Encapsulation
- Definition: Hiding the internal state and requiring all interaction to be performed through well-defined interfaces.
- Purpose: Protects object state from unintended modifications and allows internal details to change without affecting the rest of the system.
- Example:
class Account {
private double balance;
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public double getBalance() {
return balance;
}
}
10. Avoid Premature Optimization
- Definition: Don’t optimize the code until it’s necessary. Focus on clarity and correctness first.
- Purpose: Premature optimization can lead to wasted effort and complicate code.
- Example:
// Bad Example: Over-optimization for performance without knowing if it's necessary
for (int i = 0; i < 1000000
; i++) {
// Some unoptimized code
}
- Solution: Optimize only when profiling indicates a performance bottleneck.
11. Separation of Concerns (SoC)
- Definition: A design principle for separating a computer program into distinct sections, such that each section addresses a separate concern.
- Purpose: Makes the code easier to manage and test.
- Example:
class UserController {
private UserService userService;
private UserValidator userValidator;
public void createUser(User user) {
if (userValidator.isValid(user)) {
userService.save(user);
}
}
}
12. Anti-Patterns to Avoid
- God Object: An object that knows too much or does too much, breaking SRP and cohesion.
- Spaghetti Code: Highly complex and tangled code, violating KISS and SOLID principles.
- Golden Hammer: Overusing a specific technology or solution for every problem, even when it’s inappropriate.
Conclusion
By following these design principles, developers can create robust, maintainable, and scalable software systems. The key takeaway is to write code that is easy to extend, test, and maintain by adhering to clear and well-defined responsibilities, minimizing complexity, and embracing flexibility.