BACK

Software Engineering & OOAD

Master software engineering principles, Object-Oriented Analysis and Design (OOAD), and advanced testing methodologies.

Official Documentation

February 2026

Contents

Foundations

  • Introduction to Software Engineering
  • Software Development Life Cycles (SDLC)
  • Object-Oriented Fundamentals
  • Interface vs Implementation
  • Relationship: Association, Aggregation, Composition

SOLID

  • Single Responsibility Principle (SRP)
  • Open/Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

Analysis & Design

  • OO Analysis: Requirements & Use Cases
  • OO Design: Class Modeling & UML

Design Patterns

  • Singleton & Factory Patterns
  • Builder & Prototype Patterns
  • Adapter & Bridge Patterns
  • Composite & Decorator Patterns
  • Facade & Proxy Patterns
  • Observer & Strategy Patterns
  • Command & State Patterns
  • Template Method & Iterator Patterns

Architecture & Quality

  • Principles of Software Architecture

Testing

  • Unit Testing Principles
  • Test-Driven Development (TDD)
  • Behavior-Driven Development (BDD)
  • Integration Testing
  • Mocking and Stubbing

Architecture & Quality

  • Clean Code Practices
  • Refactoring Techniques

Deployment

  • DevOps and CI/CD
  • Software Quality and Maintenance

Foundations

Section Detail

Introduction to Software Engineering

Introduction to Software Engineering

Software Engineering is the application of a systematic, disciplined, quantifiable approach to the development, operation, and maintenance of software. It is not just about writing code; it encompasses the entire lifecycle of a software product, from inception to retirement.

The Software Crisis

In the late 1960s, the term “Software Crisis” was coined to describe the difficulties in writing efficient, error-free, and maintainable software. Projects were frequently over budget, behind schedule, and failed to meet user requirements. This led to the realization that software development needed to transition from a “craft” to an “engineering discipline.”

Core Principles

  1. Maintainability: Software should be written in such a way that it can evolve to meet changing needs.
  2. Dependability: Software must be reliable, secure, and safe.
  3. Efficiency: Software should not make wasteful use of system resources.
  4. Usability: Software must be usable by the users for whom it was designed.

Software Engineering vs. Computer Science

While Computer Science focuses on the theory and fundamentals of computation (algorithms, data structures, complexity), Software Engineering focus on the practicalities of developing and delivering useful software.

Example: Managing Complexity

One of the primary goals of software engineering is to manage complexity. As systems grow, they become harder to understand. We use tools like abstraction and modularity to break systems into smaller, manageable parts.

Modularity in Python

# A simple example of modularity by separating concerns
class UserManager:
    def __init__(self):
        self.users = []

    def add_user(self, name):
        self.users.append(name)
        print(f"User {name} added.")

class Logger:
    @staticmethod
    def log(message):
        print(f"[LOG]: {message}")

# Usage
logger = Logger()
manager = UserManager()
manager.add_user("Alice")
logger.log("Alice was added to the system.")

Professional Responsibility

Software engineers have responsibilities to their profession and to society. This includes maintaining confidentiality, being competent, and not misrepresenting their skills. Ethical considerations are paramount, especially as software controls more critical infrastructure.

The Engineering Mindset

The engineering mindset involves:

  • Requirement Analysis: Understanding what the user actually needs.
  • Design: Planning the structure of the system before coding.
  • Testing: Rigorously verifying that the system behaves as expected.
  • Evolution: Planning for long-term changes.

In the following lessons, we will dive deeper into the methodologies and design patterns that make high-quality software development possible.

Section Detail

Software Development Life Cycles (SDLC)

Software Development Life Cycles (SDLC)

An SDLC is a structured process used by the software industry to design, develop, and test high-quality software. The SDLC aims to produce high-quality software that meets or exceeds customer expectations, reaches completion within times and cost estimates.

Stages of SDLC

  1. Planning and Requirement Analysis: Defining the project scope and resource requirements.
  2. Defining Requirements: Documenting the product requirements and getting them approved by the customer.
  3. Designing Software: Developing the architecture and design of the product.
  4. Developing Code: The actual writing of the source code.
  5. Testing: Verifying that the application contains no bugs and meets the requirements.
  6. Deployment and Maintenance: Releasing the product to the market and maintaining it through updates.

SDLC Models

1. Waterfall Model

The Waterfall model is a linear-sequential life cycle model. In a waterfall model, each phase must be completed before the next phase can begin and there is no overlapping in the phases.

Pros: Simple and easy to understand and use. Cons: High risk and uncertainty; not a good model for complex and object-oriented projects.

2. V-Model (Validation and Verification)

An extension of the waterfall model, the V-model associates a testing phase with each development phase.

3. Agile Methodology

Agile is an iterative, incremental approach to software development. It focuses on flexibility, continuous improvement, and rapid delivery.

The Agile Manifesto

  • Individuals and interactions over processes and tools.
  • Working software over comprehensive documentation.
  • Customer collaboration over contract negotiation.
  • Responding to change over following a plan.

Implementing an Iterative Process (Pseudo-code)

Imagine we are building a feature in sprints.

def sprint(backlog, duration_weeks=2):
    print(f"Starting {duration_weeks}-week sprint...")
    completed_tasks = []
    for task in backlog:
        if can_complete(task):
            work_on(task)
            completed_tasks.append(task)
    return completed_tasks

project_backlog = ["Login UI", "Database Setup", "Auth Logic", "Profile Page"]
sprint_1_results = sprint(project_backlog[:2])
print(f"Delivered: {sprint_1_results}")

DevOps and Continuous Integration

Modern SDLCs often incorporate DevOps practices. Continuous Integration (CI) involves automatically testing and building the application every time a change is made to the codebase. Continuous Deployment (CD) goes a step further by automatically deploying those changes to production.

Benefits of CI/CD

  • Faster time to market.
  • Reduced risk of “integration hell”.
  • Higher quality through automated testing.

Choosing the Right Model

The choice of an SDLC depends on:

  • Complexity of the project.
  • Clarity of the requirements.
  • The cost of failure.
  • The project timeline and budget.

For established projects with well-defined requirements, Waterfall might still be appropriate. However, for most modern web and mobile applications, Agile is the industry standard.

Section Detail

Object-Oriented Fundamentals

Object-Oriented Fundamentals

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of “objects,” which can contain data and code. Data in the form of fields (attributes or properties), and code in the form of procedures (methods).

The four pillars of OOP are Abstraction, Encapsulation, Inheritance, and Polymorphism.

1. Abstraction

Abstraction is the concept of hiding the internal details and showing only the functionality. It helps in reducing programming complexity and effort.

C++ Example of Abstraction

In C++, we use classes to provide abstraction.

#include <iostream>
using namespace std;

class CoffeeMachine {
public:
    void makeCoffee() {
        boilWater();
        brewBeans();
        cout << "Coffee is ready!" << endl;
    }
private:
    void boilWater() { cout << "Boiling water..." << endl; }
    void brewBeans() { cout << "Brewing beans..." << endl; }
};

int main() {
    CoffeeMachine myMachine;
    myMachine.makeCoffee(); // User only knows how to make coffee, not the internals.
    return 0;
}

2. Encapsulation

Encapsulation is the bundling of data with the methods that operate on that data. It restricts direct access to some of the object’s components, which is a means of preventing accidental interference and misuse of the data.

Java Example of Encapsulation

public class BankAccount {
    private double balance; // Data is hidden

    public void deposit(double amount) {
        if (amount > 0) balance += amount;
    }

    public double getBalance() {
        return balance;
    }
}

3. Inheritance

Inheritance is a mechanism in which one object acquires all the properties and behaviors of a parent object. it represents the IS-A relationship.

Python Example of Inheritance

class Animal:
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

4. Polymorphism

Polymorphism allows objects of different types to be treated as objects of a common base type. The most common use is when a parent class reference is used to refer to a child class object.

Polymorphism in Action (Java)

Animal myDog = new Dog();
Animal myCat = new Cat();

Animal[] animals = {myDog, myCat};
for (Animal a : animals) {
    System.out.println(a.speak()); // Different behavior for different types
}

Why OOP?

  • Modularity: Code can be written and maintained independently.
  • Information Hiding: Security and reduced side effects.
  • Reusability: Through inheritance and composition.
  • Pluggability: Polymorphism allows for easily swapping components.

Understanding these fundamentals is the first step toward Object-Oriented Analysis and Design (OOAD), where we model complex real-world systems into these structures.

Section Detail

Interface vs Implementation

Interface vs Implementation

One of the most powerful concepts in Software Engineering and OOAD is the separation of Interface and Implementation.

Definitions

  • Interface: Defines what an object does (the contract). It consists of function signatures and public methods.
  • Implementation: Defines how an object does it. It contains the logic, state, and private helper methods.

The Principle: Program to an Interface

When you “program to an interface,” you write code that interacts with an abstraction (an interface or abstract class) rather than a concrete class. This makes your code more flexible and easier to change.

Example: Different Storage Mechanisms

Imagine an application that needs to save data. You could save it to a File or a Database.

Wrong Way (Programming to Implementation)

class Database {
public:
    void saveToDB(string data) { /* logic */ }
};

class App {
    Database db; // Hard dependency on concrete class
public:
    void run() { db.saveToDB("User Data"); }
};

If you want to switch to a file system, you must rewrite the App class.

Right Way (Programming to Interface)

// The Interface
class IDataStore {
public:
    virtual void save(string data) = 0;
};

// Implementation 1
class DatabaseStore : public IDataStore {
public:
    void save(string data) override { cout << "Saving to DB..." << endl; }
};

// Implementation 2
class FileStore : public IDataStore {
public:
    void save(string data) override { cout << "Saving to File..." << endl; }
};

// The App depends on the interface
class App {
    IDataStore* store;
public:
    App(IDataStore* s) : store(s) {}
    void run() { store->save("User Data"); }
};

Benefits of Decoupling

  1. Easy Swapping: You can swap implementations without changing the client code.
  2. Testability: You can inject “Mock” or “Fake” implementations for unit testing.
  3. Parallel Development: One team can work on the interface usage while another works on the implementation.
  4. Extensibility: You can add new implementations later without breaking existing systems.

