In the world of software development, adopting core programming principles can dramatically improve code quality, maintainability, and efficiency. Whether you're just starting your career as a developer or have years of experience under your belt, these principles provide a strong foundation for writing better code.

Below, we’ll dive into six key programming principles. They’re organized by complexity, starting with the simplest and moving toward more advanced concepts. For each, we’ll share examples demonstrating “bad,” “good,” and “best” practices.


1. The Boy Scout Rule

Concept: Leave the codebase cleaner than you found it.

The Boy Scout Rule is simple: improve the quality of the code you touch. This principle encourages developers to always improve the code they work on, even if it’s just minor enhancements like renaming variables, removing unused code, or breaking large functions into smaller pieces. These small improvements compound over time, leading to a healthier codebase.

Example:

Bad: Ignorning Poor Code

function fetchData() {
    let result = fetch("https://api.example.com/data").then(response => response.json());
    return result;
}

This lacks error handling and clarity.

Good: Refactoring for Readability

async function fetchData() {
    const response = await fetch("https://api.example.com/data");
    return await response.json();
}

Best: Handling Edge Cases

async function fetchData(): Promise<any> {
    try {
        const response = await fetch("https://api.example.com/data");
        if (!response.ok) {
            throw new Error("Network response was not ok");
        }
        return await response.json();
    } catch (error) {
        console.error("Failed to fetch data:", error);
        throw error;
    }
}

This version is cleaner, handles errors, and uses TypeScript for stronger typing.


2. KISS (Keep It Simple, Stupid)

Concept: Write simple, straightforward code that avoids unnecessary complexity.

When code becomes overly complicated, it’s more likely to contain bugs and harder to maintain. Always favor clarity and simplicity over being overly clever.

Example:

Bad: Overcomplicating a Sum Function

function sum(numbers) {
    let total = 0;
    for (let i = 0; i < numbers.length; i++) {
        total += numbers[i];
    }
    return total;
}

console.log(sum([1, 2, 3])); // Output: 6

While this works, it's verbose for a simple operation.

Good: Simplifying the Function

function sum(numbers) {
    return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum([1, 2, 3])); // Output: 6

Best: Adding TypeScript for Clarity

function sum(numbers: number[]): number {
    return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum([1, 2, 3])); // Output: 6


3. DRY (Don’t Repeat Yourself)

Concept: Remove repetitive code by creating reusable functions or modules.

Repeated code not only increases maintenance effort but can also lead to inconsistencies over time. DRY helps you write cleaner and more efficient code.

Example:

Bad: Repetitive Code

function getUserById(id) {
    return fetch(`https://api.example.com/users/${id}`);
}

function getProductById(id) {
    return fetch(`https://api.example.com/products/${id}`);
}

Good: Abstracting Common Logic

function getById(entity, id) {
    return fetch(`https://api.example.com/${entity}/${id}`);
}

Best: Adding TypeScript for Clarity

function getById<T>(entity: string, id: number): Promise<T> {
    return fetch(`https://api.example.com/${entity}/${id}`).then(response => response.json());
}

By making the function generic, you can reuse it across different types.


4. Separation of Concerns (SoC)

Concept: Divide your code into distinct sections, each with its own responsibility.

This principle makes your code modular, reusable, and easier to maintain. By separating different concerns, you reduce interdependencies between code components.

Example:

Bad: Tightly Coupled Code

function getUserAndDisplay() {
    fetch("https://api.example.com/users")
        .then(response => response.json())
        .then(data => console.log(data));
}

Good: Splitting Logic

function fetchUsers() {
    return fetch("https://api.example.com/users").then(response => response.json());
}

function displayUsers(users) {
    console.log(users);
}

fetchUsers().then(displayUsers);

Best: TypeScript with Modular Design

async function fetchUsers(): Promise<User[]> {
    const response = await fetch("https://api.example.com/users");
    return await response.json();
}

function displayUsers(users: User[]): void {
    console.log(users);
}

fetchUsers().then(displayUsers);

Now, each function handles a single responsibility.


5. YAGNI (You Aren’t Gonna Need It)

Concept: Avoid adding functionality unless it is necessary.

It’s tempting to preemptively add features or properties that you think might be useful later, but this can lead to unnecessary complexity and bloat. Focus on what’s needed right now.

Example:

Bad: Over-Prepared Code

class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
        this.isAdmin = false; // Unused for now
    }
}

Good: Lean Implementation

class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }
}