Abstract Classes vs. Interfaces (Java/C#)

  • Interfaces: Usually only contain method signatures (though modern Java allows default methods). A class can implement multiple interfaces.
  • Abstract Classes: Can contain both implemented methods and abstract methods. A class can typically only inherit from one abstract class.

Java Interface Example

public interface PaymentProcessor {
    void process(double amount);
}

public class StripeProcessor implements PaymentProcessor {
    public void process(double amount) {
        System.out.println("Processing via Stripe: $" + amount);
    }
}

Summary

By separating implementation from interface, we create a layer of abstraction that protects the rest of our system from changes in low-level details. This is a recurring theme in all design patterns.

Section Detail

Relationship: Association, Aggregation, Composition

Relationships in OOAD

In Object-Oriented design, classes don’t exist in isolation. They interact with each other. We categorize these interactions into several types of relationships.

1. Association

Association is the most general relationship. It represents a “has-a” or “uses-a” relationship where objects have their own independent lifecycle and there is no ownership.

Python Example of Association

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

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

# A teacher is associated with a student, but they exist independently.
t = Teacher("Mr. Smith")
s = Student("Alice")

2. Aggregation (Weak Has-A)

Aggregation is a special form of association. It represents a “part-of” relationship where the part can exist independently of the whole. The parent “aggregates” the child.

Lifecycle: If the parent is destroyed, the child usually survives.

Java Example of Aggregation

class Department {
    private List<Employee> employees;
    // If the Department is closed, the employees still exist.
}

3. Composition (Strong Has-A)

Composition is a strong form of aggregation. It represents a “part-of” relationship where the part cannot exist independently of the whole.

Lifecycle: If the parent is destroyed, the child is also destroyed.

C++ Example of Composition

class Heart {
public:
    void beat() {}
};

class Human {
    Heart heart; // The heart is part of the human.
public:
    // If Human is destroyed, heart is destroyed too.
};

4. Multiplicity

In UML and OOAD, we also define how many instances are involved:

  • 1..1: Exactly one
  • 0..*: Zero or more
  • 1..*: One or more

Comparing the Three

FeatureAssociationAggregationComposition
RelationshipLink between two classesWeak “Has-A”Strong “Has-A”
LifecycleIndependentIndependentDependent
ExampleTeacher & StudentDepartment & TeacherHouse & Room
UML SymbolStraight LineHollow DiamondFilled Diamond

Why it Matters

Choosing the right relationship type dictates:

  • How memory is managed (especially in languages like C++).
  • How objects are initialized and cleaned up.
  • The level of coupling between components.

In Design Patterns, we often prefer Composition over Inheritance to allow for more flexible code reuse without the rigid hierarchies that inheritance creates.

SOLID

Section Detail

Single Responsibility Principle (SRP)

Single Responsibility Principle (SRP)

The Single Responsibility Principle is the “S” in SOLID. It states that:

“A class should have one, and only one, reason to change.” — Robert C. Martin

What is a “Reason to Change”?

A “reason to change” is synonymous with a “responsibility”. If a class handles multiple disparate tasks, it has multiple responsibilities.

The Problem with Multiple Responsibilities

  1. Fragility: Changing one responsibility might accidentally break another.
  2. Low Cohesion: The class becomes bloated and hard to understand.
  3. Difficult Testing: You have to mock many unrelated things to test one function.

Example: The Multi-Purpose User Class

The Bad Way (Violating SRP)

This class handles user data, validation, and database saving.

class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email

    def validate_email(self):
        return "@" in self.email

    def save_to_db(self):
        print(f"Saving {self.username} to database...")

If the database schema changes, we change the User class. If the validation logic changes, we change the User class. This class has two reasons to change.

The Good Way (Applying SRP)

We split the responsibilities into separate classes.

class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email

class UserValidator:
    @staticmethod
    def validate(user):
        return "@" in user.email

class UserRepository:
    def save(self, user):
        print(f"Persisting {user.username} to DB...")

SRP in C++

Consider a class that manages a Report and also prints it.

// Violation
class Report {
    string content;
public:
    void generate() { /* ... */ }
    void print() { cout << content << endl; } // Printing logic mixed with data logic
};

// Fixed
class Report {
    string content;
public:
    string getContent() { return content; }
};

class ReportPrinter {
public:
    void print(Report& r) { cout << r.getContent() << endl; }
};

How to Identify SRP Violations

  • Size: Very large classes often have too many responsibilities.
  • Imports: A high number of imports from unrelated modules.
  • Documentation: If you find yourself using “and” to describe what a class does (e.g., “This class parses data AND sends emails”).
  • Frequent Changes: If different teams are constantly touching the same file for different reasons.

Benefits of SRP

  • Maintainability: Smaller classes are easier to read and modify.
  • Reusability: UserValidator can be reused in different parts of the app without bringing in the database logic.
  • Testability: Unit tests for UserValidator don’t need to know about databases.
Section Detail

Open/Closed Principle (OCP)

Open/Closed Principle (OCP)

The Open/Closed Principle is the “O” in SOLID. It was defined by Bertrand Meyer and popularized by Robert C. Martin. It states:

“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”

The Core Idea

You should be able to add new functionality to a system without changing the existing source code. Instead of editing an existing class, you should extend it (via inheritance or composition).

The Problem with Modification

When you modify existing code:

  1. You risk breaking existing features (regression).
  2. You must re-test all dependent modules.
  3. The code becomes a “God Object” filled with if-else or switch statements.

Example: Area Calculator

The Bad Way (Violating OCP)

class Rectangle {
    public double width;
    public double height;
}

class AreaCalculator {
    public double calculateArea(Object[] shapes) {
        double area = 0;
        for (Object shape : shapes) {
            if (shape instanceof Rectangle) {
                Rectangle r = (Rectangle) shape;
                area += r.width * r.height;
            }
            // If we add 'Circle', we MUST modify this class!
        }
        return area;
    }
}

The Good Way (Applying OCP)

We use abstraction to make the calculator “closed” to changes in shape types.

interface Shape {
    double getArea();
}

class Rectangle implements Shape {
    public double width, height;
    public double getArea() { return width * height; }
}

class Circle implements Shape {
    public double radius;
    public double getArea() { return Math.PI * radius * radius; }
}

class AreaCalculator {
    public double calculateArea(Shape[] shapes) {
        double area = 0;
        for (Shape shape : shapes) {
            area += shape.getArea();
        }
        return area;
    }
}

Now, if we want to add a Triangle, we just create a new class. We never touch AreaCalculator again.

OCP in C++: The Strategy Pattern

C++ often uses pointers to abstract classes to implement OCP.

class Logger {
public:
    virtual void log(string msg) = 0;
};

class ConsoleLogger : public Logger {
    void log(string msg) override { cout << msg << endl; }
};

class FileLogger : public Logger {
    void log(string msg) override { /* write to file */ }
};

class App {
    Logger* logger;
public:
    App(Logger* l) : logger(l) {}
    void doSomething() {
        logger->log("Something happened");
    }
};

When to Apply OCP

OCP is powerful but can lead to over-engineering. Apply it:

  • At architectural boundaries.
  • When you anticipate that a logic point will have multiple variations in the future.
  • When using plugin architectures.

Summary

OCP is about looking into the future. By using interfaces and abstractions, we create systems that are “immune” to changes in business requirements. We grow our software by adding new code, not by hacking old code.

Section Detail

Liskov Substitution Principle (LSP)

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle (LSP) is the “L” in SOLID. It was introduced by Barbara Liskov in 1987. It states:

“Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”

In simpler terms: A derived class should complement its base class, not contradict it.

The Classic Violation: Square and Rectangle

In geometry, a square is a rectangle. However, in software, this can lead to logic errors if not handled carefully.

The Violation (Python)

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    def set_width(self, w): self._width = w
    def set_height(self, h): self._height = h
    def get_area(self): return self._width * self._height

class Square(Rectangle):
    def set_width(self, w):
        self._width = w
        self._height = w # Constraints of a square

    def set_height(self, h):
        self._width = h
        self._height = h

Now, imagine a function that expects a Rectangle:

def increase_width(rect: Rectangle):
    rect.set_width(10)
    rect.set_height(5)
    # For a classic Rectangle, area should be 50.
    # For a Square, area will be 25 because set_height(5) overrode the width!
    assert rect.get_area() == 50 # This fails if rect is a Square!

The Square class is not a valid substitute for Rectangle in this context.

LSP Rules

  1. Contravariance of method arguments: A subclass should not require more than the superclass.
  2. Covariance of return types: A subclass should not return less than the superclass.
  3. No new exceptions: A subclass should not throw new exceptions that the client doesn’t expect.
  4. Preconditions cannot be strengthened: You can’t require more input data.
  5. Postconditions cannot be weakened: You must still guarantee the output.

Correct Design: Use Interfaces

Instead of inheritance, use a shared interface or rethink the hierarchy.

class Shape:
    def get_area(self):
        pass

class Rectangle(Shape):
    def __init__(self, w, h):
        self.w, self.h = w, h
    def get_area(self): return self.w * self.h

class Square(Shape):
    def __init__(self, side):
        self.side = side
    def get_area(self): return self.side ** 2

LSP in Java: Empty Implementations

A common LSP violation is when a subclass implements a method but leaves it empty or throws a “NotImplementedException”.

public interface Bird {
    void fly();
}

public class Duck implements Bird {
    public void fly() { System.out.println("Flying..."); }
}

public class Ostrich implements Bird {
    public void fly() {
        throw new UnsupportedOperationException("Ostriches can't fly!");
    }
}

If you pass an Ostrich to a function expecting Bird, and that function calls .fly(), the program crashes. Ostrich broke the contract of Bird.

How to Fix LSP Violations

  • Refactoring to Interface: Split the interface (e.g., FlyingBird vs SwimmingBird).
  • Composition: Instead of inheriting, have the class contain an instance of the other.
  • Is-A vs Behaves-Like: Ensure the subclass truly behaves like the parent.

Benefits

  • Predictability: You can trust that any object of type T will behave according to T’s contract.
  • Robustness: Reduces runtime errors and “special case” handling in calling code.
Section Detail

Interface Segregation Principle (ISP)

Interface Segregation Principle (ISP)

The Interface Segregation Principle (ISP) is the “I” in SOLID. It states:

“Clients should not be forced to depend on methods they do not use.”

This principle is about reducing the coupling between a class and its dependencies by making interfaces as specific as possible.

The Problem: Fat Interfaces

A “Fat” or “Polluted” interface is one that contains too many methods. When a class implements a fat interface, it often ends up with “dummy” implementations for methods it doesn’t need.

Example: Multi-Function Printer

The Violation (C++)

class IMachine {
public:
    virtual void print() = 0;
    virtual void scan() = 0;
    virtual void fax() = 0;
};

// A high-end printer can do everything
class AllInOnePrinter : public IMachine {
    void print() override { /* ... */ }
    void scan() override { /* ... */ }
    void fax() override { /* ... */ }
};

// An old printer can only print
class OldPrinter : public IMachine {
    void print() override { /* printing */ }
    void scan() override { /* ERROR: I can't scan! */ }
    void fax() override { /* ERROR: I can't fax! */ }
};

OldPrinter is forced to depend on scan() and fax(), even though it doesn’t support them.

Applying ISP

We split the fat interface into smaller, more focused ones.

class IPrinter {
public:
    virtual void print() = 0;
};

class IScanner {
public:
    virtual void scan() = 0;
};

class IFax {
public:
    virtual void fax() = 0;
};

// Now we can compose them
class AllInOnePrinter : public IPrinter, public IScanner, public IFax {
    void print() override { ... }
    void scan() override { ... }
    void fax() override { ... }
};

class OldPrinter : public IPrinter {
    void print() override { ... }
};

ISP in Java: Role Interfaces

Java’s standard library is a great example of ISP. Instead of a BigObjectInterface, we have interfaces like Serializable, Cloneable, Iterable, and Comparable. Each defines a very specific role.

Implementation Example

public interface Document {
    void open();
    void save();
}

public interface Encryptable {
    void encrypt();
}

public class SecureDocument implements Document, Encryptable {
    public void open() { ... }
    public void save() { ... }
    public void encrypt() { ... }
}

public class BasicDocument implements Document {
    public void open() { ... }
    public void save() { ... }
}

Signs of ISP Violation

  • Large interfaces with many methods.
  • Methods that throw NotImplementedException or are left empty.
  • Clients that only use a small subset of an interface’s methods.
  • Frequent changes to an interface that force unrelated clients to recompile.

Benefits of ISP

  1. Better Decoupling: Systems are easier to refactor, change, and redeploy.
  2. Clarity: Interfaces clearly define specific capabilities (roles).
  3. Efficiency: In some compiled languages, it reduces the amount of recompilation needed when an interface changes.

Summary

ISP teaches us to design for the client. By providing small, purpose-built interfaces, we ensure that our components only interact with the parts of the system they actually need.

Section Detail

Dependency Inversion Principle (DIP)

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle is the final “D” in SOLID. It states:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

The Hierarchy Problem

In traditional software development, high-level logic (e.g., Business Rules) often depends directly on low-level details (e.g., Database drivers, API clients). This makes the high-level logic fragile and hard to test.

Example: Notification System

The Violation (Python)

class EmailService:
    def send(self, message):
        print(f"Sending email: {message}")

class PasswordReset:
    def __init__(self):
        self.service = EmailService() # Hard dependency on low-level detail

    def reset(self):
        # logic...
        self.service.send("Your password was reset.")

Here, PasswordReset (High-level) depends on EmailService (Low-level). If we want to send an SMS instead, we must modify the PasswordReset class.

Applying DIP

We introduce an abstraction (Interface) between them.

from abc import ABC, abstractmethod

# The Abstraction
class MessageService(ABC):
    @abstractmethod
    def send(self, message):
        pass

# Low-level implementation
class EmailService(MessageService):
    def send(self, message):
        print(f"Email: {message}")

class SMSService(MessageService):
    def send(self, message):
        print(f"SMS: {message}")

# High-level module depends on the abstraction
class PasswordReset:
    def __init__(self, service: MessageService):
        self.service = service # Dependency Injection

    def reset(self):
        self.service.send("Reset link sent.")

Dependency Injection (DI) vs. DIP

While they are related, they are not the same:

  • DIP is the architectural principle (What is the dependency direction?).
  • DI is a technique for achieving DIP (How do we provide the dependency?).

DIP in Java with Spring-like patterns

In Java, we often use frameworks to handle the “Inversion of Control”.

public interface UserRepository {
    void save(User user);
}

public class UserService {
    private final UserRepository repo;

    // The service doesn't know WHO implements UserRepository
    public UserService(UserRepository repo) {
        this.repo = repo;
    }

    public void register(User user) {
        repo.save(user);
    }
}

Why “Inversion”?

It is called “Inversion” because it flips the traditional dependency graph.

  • Traditional: High-level → Low-level.
  • DIP: High-level → Abstraction ← Low-level. Both sides now “meet” at the abstraction.

Benefits

  • Flexibility: You can swap out a SQL database for a NoSQL one without touching business logic.
  • Testability: You can inject “Mock” dependencies into your high-level classes.
  • Maintainability: Low-level changes are isolated from the rest of the application.

Summary

The SOLID principles work together to create software that is easy to maintain and extend. By following the Dependency Inversion Principle, you ensure that your most valuable code (Business Logic) is protected from the volatility of external tools and frameworks.

Analysis & Design

Section Detail

OO Analysis: Requirements & Use Cases

Object-Oriented Analysis: Requirements & Use Cases

Object-Oriented Analysis (OOA) is the first technical step in the software development lifecycle where we focus on what the system should do, rather than how it should do it. The goal is to create a model of the functional requirements that is independent of implementation details.

1. Understanding Requirements

Requirements are the “wants and needs” of the stakeholders. In OOA, we categorize them into two main types:

Functional Requirements

These describe the specific behaviors, services, and tasks the system must perform.

  • Example: “The system must allow users to reset their passwords via email.”
  • Example: “The software must calculate the total tax based on the user’s region.”

Non-Functional Requirements (Qualities)

These describe how the system performs its functions—attributes like performance, security, and usability.

  • Example: “The system must respond to search queries in less than 200ms.”
  • Example: “Password data must be encrypted using AES-256.”

2. Use Case Modeling

Use case modeling is the primary tool for OOA. It helps identify the boundaries of the system and the interactions between the system and its environment.

Key Components:

  • Actors: Entities outside the system that interact with it. They can be human users (e.g., “Customer”) or external systems (e.g., “Payment Gateway”).
  • Use Cases: A discrete unit of functionality that provides value to an actor.
  • System Boundary: A box representing the limits of the software.

Use Case Diagram Example (PlantUML)

@startuml
left to right direction
actor "Customer" as C
actor "Bank System" as B

rectangle "ATM System" {
  use case "Withdraw Cash" as UC1
  use case "Check Balance" as UC2
  use case "Deposit Funds" as UC3
  use case "Transfer Money" as UC4
}

C --> UC1
C --> UC2
C --> UC3
C --> UC4

UC1 -- B
UC4 -- B
@enduml

3. Detailed Use Case Scenarios

A diagram is not enough. Each use case needs a description, often written as a “Success Scenario.”

Use Case: Withdraw Cash

  • Primary Actor: Customer
  • Preconditions: Customer has a valid card and the ATM has cash.
  • Trigger: Customer inserts card.
  • Main Success Scenario:
    1. System prompts for PIN.
    2. Customer enters PIN.
    3. System validates PIN.
    4. System prompts for amount.
    5. Customer enters amount.
    6. System checks account balance with Bank System.
    7. System dispenses cash.
    8. System updates balance and prints receipt.
  • Extensions (Alternative Flows):
    • 3a. Invalid PIN: System requests re-entry.
    • 6a. Insufficient Funds: System displays error and ejects card.

4. Requirements Elicitation Techniques

How do we find these requirements?

  1. Interviews: Talking directly to stakeholders.
  2. Workshops: Collaborative sessions with users and developers.
  3. Observation: Watching how users perform their current tasks.
  4. Prototyping: Building “mock-ups” to get early feedback.

5. From Requirements to Objects

During OOA, we also perform Domain Modeling. This involves identifying the “nouns” in our requirements which will eventually become our classes.

  • Nouns: Customer, Account, Transaction, ATM, Receipt.
  • Verbs: Withdraw, Deposit, Validate, Print.

Identifying these early ensures that the software structure mirrors the real-world domain, a core tenet of Object-Oriented Design.


Summary Checklist for OOA

  • Have all actors been identified?
  • Are the system boundaries clear?
  • Does every use case provide value to an actor?
  • Have non-functional constraints been documented?
  • Is there a glossary of domain terms?
Section Detail

OO Design: Class Modeling & UML

Object-Oriented Design: Class Modeling & UML

While Analysis focuses on the what, Object-Oriented Design (OOD) focuses on the how. It involves defining the software objects and how they collaborate to fulfill the requirements identified during analysis.

1. Static Modeling: The Class Diagram

The Class Diagram is the heart of OOD. It describes the structure of a system by showing its classes, their attributes, operations, and the relationships among objects.

Class Structure

A class is represented by a rectangle divided into three parts:

  1. Top: Class Name.
  2. Middle: Attributes (Fields).
  3. Bottom: Methods (Operations).

Relationships between Classes

Understanding relationships is critical for creating a modular system:

A. Association

A generic relationship where one class “uses” or “knows about” another.

  • Example: A Teacher teaches a Student.

B. Aggregation (Weak Has-A)

A “part-of” relationship where the “part” can exist independently of the “whole”.

  • Example: A Library has Books. If the library closes, the books still exist.
  • UML: Empty diamond arrow.

C. Composition (Strong Has-A)

A “part-of” relationship where the “part” cannot exist without the “whole”.

  • Example: A House has Rooms. If the house is destroyed, the rooms are too.
  • UML: Filled diamond arrow.

D. Generalization (Is-A)

Inheritance between a superclass and a subclass.

  • Example: A Dog is an Animal.
  • UML: Hollow triangle arrow.

UML Class Diagram Example (PlantUML)

@startuml
class Car {
  - String model
  - Engine engine
  + void start()
}

class Engine {
  - int horsepower
  + void ignite()
}

class Wheel {
  - double pressure
}

Car *-- Engine : composition
Car o-- Wheel : aggregation
@enduml

2. Dynamic Modeling: Sequence Diagrams

While class diagrams show static structure, Sequence Diagrams show how objects interact over time to perform a specific task (usually a use case).

Key Elements:

  • Lifelines: Vertical dashed lines representing an object’s existence over time.
  • Messages: Horizontal arrows showing data/calls between objects.
  • Activation bars: Thin rectangles on lifelines showing when an object is “active” or processing.

Sequence Diagram Example

@startuml
actor User
participant "LoginUI" as UI
participant "AuthService" as Auth
database "UserDB" as DB

User -> UI: Enter Credentials
UI -> Auth: login(username, pass)
Auth -> DB: fetchUser(username)
DB --> Auth: userObject
Auth -> Auth: validatePassword()
Auth --> UI: success
UI --> User: Show Dashboard
@enduml

3. Multiplicity in Design

Multiplicity defines how many instances of one class can be associated with one instance of another.

  • 1: Exactly one.
  • 0..1: Zero or one.
  • * or 0..*: Many.
  • 1..*: One or many.

4. OOD Best Practices

  1. Favor Composition over Inheritance: This promotes flexibility by allowing behavior to be changed at runtime.
  2. Program to an Interface, not an Implementation: Decouple your code from specific classes to make it easier to swap components.
  3. Keep Classes Small: A class should have one reason to change (Single Responsibility Principle).

5. Code Example: Translating UML to Java

Here is how a composition relationship looks in code:

// Engine class
class Engine {
    private int horsepower;
    public Engine(int hp) { this.horsepower = hp; }
    public void start() { System.out.println("Vroom!"); }
}

// Car class demonstrating Composition
public class Car {
    private final Engine engine; // Final implies the car 'owns' the engine's lifecycle

    public Car() {
        this.engine = new Engine(200); // Created when Car is created
    }

    public void drive() {
        engine.start();
        System.out.println("Car is moving");
    }
}

Key Takeaways

  • Class Diagrams represent the architectural blueprint.
  • Sequence Diagrams represent the execution flow.
  • A good design is decoupled, cohesive, and extensible.

Design Patterns

Section Detail

Singleton & Factory Patterns

Singleton & Factory Patterns

Design patterns are reusable solutions to common problems in software design. We begin our exploration with Creational Patterns, which handle object creation mechanisms, trying to create objects in a manner suitable to the situation.


1. The Singleton Pattern

The Problem

Sometimes you need exactly one instance of a class. For example, a configuration manager, a thread pool, or a database connection pool. If multiple instances are created, it could lead to inconsistent state or wasted resources.

The Solution

The Singleton pattern ensures a class has only one instance and provides a global point of access to it. It achieves this by:

  1. Making the constructor private.
  2. Providing a static method that returns the single instance.

Java Implementation (Thread-Safe)

public class DatabaseConnector {
    // 1. Private static instance
    private static volatile DatabaseConnector instance;

    // 2. Private constructor
    private DatabaseConnector() {
        // Initialize connection here
    }

    // 3. Static access method with Double-Checked Locking
    public static DatabaseConnector getInstance() {
        if (instance == null) {
            synchronized (DatabaseConnector.class) {
                if (instance == null) {
                    instance = new DatabaseConnector();
                }
            }
        }
        return instance;
    }

    public void query(String sql) {
        System.out.println("Executing: " + sql);
    }
}

Pros and Cons

  • Pros: Controlled access to a single instance, reduced memory footprint.
  • Cons: Can be difficult to unit test (global state), can hide dependencies.

2. The Factory Method Pattern

The Problem

Imagine a logistics app that initially only handles truck transport. If you want to add maritime transport, you’d have to change a lot of code everywhere you instantiate a Truck. Direct instantiation (new Truck()) couples your code to specific classes.

The Solution

The Factory Method pattern defines an interface for creating an object but lets subclasses decide which class to instantiate.

Python Example

from abc import ABC, abstractmethod

# Product Interface
class Transport(ABC):
    @abstractmethod
    def deliver(self):
        pass

# Concrete Products
class Truck(Transport):
    def deliver(self):
        return "Delivering by land in a box."

class Ship(Transport):
    def deliver(self):
        return "Delivering by sea in a container."

# Creator (Factory)
class Logistics(ABC):
    @abstractmethod
    def create_transport(self) -> Transport:
        pass

    def plan_delivery(self):
        transport = self.create_transport()
        return f"Logistics: {transport.deliver()}"

# Concrete Creators
class RoadLogistics(Logistics):
    def create_transport(self) -> Transport:
        return Truck()

class SeaLogistics(Logistics):
    def create_transport(self) -> Transport:
        return Ship()

# Usage
logistics = RoadLogistics()
print(logistics.plan_delivery())

When to use Factory Method?

  • When a class can’t anticipate the class of objects it must create.
  • When you want to provide users of your library with a way to extend its internal components.

3. Abstract Factory (Bonus)

While the Factory Method creates one product, the Abstract Factory creates families of related products.

Imagine a UI toolkit that needs to create Buttons, Checkboxes, and Sliders. You might have a WindowsFactory and a MacFactory. Each factory produces a set of components that “match” each other.

Key Difference

  • Factory Method: One method, one product.
  • Abstract Factory: Many methods, many related products.

Comparison Summary

PatternFocusUse Case
SingletonUniquenessResource managers, global state.
Factory MethodSubclassingWhen exact types are determined by subclasses.
Abstract FactoryFamiliesWhen products must work together (themes, OS).
Section Detail

Builder & Prototype Patterns

Builder & Prototype Patterns

In this lesson, we continue our journey through Creational Patterns by looking at the Builder and Prototype patterns. These patterns solve specific problems related to complex construction and efficient object duplication.


1. The Builder Pattern

The Problem

Imagine a Pizza class with 10+ optional parameters (size, crust, cheese, pepperoni, olives, onions, etc.). You end up with:

  1. Telescoping Constructors: A monster constructor with many arguments, many of which are often null or default.
  2. Poor Readability: new Pizza(12, true, false, true, "thin", null, ...) is impossible to read or maintain.

The Solution

The Builder pattern separates the construction of a complex object from its representation. It allows you to produce different types and representations of an object using the same construction code.

Java Example (Fluent API)

public class Pizza {
    private int size;
    private String crust;
    private boolean cheese;
    private boolean pepperoni;
    private boolean olives;

    // Private constructor - only accessible via the Builder
    private Pizza(Builder builder) {
        this.size = builder.size;
        this.crust = builder.crust;
        this.cheese = builder.cheese;
        this.pepperoni = builder.pepperoni;
        this.olives = builder.olives;
    }

    public static class Builder {
        private int size; // Required
        private String crust = "Regular"; // Default
        private boolean cheese = false;
        private boolean pepperoni = false;
        private boolean olives = false;

        public Builder(int size) { this.size = size; }

        public Builder setCrust(String crust) { this.crust = crust; return this; }
        public Builder addCheese() { this.cheese = true; return this; }
        public Builder addPepperoni() { this.pepperoni = true; return this; }
        
        public Pizza build() {
            return new Pizza(this);
        }
    }
}

// Usage
Pizza myPizza = new Pizza.Builder(12)
                    .setCrust("Thin")
                    .addCheese()
                    .addPepperoni()
                    .build();

2. The Prototype Pattern

The Problem

Creating a new object from scratch can sometimes be expensive (e.g., it requires a database query or a complex calculation). If you already have an object that is similar to what you need, it might be more efficient to simply copy it.

However, standard assignment (obj1 = obj2) only copies the reference in most languages. You need a “deep copy.”

The Solution

The Prototype pattern delegates the cloning process to the actual objects that are being cloned. The pattern declares a common interface for all objects that support cloning.

C++ Example

#include <iostream>
#include <string>
#include <memory>

class Shape {
public:
    virtual ~Shape() {}
    virtual std::unique_ptr<Shape> clone() const = 0;
    virtual void draw() const = 0;
};

class Circle : public Shape {
    int radius;
public:
    Circle(int r) : radius(r) {}
    
    // Cloning logic
    std::unique_ptr<Shape> clone() const override {
        return std::make_unique<Circle>(*this);
    }
    
    void draw() const override {
        std::cout << "Drawing Circle with radius: " << radius << std::endl;
    }
};

int main() {
    Circle prototypeCircle(10);
    
    // Instead of 'new', we clone
    auto clonedCircle = prototypeCircle.clone();
    clonedCircle->draw();
    
    return 0;
}

3. Comparison and Synergy

Builder vs. Abstract Factory

  • Builder focuses on constructing a complex object step-by-step.
  • Abstract Factory focuses on families of product objects (either simple or complex).
  • Builder returns the product as a final step, whereas the Abstract Factory returns the product immediately.

When to use Prototype?

  • When the classes to instantiate are specified at runtime.
  • When you want to avoid a hierarchy of factories.
  • When instances of a class can have one of only a few different combinations of state.

Deep Copy vs. Shallow Copy

A critical concept in the Prototype pattern is how the object is cloned:

  1. Shallow Copy: Copies the object’s top-level fields. If a field is a reference to another object, both the original and the clone will point to the same memory location.
  2. Deep Copy: Recursively copies all objects referenced by the original object. The clone is entirely independent.

In most Prototype implementations, a Deep Copy is preferred to ensure that modifying the clone does not inadvertently change the original object.


Key Takeaways

  • Use Builder when you have a “Configuration Explosion” in your constructors.
  • Use Prototype when object creation is costly and you want to reduce overhead by copying existing instances.
  • Both patterns help keep your client code decoupled from the specific implementation details of object creation.
Section Detail

Adapter & Bridge Patterns

Adapter & Bridge Patterns

We now move into Structural Patterns, which explain how to assemble objects and classes into larger structures while keeping these structures flexible and efficient.


1. The Adapter Pattern

The Problem

You have an existing class (Legacy or 3rd-party) that provides the functionality you need, but its interface doesn’t match the one your application uses. You cannot change the legacy code. This is common when integrating new libraries into an existing codebase.

The Solution

The Adapter acts as a wrapper between two objects. It catches calls for one object and transforms them to format and interface recognizable by the second object.

Class Adapter vs. Object Adapter

There are two ways to implement this:

  1. Object Adapter (Composition): The adapter contains an instance of the legacy class. This is the most flexible approach and is used in the example below.
  2. Class Adapter (Inheritance): The adapter inherits from both the target interface and the legacy class. This requires multiple inheritance, which is not supported in all languages (like Java).

Python Example (Object Adapter)

# Target Interface (what our app expects)
class ModernUSB:
    def connect_usb_c(self):
        pass

# Adaptee (existing/legacy component)
class OldMemoryStick:
    def plug_in_usb_a(self):
        return "Connected to old USB-A port."

# Adapter
class USBAdapter(ModernUSB):
    def __init__(self, old_stick: OldMemoryStick):
        self.old_stick = old_stick

    def connect_usb_c(self):
        # Translate the call
        return self.old_stick.plug_in_usb_a()

# Usage
legacy_stick = OldMemoryStick()
adapter = USBAdapter(legacy_stick)

# Our app thinks it's talking to a ModernUSB device
print(adapter.connect_usb_c())

Key Concept

The Adapter is often called a Wrapper. It provides a different interface to its subject.


2. The Bridge Pattern

The Problem

If you have a Shape class with subclasses Circle and Square, and you want to add colors (Red and Blue), you might end up with RedCircle, BlueCircle, RedSquare, and BlueSquare. Adding a NEW shape or a NEW color results in a “Cartesian product” explosion of classes.

The Solution

The Bridge pattern suggests that you extract one of the dimensions (e.g., Color) into a separate class hierarchy. Now the original class (Shape) has a reference to an object of the new hierarchy. This reference acts as a “bridge” between the Abstraction and the Implementation.

Java Example

// Implementation Interface
interface Device {
    void setVolume(int percent);
}

// Concrete Implementations
class TV implements Device {
    public void setVolume(int p) { System.out.println("TV volume: " + p); }
}

class Radio implements Device {
    public void setVolume(int p) { System.out.println("Radio volume: " + p); }
}

// Abstraction
abstract class RemoteControl {
    protected Device device;
    public RemoteControl(Device device) { this.device = device; }
    public abstract void volumeUp();
}

// Refined Abstraction
class AdvancedRemote extends RemoteControl {
    private int volume = 10;
    public AdvancedRemote(Device device) { super(device); }
    
    public void volumeUp() {
        volume += 10;
        device.setVolume(volume);
    }
}

// Usage
RemoteControl remote = new AdvancedRemote(new TV());
remote.volumeUp(); // Bridges the remote command to the TV implementation

3. Adapter vs. Bridge

While they look similar, they have different intents:

  • Adapter is used to make existing classes work together. It’s usually applied after the system is designed to resolve incompatibilities.
  • Bridge is used up-front in the design phase to let abstractions and implementations vary independently. It’s about decoupling.

Use Cases

  • Use Adapter when you need to use a 3rd party library whose interface doesn’t match your code.
  • Use Bridge when you want to avoid a permanent binding between an abstraction and its implementation (e.g., when you switch between cross-platform UI toolkits).

Summary Table

PatternGoalRelationship
AdapterInterface CompatibilityWrapper around one object
BridgeDecouplingLink between two hierarchies

By using these patterns, you can create systems that are easier to maintain because changes in one part of the system (like the implementation detail of a TV) don’t force changes in another part (the remote control logic).

Section Detail

Composite & Decorator Patterns

Composite & Decorator Patterns

Continuing our journey through Structural Patterns, we look at two patterns that help manage the relationship between objects and their compositions. While both involve wrapping or containing objects, they serve very different architectural purposes.


1. The Composite Pattern

The Problem

Imagine you are building a file system representation. You have File objects and Folder objects. Folders can contain both Files and other Folders. If you want to calculate the total size of a Folder, you have to check every item: if it’s a File, add its size; if it’s a Folder, recursively call its size method. This makes the client code complex as it must treat individual objects and groups of objects differently.

The Solution

The Composite pattern allows you to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.

By defining a common interface for both the simple elements (Leaves) and the containers (Composites), the client can interact with any object in the tree without knowing its exact concrete class.

Java Implementation

import java.util.ArrayList;
import java.util.List;

// 1. Component Interface
interface FileSystemComponent {
    void showDetails();
    int getSize();
}

// 2. Leaf Object
class File implements FileSystemComponent {
    private String name;
    private int size;

    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }

    public void showDetails() {
        System.out.println("File: " + name + " (" + size + "KB)");
    }

    public int getSize() {
        return size;
    }
}

// 3. Composite Object
class Folder implements FileSystemComponent {
    private String name;
    private List<FileSystemComponent> components = new ArrayList<>();

    public Folder(String name) {
        this.name = name;
    }

    public void addComponent(FileSystemComponent component) {
        components.add(component);
    }

    public void showDetails() {
        System.out.println("Folder: " + name);
        for (FileSystemComponent component : components) {
            component.showDetails();
        }
    }

    public int getSize() {
        int totalSize = 0;
        for (FileSystemComponent component : components) {
            totalSize += component.getSize();
        }
        return totalSize;
    }
}

Pros and Cons

  • Pros: Simplifies client code by treating complex tree structures as single objects. Makes it easy to add new types of components.
  • Cons: Can make the design overly general; it might be difficult to restrict which components can be added to a folder at compile time.

2. The Decorator Pattern

The Problem

Suppose you have a Coffee class. You want to offer options like Milk, Sugar, or WhippedCream. If you use inheritance, you end up with a “class explosion”: CoffeeWithMilk, CoffeeWithSugar, CoffeeWithMilkAndSugar, etc. Static inheritance is inflexible because you cannot add behavior at runtime.

The Solution

The Decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Instead of inheriting from the base class, you “wrap” the base object inside a decorator object that implements the same interface and adds its own behavior before or after delegating to the wrapped object.