Best: Adding TypeScript for Clarity

class User {
    constructor(public name: string, public email: string) {}
}

Unnecessary properties and boilerplate are removed.



6. SOLID Principles

The SOLID principles are a collection of advanced guidelines designed to help you write maintainable, scalable, and object-oriented code. Let’s break down each principle with examples.

Single Responsibility Principle (SRP)

Concept: A class should have only one reason to change.

Bad:

The User class is responsible for both saving and validating, violating SRP.

class User {
    save() {
        // save logic
    }

    validate() {
        // validation logic
    }
}

Good:

The User class focuses on the user itself, while UserService handles saving logic.

class User {
    // user properties
}

class UserService {
    save(user) {
        // save logic
    }
}

Best:

TypeScript ensures strong typing for the User object, making the design more robust.

class User {
    constructor(public id: number, public name: string) {}
}

class UserService {
    save(user: User): void {
        // save logic
    }
}

Open/Closed Principle (OCP)

Concept: Classes should be open for extension but closed for modification.

Bad:

Adding new payment types requires modifying the Payment class.

class Payment {
    process(type) {
        if (type === 'credit') {
            // credit logic
        } else if (type === 'paypal') {
            // PayPal logic
        }
    }
}

Good:

New payment types can be added as separate classes without modifying the existing ones.

class CreditPayment {
    process() {
        // credit logic
    }
}

class PaypalPayment {
    process() {
        // PayPal logic
    }
}

Best:

An interface ensures all payment types implement the process method, making the system more extensible.

interface Payment {
    process(): void;
}

class CreditPayment implements Payment {
    process(): void {
        // credit logic
    }
}

class PaypalPayment implements Payment {
    process(): void {
        // PayPal logic
    }
}

Liskov Substitution Principle (LSP)

Concept: Subtypes must be substitutable for their base types.

Bad:

The Penguin class violates LSP because it doesn’t behave like other Bird instances.

class Bird {
    fly() {
        // fly logic
    }
}

class Penguin extends Bird {
    fly() {
        throw new Error('Penguins can’t fly!');
    }
}

Good:

Separating flying and non-flying birds improves design.

class Bird {
    // bird properties
}

class FlyingBird extends Bird {
    fly() {
        // fly logic
    }
}

class Penguin extends Bird {
    swim() {
        // swim logic
    }
}

Best:

Using abstract classes enforces a clear distinction between behaviors, adhering to LSP.

abstract class Bird {}

abstract class FlyingBird extends Bird {
    abstract fly(): void;
}

class Sparrow extends FlyingBird {
    fly(): void {
        // fly logic
    }
}

class Penguin extends Bird {
    swim(): void {
        // swim logic
    }
}

Interface Segregation Principle (ISP)

Concept: Interfaces should only include methods relevant to the implementing class.

Bad:

The Worker interface is too broad and forces robots to implement unrelated methods.

interface Worker {
    work(): void;
    eat(): void;
}

class Robot implements Worker {
    work(): void {
        // work logic
    }

    eat(): void {
        throw new Error('Robots do not eat');
    }
}

Good:

By splitting the interface, each class implements only the methods it needs.

interface Worker {
    work(): void;
}

interface HumanWorker extends Worker {
    eat(): void;
}

class Robot implements Worker {
    work(): void {
        // work logic
    }
}

class Human implements HumanWorker {
    work(): void {
        // work logic
    }

    eat(): void {
        // eat logic
    }
}

Dependency Inversion Principle (DIP)

Concept: High-level modules should not depend on low-level modules. Both should depend on abstractions.

Bad:

The App class depends directly on the Database implementation.

class Database {
    save(data) {
        // save logic
    }
}

class App {
    constructor() {
        this.database = new Database();
    }

    saveData(data) {
        this.database.save(data);
    }
}

Good:

By introducing an interface, the App class is decoupled from specific database implementations.

interface Database {
    save(data: any): void;
}

class SQLDatabase implements Database {
    save(data: any): void {
        // SQL save logic
    }
}

class App {
    constructor(private database: Database) {}

    saveData(data: any): void {
        this.database.save(data);
    }
}

const app = new App(new SQLDatabase());


Conclusion

By applying these principles, you can write cleaner, more maintainable, and scalable code. Start with simple principles like the Boy Scout Rule and KISS, then gradually work your way toward mastering advanced concepts like SOLID.

Remember, these principles are tools meant to guide your development — not strict rules. Always adapt them to your specific project and team needs.