Python Example

from abc import ABC, abstractmethod

# 1. Component Interface
class Coffee(ABC):
    @abstractmethod
    def get_cost(self):
        pass

    @abstractmethod
    def get_description(self):
        pass

# 2. Concrete Component
class SimpleCoffee(Coffee):
    def get_cost(self):
        return 2.0

    def get_description(self):
        return "Simple Coffee"

# 3. Base Decorator
class CoffeeDecorator(Coffee):
    def __init__(self, coffee: Coffee):
        self._decorated_coffee = coffee

    def get_cost(self):
        return self._decorated_coffee.get_cost()

    def get_description(self):
        return self._decorated_coffee.get_description()

# 4. Concrete Decorators
class MilkDecorator(CoffeeDecorator):
    def get_cost(self):
        return super().get_cost() + 0.5

    def get_description(self):
        return super().get_description() + ", Milk"

class SugarDecorator(CoffeeDecorator):
    def get_cost(self):
        return super().get_cost() + 0.2

    def get_description(self):
        return super().get_description() + ", Sugar"

# Usage
my_coffee = SimpleCoffee()
my_coffee = MilkDecorator(my_coffee)
my_coffee = SugarDecorator(my_coffee)

print(f"Order: {my_coffee.get_description()}")
print(f"Total: ${my_coffee.get_cost():.2f}")

Pros and Cons

  • Pros: Greater flexibility than static inheritance. Allows for “mix-and-match” behavior at runtime. Adheres to the Single Responsibility Principle (each decorator handles one feature).
  • Cons: Can result in many small objects that look alike, making debugging harder. The order of decorators might matter, which can be a source of bugs.
Section Detail

Facade & Proxy Patterns

Facade & Proxy Patterns

Structural patterns provide various ways to manage relationships between classes and objects. The Facade and Proxy patterns both wrap objects, but while the Facade simplifies an interface, the Proxy controls access to an object.


1. The Facade Pattern

The Problem

Large systems often consist of dozens of classes, each with its own complex API. For a client to perform a simple task, it might need to interact with many of these classes in a specific order. This couples the client code to the internal implementation details of the subsystem, making it hard to maintain and understand.

The Solution

The Facade pattern provides a simplified, higher-level interface to a complex set of classes in a subsystem. Components of the subsystem still exist and can be used directly if needed, but the Facade provides a “shortcut” for the most common use cases.

Think of it like the front desk of a hotel: you talk to the clerk (the Facade) to check in, and they handle the communication with the cleaners, the luggage handlers, and the accounting department (the Subsystem).

Java Implementation

// Subsystem Classes
class Amplifier { void on() { System.out.println("Amp on"); } }
class DvdPlayer { void play(String movie) { System.out.println("Playing " + movie); } }
class Projector { void wideScreenMode() { System.out.println("Projector in widescreen"); } }

// The Facade
class HomeTheaterFacade {
    private Amplifier amp;
    private DvdPlayer dvd;
    private Projector projector;

    public HomeTheaterFacade(Amplifier amp, DvdPlayer dvd, Projector projector) {
        this.amp = amp;
        this.dvd = dvd;
        this.projector = projector;
    }

    public void watchMovie(String movie) {
        System.out.println("Get ready to watch a movie...");
        projector.wideScreenMode();
        amp.on();
        dvd.play(movie);
    }
}

// Client Code
public class Main {
    public static void main(String[] args) {
        HomeTheaterFacade theater = new HomeTheaterFacade(new Amplifier(), new DvdPlayer(), new Projector());
        theater.watchMovie("Inception");
    }
}

Pros and Cons

  • Pros: Reduces coupling between clients and the subsystem. Promotes the Principle of Least Knowledge (Law of Demeter).
  • Cons: The Facade can become a “god object” tied to every class of an app if not carefully designed.

2. The Proxy Pattern

The Problem

Sometimes an object is “expensive” to create (like a high-resolution image or a remote database connection), or you need to control who can access certain methods, or you want to log every time a method is called. You don’t want the client to deal with these complexities directly.

The Solution

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. The Proxy implements the same interface as the original object (the “Real Subject”). When the client calls the Proxy, it can perform additional logic (like lazy initialization, access control, or logging) before or after forwarding the call to the Real Subject.

Python Example (Virtual Proxy)

from abc import ABC, abstractmethod
import time

# Subject Interface
class Image(ABC):
    @abstractmethod
    def display(self):
        pass

# Real Subject
class RealImage(Image):
    def __init__(self, filename):
        self.filename = filename
        self._load_from_disk()

    def _load_from_disk(self):
        print(f"Loading {self.filename}...")
        time.sleep(2)  # Simulate expensive loading

    def display(self):
        print(f"Displaying {self.filename}")

# Proxy
class ProxyImage(Image):
    def __init__(self, filename):
        self.filename = filename
        self.real_image = None

    def display(self):
        # Lazy Initialization (Virtual Proxy)
        if self.real_image is None:
            self.real_image = RealImage(self.filename)
        self.real_image.display()

# Usage
image = ProxyImage("high_res_photo.jpg")
# The image is NOT loaded yet
print("Proxy created. Client is doing other work...")

# Image is loaded only when display() is called
image.display()
# Second call is fast because image is already loaded
image.display()

Types of Proxies

  1. Virtual Proxy: Delays creation of expensive objects until they are actually needed (as shown above).
  2. Protection Proxy: Controls access based on permissions.
  3. Remote Proxy: Represents an object located in a different address space (e.g., via a network).
  4. Logging Proxy: Keeps a log of requests to the service.

Pros and Cons

  • Pros: Allows for optimization (lazy loading), security (access control), and monitoring without changing the client or the original object.
  • Cons: Can introduce latency (the proxy adds a layer) and the code might become more complicated due to many classes.
Section Detail

Observer & Strategy Patterns

Observer & Strategy Patterns

We now move into Behavioral Patterns, which deal with communication between objects and how responsibilities are assigned. These patterns help manage complex control flows and make your system more flexible to change.


1. The Observer Pattern

The Problem

Imagine a Weather Station that tracks temperature. Multiple displays (mobile app, web dashboard, physical LED screen) need to update whenever the temperature changes. If the Weather Station keeps track of every display and calls them directly, it becomes tightly coupled to those displays. Adding a new type of display would require modifying the Weather Station code.

The Solution

The Observer pattern defines a one-to-many dependency between objects so that when one object (the Subject) changes state, all its dependents (Observers) are notified and updated automatically.

This decouples the Subject from its Observers. The Subject only knows that its observers implement a specific Observer interface.

Java Implementation

import java.util.ArrayList;
import java.util.List;

// 1. Observer Interface
interface Observer {
    void update(float temp);
}

// 2. Subject (Observable)
class WeatherStation {
    private List<Observer> observers = new ArrayList<>();
    private float temperature;

    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    public void setTemperature(float temp) {
        this.temperature = temp;
        notifyObservers();
    }

    private void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(temperature);
        }
    }
}

// 3. Concrete Observers
class PhoneDisplay implements Observer {
    public void update(float temp) {
        System.out.println("Phone Display: Temp updated to " + temp);
    }
}

class WindowDisplay implements Observer {
    public void update(float temp) {
        System.out.println("Window Display: Temp updated to " + temp);
    }
}

Pros and Cons

  • Pros: Established “loose coupling” between objects. Supports the Open/Closed Principle (you can add new observers without changing the subject).
  • Cons: Observers are notified in random order. If not careful, you can create memory leaks if observers aren’t properly removed.

2. The Strategy Pattern

The Problem

Suppose you have a Navigator app. It started with a Walk route algorithm. Later, you added Road and PublicTransport. If you use a single class with many if-else or switch statements to handle these different routing logics, the class becomes massive and hard to maintain.

The Solution

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

Instead of the Navigator class implementing all algorithms, it delegates the work to a “strategy” object. The client can swap the strategy at runtime.

Python Example

from abc import ABC, abstractmethod

# 1. Strategy Interface
class RouteStrategy(ABC):
    @abstractmethod
    def build_route(self, start, end):
        pass

# 2. Concrete Strategies
class WalkingStrategy(RouteStrategy):
    def build_route(self, start, end):
        return f"Walking route from {start} to {end}: 20 mins."

class DrivingStrategy(RouteStrategy):
    def build_route(self, start, end):
        return f"Driving route from {start} to {end}: 5 mins (heavy traffic)."

# 3. Context
class Navigator:
    def __init__(self, strategy: RouteStrategy):
        self._strategy = strategy

    def set_strategy(self, strategy: RouteStrategy):
        self._strategy = strategy

    def execute_route(self, start, end):
        return self._strategy.build_route(start, end)

# Usage
nav = Navigator(WalkingStrategy())
print(nav.execute_route("Home", "Gym"))

# Change strategy at runtime
nav.set_strategy(DrivingStrategy())
print(nav.execute_route("Home", "Gym"))

Strategy vs. State

Wait, isn’t Strategy similar to the State pattern? Yes, the structure is similar, but the intent is different:

  • Strategy is about algorithm selection (the client usually chooses the strategy).
  • State is about lifecycle management (the object changes its own state automatically).

Pros and Cons

  • Pros: Swapping algorithms at runtime. Isolate implementation details of an algorithm. Avoid massive conditional statements.
  • Cons: The client must be aware of the different strategies to choose the right one. Increases the number of classes in the system.
Section Detail

Command & State Patterns

Command & State Patterns

Behavioral patterns allow us to manage the “how” of object interaction. The Command pattern focuses on encapsulating requests for later execution or undoing, while the State pattern focuses on how object behavior changes as its internal state evolves.


1. The Command Pattern

The Problem

If you’re building a GUI with buttons, you might have a “Save” button and a “Copy” button. You don’t want the Button class to know exactly what logic to execute (e.g., saveDatabase() or copyToClipboard()). If you hardcode this logic into the Button class, you can’t reuse it for other purposes (like a keyboard shortcut or a menu item).

The Solution

The Command pattern turns a request into a stand-alone object that contains all information about the request. This transformation lets you pass requests as method arguments, delay or queue a request’s execution, and support undoable operations.

The setup usually involves:

  • Command: Declares the interface.
  • ConcreteCommand: Implements the call to the Receiver.
  • Receiver: Knows how to perform the actual work (the “Logic”).
  • Invoker: Triggers the command (e.g., the Button).

Java Implementation (with Undo)

// 1. Command Interface
interface Command {
    void execute();
    void undo();
}

// 2. Receiver
class Light {
    public void on() { System.out.println("Light is ON"); }
    public void off() { System.out.println("Light is OFF"); }
}

// 3. Concrete Command
class LightOnCommand implements Command {
    private Light light;
    public LightOnCommand(Light light) { this.light = light; }

    public void execute() { light.on(); }
    public void undo() { light.off(); }
}

// 4. Invoker
class RemoteControl {
    private Command command;
    public void setCommand(Command command) { this.command = command; }
    public void pressButton() { command.execute(); }
    public void pressUndo() { command.undo(); }
}

Pros and Cons

  • Pros: Decouples the object that triggers the operation from the one that knows how to perform it. Supports Undo/Redo.
  • Cons: The code can become quite complicated since you’re introducing a whole new layer between senders and receivers.

2. The State Pattern

The Problem

Consider a Document class in an editor. It can be in one of three states: Draft, Moderation, or Published. In each state, the publish() method behaves differently:

  • In Draft, it moves the doc to Moderation.
  • In Moderation, it makes the doc Published if the user is an admin.
  • In Published, it does nothing. Using many if-else or switch statements for every method of the Document class leads to messy code that is hard to maintain.

The Solution

The State pattern lets an object alter its behavior when its internal state changes. The object will appear to change its class.

The key is to extract state-specific behaviors into separate “State” classes and delegate the work to the current state object.

Python Example

from abc import ABC, abstractmethod

# 1. State Interface
class State(ABC):
    @abstractmethod
    def publish(self, document):
        pass

# 2. Concrete States
class DraftState(State):
    def publish(self, document):
        print("Moving document from Draft to Moderation.")
        document.set_state(ModerationState())

class ModerationState(State):
    def publish(self, document):
        print("Reviewing document... Approved! Moving to Published.")
        document.set_state(PublishedState())

class PublishedState(State):
    def publish(self, document):
        print("Document is already published. Doing nothing.")

# 3. Context (Document)
class Document:
    def __init__(self):
        self._state = DraftState()

    def set_state(self, state: State):
        self._state = state

    def publish(self):
        self._state.publish(self)

# Usage
doc = Document()
doc.publish() # Moves to Moderation
doc.publish() # Moves to Published
doc.publish() # Already published

Strategy vs. State

While the class diagrams look identical (a Context class holding a reference to an Interface), the intent is different:

  • State: Objects know about each other and transition from one state to another.
  • Strategy: Strategies are usually independent and unaware of each other; the client chooses which one to use.

Pros and Cons

  • Pros: Adheres to the Single Responsibility Principle. Eliminates massive conditional state machines.
  • Cons: Can be overkill if a state machine has only a few states or rarely changes.
Section Detail

Template Method & Iterator Patterns

Template Method & Iterator Patterns

Our final look at Behavioral Patterns covers two patterns that optimize how we structure algorithms and how we traverse data. Both patterns are heavily used in modern frameworks and standard libraries.


1. The Template Method Pattern

The Problem

Imagine you are writing a data mining application that extracts information from various document formats (PDF, DOC, CSV). Most of the steps are identical: open the file, extract data, parse data, analyze data, and close the file. However, the extraction and parsing logic differs for each format. If you duplicate the skeleton logic in every class, you’ll have a maintenance nightmare.

The Solution

The Template Method pattern defines the skeleton of an algorithm in a method, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.

The base class defines “hooks” or abstract methods that subclasses must implement, while the “template method” itself is usually final (in Java) or intended not to be overridden.

Java Implementation

abstract class DataMiner {
    // 1. The Template Method
    public final void mine(String path) {
        openFile(path);
        extractData();
        parseData();
        analyzeData();
        closeFile();
    }

    // 2. Concrete steps (same for all)
    private void analyzeData() { System.out.println("Analyzing data..."); }
    private void closeFile() { System.out.println("Closing file."); }

    // 3. Abstract steps (different for each)
    protected abstract void openFile(String path);
    protected abstract void extractData();
    protected abstract void parseData();
}

class PdfDataMiner extends DataMiner {
    protected void openFile(String path) { System.out.println("Opening PDF: " + path); }
    protected void extractData() { System.out.println("Extracting text from PDF..."); }
    protected void parseData() { System.out.println("Parsing PDF structure..."); }
}

Pros and Cons

  • Pros: Promotes code reuse by pulling shared logic into a superclass. Provides a rigid structure that subclasses can’t easily break.
  • Cons: Some clients might be limited by the provided skeleton. Violates the Liskov Substitution Principle if you’re not careful.

2. The Iterator Pattern

The Problem

A collection is just a container for a group of objects. It could be a List, a Stack, a Tree, or a Graph. If you want to traverse these collections, you need various methods. A Tree requires Depth-First or Breadth-First traversal. If the client code has to handle these different traversal logics, it becomes tightly coupled to the internal structure of the collection.

The Solution

The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation (List, Stack, Tree, etc.).

By implementing an Iterator interface, different collections can provide their own iterators, allowing the client to traverse them using the same consistent API (e.g., hasNext() and next()).

Python Example

from collections.abc import Iterable, Iterator

# Concrete Iterator
class AlphabeticalOrderIterator(Iterator):
    def __init__(self, collection, reverse: bool = False):
        self._collection = collection
        self._reverse = reverse
        self._position = -1 if reverse else 0

    def __next__(self):
        try:
            value = self._collection[self._position]
            self._position += -1 if self._reverse else 1
        except IndexError:
            raise StopIteration()
        return value

# Concrete Collection
class WordCollection(Iterable):
    def __init__(self, collection = []):
        self._collection = collection

    def __iter__(self) -> AlphabeticalOrderIterator:
        return AlphabeticalOrderIterator(self._collection)

    def get_reverse_iterator(self) -> AlphabeticalOrderIterator:
        return AlphabeticalOrderIterator(self._collection, True)

    def add_item(self, item):
        self._collection.append(item)

# Usage
words = WordCollection()
words.add_item("First")
words.add_item("Second")
words.add_item("Third")

print("Straight traversal:")
for item in words:
    print(item)

print("\nReverse traversal:")
for item in words.get_reverse_iterator():
    print(item)

Iterator in Modern Languages

Most modern languages (Java, Python, C#, JavaScript) have built-in support for the Iterator pattern. In Java, it’s the Iterator<T> interface and foreach loops. In Python, it’s the __iter__ and __next__ methods.

Pros and Cons

  • Pros: Clean client code (no complex loops). Multiple traversals can happen simultaneously. You can delay traversal (lazy loading).
  • Cons: Can be overkill for simple collections. Might be slightly less efficient than a direct loop for some data structures.

Architecture & Quality

Section Detail

Principles of Software Architecture

Principles of Software Architecture

Software architecture refers to the high-level structures of a software system, the discipline of creating such structures, and the documentation of these structures. It is the blueprint of the system and provides an abstraction to manage the complexity and establish a communication and coordination mechanism among components.

What is Software Architecture?

Architecture is about the significant decisions—decisions that are expensive to change. It defines how the system is partitioned into components, how those components interact (interfaces), and the constraints under which they operate.

Key Architectural Principles

1. Separation of Concerns (SoC)

SoC is a design principle for separating a computer program into distinct sections such that each section addresses a separate concern. A concern is a set of information that affects the code of a computer program.

2. Low Coupling and High Cohesion

  • Cohesion refers to the degree to which the elements inside a module belong together. We strive for High Cohesion.
  • Coupling is the degree of interdependence between software modules. We strive for Low Coupling.

3. Least Knowledge (Law of Demeter)

A component should have limited knowledge about other units: only units “closely” related to the current unit. In terms of code, an object should only call methods on itself, its fields, or its parameters.

4. Directing Dependency (Dependency Rule)

Dependencies should point towards higher-level policies and abstractions, not towards low-level details (like databases or UIs). This is closely related to the Dependency Inversion Principle (DIP).

Common Architectural Patterns

Architectural patterns provide a template for solving recurring design problems in a particular context.

Layers Pattern

This pattern organizes the system into layers, where each layer provides services to the layer above it and uses services from the layer below. Typical layers include Presentation, Business, and Data Access.

Microservices

An architectural style that structures an application as a collection of services that are highly maintainable, testable, loosely coupled, and independently deployable.

Event-Driven Architecture (EDA)

In EDA, components communicate by producing and consuming events. This allows for high scalability and decoupling between the producer and consumer.

Architecture for Scale and Resiliency

In modern distributed systems, architecture must account for failure as a first-class citizen and ensure the system can handle growth.

Horizontal vs. Vertical Scaling

  • Vertical Scaling (Scaling Up): Adding more resources (CPU, RAM) to an existing server. It has a hard ceiling and carries a single point of failure.
  • Horizontal Scaling (Scaling Out): Adding more servers to the pool. This is the preferred approach for high availability and cloud-native systems.

Resiliency Patterns

To prevent a failure in one service from cascading through the entire system, architects use resiliency patterns:

  • Circuit Breaker: Detects failures and encapsulates the logic of preventing a failure from constantly recurring, similar to an electrical circuit breaker. It “trips” after a certain number of failures, giving the failing service time to recover.
  • Bulkhead: Segregates resources into different “pools” such that if one group fails, the others continue to work. For example, assigning separate thread pools for different APIs.
  • Retry Pattern: Automatically retries a failed operation, which is useful for transient faults like temporary network glitches.

Quality Attributes (The “-ilities”)

Architecture is judged by its ability to satisfy quality attributes, often called non-functional requirements:

  • Scalability: The ability of the system to handle increased load.
  • Maintainability: The ease with which a system can be modified.
  • Reliability: The ability of a system to remain functional under pressure.
  • Availability: The proportion of time the system is functional and reachable.

Example: N-Tier Architecture with Java

// Logic Layer (Service)
public class OrderService {
    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    public void processOrder(Order order) {
        // Business logic here
        repository.save(order);
    }
}

// Data Access Layer (Repository)
public interface OrderRepository {
    void save(Order order);
}

In this example, the OrderService depends on an abstraction (OrderRepository), following the principle of Dependency Inversion and Separation of Concerns.

Testing

Section Detail

Unit Testing Principles

Unit Testing Principles

Unit testing is a software testing method where individual units or components of a software are tested. The purpose is to validate that each unit of the software code performs as expected. A unit is the smallest testable part of any software; it usually has one or a few inputs and usually a single output.

The Importance of Unit Testing

  • Early Defect Detection: Bugs caught during unit testing are significantly cheaper to fix than those found in production.
  • Refactoring Confidence: Unit tests act as a safety net. You can clean up or optimize code knowing that the tests will catch any regressions.
  • Documentation: Tests serve as executable documentation, showing how an API or class is intended to be used.

The FIRST Properties

Good unit tests follow the FIRST acronym:

  1. Fast: Tests should run quickly so that developers can run them frequently.
  2. Independent: Tests should not depend on each other or on a specific run order.
  3. Repeatable: A test should yield the same result every time it is run in any environment.
  4. Self-Validating: Tests should have a clear binary output (pass or fail) without requiring manual interpretation.
  5. Thorough/Timely: Tests should cover all boundary conditions and be written ideally at the same time as the code.

The AAA Pattern

A common structure for unit tests is Arrange, Act, Assert:

  • Arrange: Set up the test conditions and inputs.
  • Act: Invoke the method or function under test.
  • Assert: Verify that the output or state change matches expectations.

Test Doubles (Mocks and Stubs)

When a unit has dependencies (like a database or a web service), we use test doubles to isolate the unit:

  • Stubs: Provide canned answers to calls made during the test.
  • Mocks: Record expectations about how they are called and can be verified.

Measuring Test Quality

Code Coverage

Code coverage measures the percentage of code executed during the test suite. Common metrics include:

  • Statement Coverage: Which lines of code were executed?
  • Branch Coverage: Were all branches (e.g., if/else paths) taken?
  • Path Coverage: Were all possible execution paths through the function tested?

Crucial Note: 100% code coverage does not mean 100% of the logic is correct or that the code is well-tested. It only means the code was executed.

Mutation Testing

Mutation testing is a more advanced technique where the testing tool automatically makes small changes (mutations) to the production code (e.g., changing a > to a <). If the test suite still passes after a mutation, the test is “weak” because it failed to catch the regression.

Example: Unit Testing with Python (PyTest)

Consider a simple calculator class:

# calculator.py
class Calculator:
    def add(self, a, b):
        return a + b

    def divide(self, a, b):
        if b == 0:
            raise ValueError("Cannot divide by zero")
        return a / b

The corresponding unit test using pytest:

# test_calculator.py
import pytest
from calculator import Calculator

def test_add():
    # Arrange
    calc = Calculator()
    
    # Act
    result = calc.add(2, 3)
    
    # Assert
    assert result == 5

def test_divide():
    calc = Calculator()
    assert calc.divide(10, 2) == 5

def test_divide_by_zero():
    calc = Calculator()
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        calc.divide(10, 0)

Java Example (JUnit 5)

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class CalculatorTest {

    @Test
    void testAdd() {
        Calculator calc = new Calculator();
        assertEquals(5, calc.add(2, 3), "2 + 3 should equal 5");
    }

    @Test
    void testException() {
        Calculator calc = new Calculator();
        assertThrows(IllegalArgumentException.class, () -> {
            calc.divide(1, 0);
        });
    }
}

By adhering to these principles, unit tests become a powerful tool for ensuring code quality and long-term maintainability.

Section Detail

Test-Driven Development (TDD)

Test-Driven Development (TDD)

Test-Driven Development (TDD) is a software development process where you write a test before you write the actual production code. It is a reversal of the traditional development process. TDD is not just a testing technique; it is a design process that ensures your code is testable, modular, and fulfills requirements.

The Red-Green-Refactor Cycle

The core of TDD is a simple repetitive cycle:

  1. 🔴 Red: Write a small, failing test for a new piece of functionality. The test should not even compile initially (if applicable) or should fail upon execution because the implementation doesn’t exist.
  2. 🟢 Green: Write the minimum amount of code necessary to make the test pass. Don’t worry about elegant code at this stage; just get it working.
  3. 🔵 Refactor: Clean up the code while ensuring all tests still pass. This includes removing duplication, improving naming, and ensuring the design is sound.

The Three Laws of TDD

Robert C. Martin (Uncle Bob) popularized three rules for TDD:

  1. You are not allowed to write any production code unless it is to make a failing unit test pass.
  2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

Benefits of TDD

  • Reduced Debugging Time: Since you test every small change, you know exactly where the bug is if a test fails.
  • Cleaner Architecture: TDD forces you to think about interfaces first, leading to more modularized code.
  • Documentation: The test suite becomes a living specification of the system.
  • Elimination of Fear: You can make large changes or refactors with the confidence that the tests will catch any breaks.

TDD and The Testing Pyramid

TDD is most effective when applied at the base of the “Testing Pyramid.” The pyramid suggests:

  • Unit Tests (Base): Largest number of tests. Fast and cheap.
  • Integration Tests (Middle): Fewer tests. Slower, testing interactions.
  • UI/E2E Tests (Top): Fewest tests. Very slow and brittle.

By focusing TDD on unit tests, you build a solid foundation of correctness that allows you to move faster.

ATDD: Acceptance Test-Driven Development

ATDD is an extension of TDD where the entire team (Business, Dev, QA) collaborates to define acceptance tests before development starts. These tests are written from the perspective of the user and define the “Definition of Done.” ATDD aligns closely with BDD (Behavior-Driven Development).

Example: TDD Workflow in TypeScript (Jest)

Imagine we are building a FizzBuzz function.

Phase 1: Red (Test for ‘1’)

// fizzbuzz.test.ts
import { fizzBuzz } from './fizzbuzz';

test('returns "1" for 1', () => {
  expect(fizzBuzz(1)).toBe("1");
});

This fails because fizzBuzz is not defined.

Phase 2: Green

// fizzbuzz.ts
export function fizzBuzz(n: number): string {
  return "1"; // Minimal code to pass
}

The test passes.

Phase 3: Red (Test for ‘3’)

test('returns "Fizz" for 3', () => {
  expect(fizzBuzz(3)).toBe("Fizz");
});

This fails because it returns “1”.

Phase 4: Green

export function fizzBuzz(n: number): string {
  if (n === 3) return "Fizz";
  return "1";
}

Phase 5: Refactor

As we add more tests (for 5, 15, etc.), the implementation will grow.

export function fizzBuzz(n: number): string {
  if (n % 15 === 0) return "FizzBuzz";
  if (n % 3 === 0) return "Fizz";
  if (n % 5 === 0) return "Buzz";
  return n.toString();
}

By following TDD, we ensure that every line of code is justified by a test case.

Section Detail

Behavior-Driven Development (BDD)

Behavior-Driven Development (BDD)

Behavior-Driven Development (BDD) is an agile software development process that encourages collaboration among developers, QA, and non-technical or business participants in a software project. It evolved from Test-Driven Development (TDD) by focusing on the behavior of the system from the user’s perspective.

The Core Concept

While TDD focuses on how a unit of code works, BDD focuses on what the system does for the user. It uses a domain-specific language (DSL) that is readable by both technical and non-technical stakeholders.

Collaborative BDD: The “Three Amigos”

BDD is as much about communication as it is about automation. The “Three Amigos” is a practice where three key perspectives meet to discuss a requirement before it is implemented:

  1. Business (Product Owner): What problem are we solving?
  2. Development: How will we implement the solution?
  3. Testing (QA): What about the edge cases? What could go wrong?

This collaboration results in a shared understanding and a set of concrete examples that form the basis of the Gherkin scenarios.

Specification by Example (SBE)

BDD is often implemented through Specification by Example. Instead of abstract requirements, the team uses concrete examples of the system in action.

  • Requirement: “The system should calculate discount for bulk orders.”
  • Example: “Given a user adds 10 items to the cart, when they check out, a 5% discount should be applied.”

Gherkin Syntax: Given / When / Then

BDD scenarios are often written in Gherkin, a plain-text language that follows a specific structure:

  • Feature: A high-level description of a software feature.
  • Scenario: A specific example illustrating how the feature should behave.
  • Given: The initial context or “preconditions.”
  • When: The action taken by the user or the event that occurs.
  • Then: The expected outcome or “postconditions.”

Example Scenario (Cucumber/Gherkin)

Feature: User Login
  As a registered user
  I want to log into my account
  So that I can access my personalized dashboard

  Scenario: Successful login with valid credentials
    Given I am on the login page
    When I enter "john.doe@example.com" in the email field
    And I enter "password123" in the password field
    And I click the "Login" button
    Then I should be redirected to the dashboard
    And I should see a welcome message "Welcome, John!"

Step Definitions (JavaScript with Cucumber.js)

The scenarios in Gherkin are mapped to “Step Definitions” in code.

const { Given, When, Then } = require('@cucumber/cucumber');
const assert = require('assert');

Given('I am on the login page', async function () {
  await browser.url('/login');
});

When('I enter {string} in the email field', async function (email) {
  await browser.setValue('#email', email);
});

When('I enter {string} in the password field', async function (password) {
  await browser.setValue('#password', password);
});

When('I click the {string} button', async function (buttonName) {
  await browser.click('#login-btn');
});

Then('I should be redirected to the dashboard', async function () {
  const url = await browser.getUrl();
  assert.strictEqual(url.includes('/dashboard'), true);
});

Benefits of BDD

  1. Shared Understanding: Eliminates the “lost in translation” problem between business owners and developers.
  2. Living Documentation: The features themselves serve as documentation that is always in sync with the code.
  3. Customer Focus: Ensures that the development team is building features that provide actual value to the user.
  4. Early Automation: Encourages writing acceptance tests early in the development lifecycle.

BDD vs. TDD

FeatureTDDBDD
AudienceDevelopersDevelopers, QA, Business Stakeholders
LanguageProgramming Language (JUnit, etc.)Domain-Specific Language (Gherkin)
FocusUnit implementation / Code qualitySystem behavior / User requirements
GranularityMethod/Class levelFeature/User Story level

BDD doesn’t replace TDD; they are complementary. TDD ensures the code is built right, while BDD ensures the right system is built.

Section Detail

Integration Testing

Integration Testing

Integration testing is the phase in software testing where individual software modules are combined and tested as a group. It occurs after unit testing and before system testing. The goal is to uncover faults in the interaction between integrated units.

Purpose of Integration Testing

Even if every unit works perfectly in isolation (unit tests), the system can still fail because:

  • Data Mismatches: One module expects an integer while another sends a string.
  • Protocol Errors: Incorrect HTTP status codes or header handling between microservices.
  • State Conflicts: Two modules sharing a global state or database records in conflicting ways.
  • Third-Party Dependencies: Issues with databases, message brokers, or external APIs.

Integration Testing Strategies

1. Big Bang Integration

Everything is integrated at once, and then the entire system is tested. While simple, it makes debugging extremely difficult because failure can be anywhere.

2. Top-Down Integration

Testing begins from the top-level modules (e.g., UI) and moves down to low-level modules. Missing low-level modules are replaced with stubs.

3. Bottom-Up Integration

Testing starts from the lowest-level modules and moves up. Higher-level modules are replaced with drivers.

4. Sandwich (Hybrid) Integration

A combination of Top-Down and Bottom-Up approaches.

Modern Approaches: Consumer-Driven Contract Testing

In microservice architectures, integration tests can become very slow and brittle. Contract Testing (e.g., using Pact) allows you to test the interface between services without needing both services running at the same time.

  • Consumer: Defines a “contract” of what it expects from the provider.
  • Provider: Verifies that it can fulfill the contract.
  • Benefit: Catches breaking changes in APIs immediately without full end-to-end environment setup.

The Testing Trophy

While the “Testing Pyramid” emphasizes unit tests, some modern developers (like Kent C. Dodds) advocate for the Testing Trophy. The trophy emphasizes Integration Tests as the “sweet spot” of testing—providing the best balance between speed, confidence, and cost. It suggests that since many bugs occur at the integration points, that is where the bulk of the testing effort should go.

Testing External Systems with Docker (Testcontainers)

Modern integration testing often uses tools like Testcontainers to spin up real databases or message brokers in Docker containers during the test run.

Example: Spring Boot Integration Test with Testcontainers (Java)

@SpringBootTest
@Testcontainers
class OrderIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alphine");

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void shouldSaveAndRetrieveOrder() {
        // Arrange
        Order order = new Order("item-123", 2);

        // Act
        orderRepository.save(order);
        List<Order> orders = orderRepository.findAll();

        // Assert
        assertEquals(1, orders.size());
        assertEquals("item-123", orders.get(0).getItemId());
    }
}

API Integration Testing (REST)

Testing how your application interacts with RESTful APIs is a common form of integration testing.

Example: API Testing with Supertest (Node.js)

const request = require('supertest');
const app = require('../src/app');

describe('POST /api/orders', () => {
  it('should create a new order and return 201', async () => {
    const res = await request(app)
      .post('/api/orders')
      .send({
        itemId: 'SKU-001',
        quantity: 5
      });

    expect(res.statusCode).toEqual(201);
    expect(res.body).toHaveProperty('id');
    expect(res.body.itemId).toBe('SKU-001');
  });
});

Challenges in Integration Testing

  • Environment Parity: Ensuring the test environment closely matches production.
  • Data Cleanup: Ensuring that one test doesn’t leave data in the database that affects the next test.
  • Speed: Integration tests are slower than unit tests because they involve network calls and disk I/O.
  • Flakiness: Network issues or external service downtime can cause tests to fail intermittently.

Integration testing is crucial for ensuring that the various “cogs” of your software machine work together smoothly, preventing costly “integration hell” late in the development cycle.

Section Detail

Mocking and Stubbing

Mocking and Stubbing

In modular software systems, components rarely exist in a vacuum. They depend on database connections, external APIs, file systems, or other complex services. When writing unit tests, we want to test a single unit in isolation. If that unit calls a real database, it’s no longer a unit test—it’s an integration test.

To maintain isolation, speed, and reliability, we use Test Doubles.

Types of Test Doubles

Gerard Meszaros defined several types of test doubles in his book xUnit Test Patterns:

  1. Dummy: Objects that are passed around but never actually used. Usually, they are just used to fill parameter lists.
  2. Fake: Objects that have working implementations, but usually take some shortcut which makes them not suitable for production (e.g., an in-memory database).
  3. Stub: Provide canned answers to calls made during the test, usually not responding at all to anything outside what’s programmed in for the test.
  4. Mock: Objects pre-programmed with expectations which form a specification of the calls they are expected to receive. They can verify that a specific method was called with specific arguments.
  5. Spy: Stubs that also record some information based on how they were called (e.g., a service that records how many messages it sent).

Stubbing vs. Mocking

While often used interchangeably, there is a fundamental difference in their verification style:

  • Stubs use state-based verification. You provide the stub with data, run your code, and then check the state of the object under test.
  • Mocks use behavior-based verification. You tell the mock what to expect, run your code, and then ask the mock if it received the expected calls.

Example: Mocking in Java with Mockito

Mockito is the most popular framework for mocking in the Java ecosystem.

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

class OrderServiceTest {
    @Test
    void testCheckoutProcessesPayment() {
        // 1. Arrange (Create Mocks)
        PaymentProcessor mockPayment = mock(PaymentProcessor.class);
        InventoryService mockInventory = mock(InventoryService.class);
        
        // Stubbing: Define behavior
        when(mockInventory.isAvailable("SKU-123")).thenReturn(true);
        
        OrderService service = new OrderService(mockPayment, mockInventory);
        Order order = new Order("SKU-123", 100.00);

        // 2. Act
        service.checkout(order);

        // 3. Assert / Verify
        // Verify behavior: Was the payment processed exactly once?
        verify(mockPayment, times(1)).process(100.00);
        
        // Verify with Argument Captor
        ArgumentCaptor<Double> captor = ArgumentCaptor.forClass(Double.class);
        verify(mockPayment).process(captor.capture());
        assertEquals(100.00, captor.getValue());
    }
}

Example: Mocking in Python with unittest.mock

Python includes a powerful mocking library in the standard library.

from unittest.mock import MagicMock, patch
import unittest

class TestUserService(unittest.TestCase):
    def test_get_user_name_from_db(self):
        # Create a mock database connection
        mock_db = MagicMock()
        
        # Stub the return value of a method
        mock_db.get_user.return_value = {"id": 1, "name": "Alice"}
        
        # Inject the mock
        user_service = UserService(database=mock_db)
        name = user_service.get_user_primary_name(1)
        
        # Assertions
        self.assertEqual(name, "Alice")
        
        # Verification: Check if get_user was called with correct ID
        mock_db.get_user.assert_called_once_with(1)

    @patch('requests.get')
    def test_api_call(self, mock_get):
        # Setup mock response
        mock_get.return_value.status_code = 200
        mock_get.return_value.json.return_value = {"status": "ok"}
        
        client = MyApiClient()
        response = client.fetch_data()
        
        self.assertEqual(response["status"], "ok")
        mock_get.assert_called_once()

Best Practices for Mocking

  1. Don’t Mock Everything: If a class is a simple data holder (POJO/DTO), don’t mock it. Use the real object.
  2. Only Mock Types You Own: Mocking 3rd party libraries can lead to brittle tests if the library’s internal behavior changes. It’s often better to wrap the library in an interface you control, then mock that interface.
  3. One Mock Verification Per Test: Try to focus each test on verifying one specific interaction.
  4. Avoid “Over-Mocking”: If your test setup is 50 lines of when(...).thenReturn(...) just to test a 2-line function, your code might be too tightly coupled or the unit too large.
  5. Don’t Mock Logic: A stub should return data, not perform complex logic to calculate what to return. If you need logic, you might need a Fake.

Architecture & Quality

Section Detail

Clean Code Practices

Clean Code Practices

”Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” — Martin Fowler.

Clean code is not about a rigid set of rules, but about a mindset. It is code that looks like it was written by someone who cares.

1. Meaningful Names

Names should reveal intent. Avoid generic names like data, info, or item.

Variable Names

  • Use searchable names: MAX_RETRY_ATTEMPTS is better than 7.
  • Avoid encodings: Don’t use Hungarian notation (e.g., strName) or member prefixes (m_name). Modern IDEs handle types and scope.
  • Pronounceable: If you can’t discuss it in a meeting without spelling it out, it’s a bad name.

Method Names

  • Verbs: Methods represent actions and should be verbs like postPayment(), deletePage(), or save().
  • Accessors/Mutators: Prefix with get, set, and is (for booleans).

2. Functions

Functions should be small and do one thing.

  • Small: Aim for functions that fit on one screen without scrolling.
  • Single Responsibility: A function should do one thing and do it well. If you can describe it using “and”, it probably does too much.
  • Low Parameter Count: Ideally 0–2. Three requires a strong justification. More than three? Wrap them in an object.
  • No Side Effects: A function shouldn’t silently change the state of the system unless that is its explicit purpose.

3. The DRY and AHA Principles

  • DRY (Don’t Repeat Yourself): Every piece of knowledge must have a single, unambiguous representation within a system. Logic duplication leads to maintenance nightmares.
  • AHA (Avoid Hasty Abstractions): Sometimes duplication is better than a bad abstraction. Don’t force code into a shared function before the commonalities are truly understood.

4. Comments

”Comments are always failures… because we cannot always write code that is clear enough.” — Robert C. Martin.

  • Explain “Why”, not “What”: If the code is complex, explain the reasoning behind the algorithm, not what each line does.
  • TODO Comments: Use these for technical debt, but ensure they are tracked.
  • Warning of Consequences: Warn other developers about side effects (e.g., // This takes 30 seconds to run).

Clean Code Example (Refactoring)

Before: Unclean Code

// Function to handle users
function handle(u, list) {
  for (let i = 0; i < list.length; i++) {
    if (list[i].status === 'active') {
      if (list[i].age > 18) {
        // Send email
        smtp.send(list[i].email, "Hello " + u.name);
      }
    }
  }
}

After: Clean Code

/**
 * Notifies all active adult users about the new update.
 */
function notifyAdultSubscribers(users) {
  const activeAdults = users.filter(isAdult).filter(isActive);
  
  activeAdults.forEach(sendWelcomeEmail);
}

function isAdult(user) {
  return user.age >= 18;
}

function isActive(user) {
  return user.status === 'active';
}

function sendWelcomeEmail(user) {
  const subject = `Hello ${user.name}`;
  emailService.send(user.email, subject);
}

5. Error Handling

  • Use Exceptions, Not Return Codes: Return codes force the caller to handle the error immediately, cluttering the logic. Exceptions allow you to separate error handling from the main workflow.
  • Don’t Return Null: Returning null forces a null check on every caller. Return an empty collection, an Optional, or use the Null Object Pattern.
  • Provide Context: Your error messages should capture the “what” and “where” to aid debugging.

Summary Checklist for Clean Code

  1. Can I understand the intent of this variable without looking at its usage?
  2. Does this function do exactly one thing?
  3. Is the error handling separated from the business logic?
  4. Are there any “magic numbers” that should be constants?
  5. Is the code itself readable enough that I don’t need these comments?
Section Detail

Refactoring Techniques

Refactoring Techniques

Refactoring is the process of restructuring existing computer code—changing the factoring—without changing its external behavior. It is essentially “cleaning up” code to make it more maintainable and understandable.

Why Refactor?

  • Improve Design: Code drifts over time as new features are bolted on. Refactoring brings it back to a cohesive design.
  • Reduced Complexity: Simplifies logic and removes duplication.
  • Easier Debugging: Cleaner code makes bugs easier to spot.
  • Faster Development: Well-structured code is easier to build upon.

When to Refactor?

  • Rule of Three: When you’re doing something for the third time, refactor.
  • When adding a feature: Clean up the area you’re about to modify to make the new feature easier to implement.
  • When fixing a bug: If code is so messy that a bug was hard to find, it’s a sign it needs refactoring.
  • During Code Review: Use reviews as an opportunity to suggest refactoring.

Common Code Smells

A “code smell” is a surface indication that usually corresponds to a deeper problem in the system.

  1. Bloaters: Large classes or methods (e.g., Long Method, Large Class, Data Clumps).
  2. Object-Orientation Abusers: Improper use of OO principles (e.g., Switch Statements where polymorphism should be used, Alternative Classes with Different Interfaces).
  3. Change Preventers: Code that is hard to change (e.g., Divergent Change, Shotgun Surgery).
  4. Dispensables: Code that should be removed (e.g., Comments, Duplicate Code, Dead Code).
  5. Couplers: Excessive coupling between classes (e.g., Feature Envy, Inappropriate Intimacy).

Core Refactoring Techniques

1. Extract Method

If you have a code fragment that can be grouped together, turn the fragment into a method whose name explains the purpose of the method.

Before:

void printOwing() {
    printBanner();

    // Print details
    System.out.println("name: " + name);
    System.out.println("amount: " + getOutstanding());
}

After:

void printOwing() {
    printBanner();
    printDetails(getOutstanding());
}

void printDetails(double outstanding) {
    System.out.println("name: " + name);
    System.out.println("amount: " + outstanding);
}

2. Replace Conditional with Polymorphism

If you have a conditional (switch or if-else) that performs different behaviors depending on the type of an object, use subclasses and polymorphism instead.

Before:

def get_speed(bird):
    if bird.type == "EUROPEAN":
        return base_speed()
    elif bird.type == "AFRICAN":
        return base_speed() - load_factor() * bird.num_coconuts
    elif bird.type == "NORWEGIAN_BLUE":
        return 0 if bird.is_nailed else base_speed()

After:

class Bird:
    def get_speed(self): pass

class European(Bird):
    def get_speed(self): return base_speed()

class African(Bird):
    def get_speed(self): return base_speed() - load_factor() * self.num_coconuts

class NorwegianBlue(Bird):
    def get_speed(self): return 0 if self.is_nailed else base_speed()

3. Move Method / Field

If a method is used more by another class than the one it’s in, move it to the other class. This reduces Feature Envy.

4. Replace Temp with Query

Replace a temporary variable used to store an expression with a method. This makes the logic reusable throughout the class.

Before:

let basePrice = quantity * itemPrice;
if (basePrice > 1000) return basePrice * 0.95;
else return basePrice * 0.98;

After:

if (basePrice() > 1000) return basePrice() * 0.95;
else return basePrice() * 0.98;

function basePrice() {
    return quantity * itemPrice;
}

The Workflow of Refactoring

  1. Ensure Tests are Green: Never refactor without a solid test suite.
  2. Make Small Changes: One refactoring at a time.
  3. Run Tests: After every small change, run the tests to ensure you haven’t broken the external behavior.
  4. Commit Often: This allows you to revert easily if a refactor goes wrong.

Deployment

Section Detail

DevOps and CI/CD

DevOps and CI/CD

In modern software engineering, the boundary between “development” and “operations” has blurred. DevOps is a set of practices, tools, and a cultural philosophy that automate and integrate the processes between software development and IT teams.

The DevOps Lifecycle

  1. Plan: Requirement gathering and project management.
  2. Code: Development and version control.
  3. Build: Compiling code and managing dependencies.
  4. Test: Automated unit, integration, and security tests.
  5. Release: Preparing the artifact for deployment.
  6. Deploy: Pushing the code to production or staging.
  7. Operate: Managing the infrastructure.
  8. Monitor: Tracking performance and user experience.

CI/CD: The Engine of DevOps

Continuous Integration (CI)

CI is the practice of merging all developer working copies to a shared mainline several times a day.

  • Goal: Catch integration bugs early.
  • Key Action: Every commit triggers an automated build and test suite.

Continuous Delivery (CD)

Continuous Delivery is an extension of CI to ensure that you can release new changes to your customers quickly in a sustainable way.

  • Goal: The codebase is always in a deployable state.
  • Key Action: Automated release process, but deployment to production might be manual.

Continuous Deployment (CD)

Every change that passes all stages of your production pipeline is released to your customers. There is no human intervention.

Designing a CI/CD Pipeline

A typical pipeline consists of several stages:

1. Source Stage

Triggered by a code change in a repository (e.g., GitHub, GitLab, Bitbucket).

2. Build Stage

The application is compiled, and artifacts (like JAR files, Docker images) are created.

3. Test Stage

  • Unit Tests: Verify individual components.
  • Static Analysis (Linting): Check for code style and potential bugs (e.g., SonarQube).
  • Security Scanning: Check for vulnerabilities in dependencies.

4. Deploy Stage

Artifacts are deployed to environments (Staging, then Production). Use techniques like Blue-Green Deployment or Canary Releases to minimize risk.

Example: GitHub Actions Workflow (YAML)

GitHub Actions allows you to define your CI/CD pipeline in a .yaml file within your repository.

# .github/workflows/ci.yml
name: Java CI with Maven

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
        cache: maven
        
    - name: Build and Test with Maven
      run: mvn -B package --file pom.xml

    - name: Run Static Analysis
      run: mvn sonar:sonar
      env:
        SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        
    - name: Upload Artifact
      uses: actions/upload-artifact@v3
      with:
        name: app-jar
        path: target/*.jar

Benefits of CI/CD

  • Reduced Risk: Smaller changes are easier to test and revert.
  • Faster Time to Market: Features reach users in hours or days instead of months.
  • Increased Productivity: Developers spend less time on manual deployment and more on code.
  • Better Quality: Constant automated testing ensures regressions are caught immediately.

Infrastructure as Code (IaC)

DevOps often involves managing infrastructure through code (e.g., Terraform, CloudFormation, Ansible). This allows you to version control your server configurations just like your application code.

Section Detail

Software Quality and Maintenance

Software Quality and Maintenance

Software engineering doesn’t end when the code is deployed. In fact, for most successful systems, the Maintenance phase accounts for 60-80% of the total software lifecycle cost.

Software Quality Attributes (ISO 25010)

Quality is not just “lack of bugs.” The ISO 25010 standard defines a quality model with several key attributes:

  1. Functional Suitability: Does it do what the user needs?
  2. Performance Efficiency: How fast is it? How many resources does it use?
  3. Compatibility: Can it exchange information with other systems?
  4. Usability: How easy is it for users to learn and use?
  5. Reliability: How often does it fail? Can it recover?
  6. Security: How well does it protect data and resist attacks?
  7. Maintainability: How easy is it to modify, test, and improve?
  8. Portability: How easy is it to move to a different environment?

Technical Debt

Technical debt is a metaphor for the eventual consequences of poor system design, software architecture, or software development within a codebase.

  • Causes: Pressure to meet deadlines, lack of knowledge, poor documentation, or evolving requirements.
  • The Interest: The extra effort required to add new features because of the mess in the existing code.
  • Managing Debt: Use tools like SonarQube to track “debt” in hours. Schedule dedicated “refactoring sprints” or allocate 20% of every sprint to paying down debt.

Types of Software Maintenance

  1. Corrective Maintenance: Fixing bugs reported by users.
  2. Adaptive Maintenance: Modifying software to work in a new or changed environment (e.g., a new OS version).
  3. Perfective Maintenance: Improving performance or maintainability (e.g., refactoring).
  4. Preventive Maintenance: Making changes to prevent future problems (e.g., updating a library before its support ends).

Legacy Systems

A legacy system is an old method, technology, computer system, or application program that is still in use.

  • The Challenge: They often lack tests, documentation, and the original developers have left.
  • Strategy: Don’t rewrite the whole thing at once. Use the Strangler Fig Pattern: gradually replace parts of the system with new services until the old system is “strangled” and can be retired.

Course Conclusion: Software Engineering & OOAD

Congratulations on completing the Software Engineering & Object-Oriented Analysis and Design course!

Over the past 30 lessons, we have traveled from the basic building blocks of objects and classes to the complex ecosystems of DevOps and software maintenance.

Key Takeaways:

  1. Abstraction is Power: Mastering the ability to hide complexity behind interfaces and abstract classes is the core of good design.
  2. SOLID Principles are North Stars: They guide you toward code that is flexible enough to handle the only constant in software: Change.
  3. Patterns, not Recipes: Design patterns are tools in your toolbox. Don’t force them into every problem; use them when the context fits.
  4. Testing is Part of Coding: You haven’t finished the feature until you’ve written the tests that prove it works and won’t break later.
  5. Focus on People: Software is built by people, for people. Clean code, documentation, and DevOps cultures are all about improving the human experience of building and using software.

What’s Next?

  • Build something: Apply these patterns to a real-world side project.
  • Read the Classics: Explore Design Patterns (Gang of Four) and Clean Code (Robert C. Martin).
  • Keep Learning: The field of software engineering is always evolving. Stay curious!

Happy Coding!