CSC364 Software Engineering

Software EngineeringUnit 420 min read

Design Principles & Patterns: UML, SOLID, GoF, MVC, Layers & Reuse

Unit 4 of Software Engineering covers fundamental design principles (SOLID, DRY, KISS), architectural patterns (MVC, Layered, Pipe-Filter), GoF design patterns (Singleton, Observer, Factory), UML modeling, and software reuse strategies—with real-world examples from eSewa, Daraz, and banking systems.

Core Concepts: Why Design Matters

1. Principles vs. Patterns

  • Principles are guidelines (e.g., "Single Responsibility Principle") that shape how you write code.
  • Patterns are proven solutions to recurring problems (e.g., "Singleton pattern for one global instance").
mindmap
  root((Software Design))
    Principles["Principles (Rules)"]
      SOLID["5 Rules for Clean Code"]
      DRY["Don't Repeat Yourself"]
      KISS["Keep It Simple, Stupid"]
    Patterns["Patterns (Templates)"]
      GoF["23 Gang-of-Four Patterns"]
      Architectural["MVC, Layered, Microservices"]
      Anti["Anti-patterns to Avoid"]

2. SOLID Principles: The 5 Pillars of Clean Code

SOLID is an acronym for five principles that make software maintainable and scalable. Violating these leads to "spaghetti code" (tightly coupled, hard-to-change systems).

One class = one reason to changeSingle Responsibility (SRP)Extend without modifyingOpen/Closed (OCP)Subtypes must replace base typesLiskov Substitution (LSP)Small, role-specific interfacesInterface Segregation (ISP)Depend on abstractions, not concretionsDependency Inversion (DIP)SOLID Principles
SOLID principles as interconnected design goals

A. Single Responsibility Principle (SRP)

"A class should have only one reason to change."

Example (Violation → Fix):

// ❌ Violates SRP: User class handles *both* data *and* validation
class User {
    String name;
    void saveToDB() { ... }
    boolean isValidEmail(String email) { ... } // Should be in EmailValidator!
}
// ✅ Fixed: Separate classes for each responsibility
class User { String name; void saveToDB(); }
class EmailValidator { boolean isValid(String email); }

Real-World Tie-In:

  • eSewa’s Payment System:
    • ❌ Bad: A single PaymentService class handles user authentication, transaction logging, and fraud detection.
    • ✅ Good: Split into AuthService, TransactionLogger, FraudDetector (each with one job).
    • Why? If fraud rules change, only FraudDetector needs updates—no risk of breaking authentication.

B. Open/Closed Principle (OCP)

"Software entities should be open for extension but closed for modification."

Example (Violation → Fix):

// ❌ Closed for extension: Adding a new shape requires modifying AreaCalculator
class AreaCalculator {
    double calculate(Shape shape) {
        if (shape instanceof Circle) return 3.14 * shape.radius * shape.radius;
        else if (shape instanceof Square) return shape.side * shape.side;
        // ❌ What if we add a Triangle later? We must edit this method!
    }
}
// ✅ Open for extension: New shapes add *new classes*, not edits
interface Shape { double area(); }
class Circle implements Shape { public double area() { ... } }
class Square implements Shape { public double area() { ... } }
class Triangle implements Shape { public double area() { ... } } // ✅ No changes to AreaCalculator!

Real-World Tie-In:

  • Daraz’s Order Processing:
    • ❌ Bad: A single OrderProcessor class has if-else blocks for CashOnDelivery, Khalti, Esewa. Adding a new payment method requires editing the class.
    • ✅ Good: Each payment method is a separate class implementing PaymentStrategy interface. New methods (e.g., Fonepay) are added without touching OrderProcessor.
    • Why? Daraz can add payment options without breaking existing code (critical for Nepal’s dynamic fintech scene).

C. Liskov Substitution Principle (LSP)

"Subtypes must be substitutable for their base types without breaking the program."

Example (Violation):

class Rectangle {
    int width, height;
    void setWidth(int w) { width = w; }
    void setHeight(int h) { height = h; }
}
class Square extends Rectangle {
    void setWidth(int w) { width = height = w; } // ❌ Breaks LSP!
    void setHeight(int h) { height = width = h; }
}

Problem: If you substitute a Square for a Rectangle, calling setWidth(5) and setHeight(10) won’t work as expected (square forces equal sides).

Real-World Tie-In:

  • NTC’s Traffic Light System:
    • ❌ Bad: A TrafficLight base class with changeColor() is subclassed into UrbanLight and HighwayLight. If HighwayLight overrides changeColor() to ignore pedestrian signals, it violates LSP.
    • ✅ Good: Separate UrbanTrafficLight and HighwayTrafficLight with distinct interfaces. No substitution confusion.

D. Interface Segregation Principle (ISP)

"Clients should not be forced to depend on interfaces they don’t use."

Example (Violation → Fix):

// ❌ Fat interface: Worker forced to implement unused methods
interface Worker {
    void work();
    void eat(); // ❌ Robot doesn’t eat!
}
class HumanWorker implements Worker { ... }
class RobotWorker implements Worker { // ❌ Must implement eat() even though it can't!
    public void eat() { throw new UnsupportedOperationException(); }
}
// ✅ Segregated interfaces
interface Workable { void work(); }
interface Eatable { void eat(); }
class HumanWorker implements Workable, Eatable { ... }
class RobotWorker implements Workable { ... } // ✅ No forced eat()

Real-World Tie-In:

  • Pathao’s Driver App:
    • ❌ Bad: A single Driver interface forces all drivers to implement deliverFood(), deliverGroceries(), and deliverMedicine().
    • ✅ Good: Split into FoodDeliveryDriver, GroceryDeliveryDriver, etc., each implementing only relevant methods.
    • Why? Saves battery and reduces app complexity for drivers who don’t offer all services.

E. Dependency Inversion Principle (DIP)

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

Example (Violation → Fix):

// ❌ High-level class depends on low-level class
class LightBulb { void turnOn() { ... } }
class Switch {
    LightBulb bulb;
    void operate() { bulb.turnOn(); } // ❌ Tight coupling!
}
// ✅ Depend on abstraction (interface)
interface Switchable { void turnOn(); }
class LightBulb implements Switchable { ... }
class Switch {
    Switchable device; // ✅ Can now work with any Switchable (e.g., Fan, AC)
    void operate() { device.turnOn(); }
}

Real-World Tie-In:

  • Nepal Rastra Bank’s Loan System:
    • ❌ Bad: LoanProcessor directly calls BankDatabase.save().
    • ✅ Good: LoanProcessor depends on DataStorage interface. In testing, you can mock DataStorage without touching LoanProcessor.
    • Why? Enables unit testing and swapping databases (e.g., from SQL to NoSQL) without rewriting business logic.

3. Design Patterns: GoF’s 23 Solutions

The Gang of Four (GoF) patterns are categorized into three groups:

Singleton (e.g., ConfigManager)Factory (e.g., PaymentMethodFactory)Builder (e.g., Daraz Order Builder)Creational (Object Creation)Adapter (e.g., Legacy API wrappers)Decorator (e.g., eSewa’s DiscountDecorator)Facade (e.g., NTC’s Traffic API)Structural (Class/Object Composition)Observer (e.g., WhatsApp notifications)Strategy (e.g., Daraz’s SortingStrategy)Command (e.g., Undo/Redo in text editors)Behavioral (Object Interaction)GoF Design Patterns
Hierarchical categorization of GoF’s 23 patterns with local examples

A. Singleton Pattern

Problem: Ensure a class has only one instance and provide a global point of access. Use Case: Configuration managers, logging systems, database connections.

Example:

class DatabaseConnection {
    private static DatabaseConnection instance;
    private DatabaseConnection() {} // Private constructor
    public static DatabaseConnection getInstance() {
        if (instance == null) instance = new DatabaseConnection();
        return instance;
    }
}

Real-World Tie-In:

  • eSewa’s Transaction Log:
    • Only one TransactionLogger instance writes to the database to avoid duplicate logs.
    • Why? Prevents race conditions and ensures all transactions are logged consistently.

Thread-Safety Warning:

// ❌ Not thread-safe (race condition)
public static DatabaseConnection getInstance() {
    if (instance == null) instance = new DatabaseConnection(); // ❌ Two threads can pass this check!
    return instance;
}
// ✅ Thread-safe (double-checked locking)
public static DatabaseConnection getInstance() {
    if (instance == null) {
        synchronized (DatabaseConnection.class) {
            if (instance == null) instance = new DatabaseConnection();
        }
    }
    return instance;
}

B. Observer Pattern

Problem: Notify multiple objects about changes in another object (e.g., events, state changes). Use Case: UI updates, publish-subscribe systems, stock market tickers.

Example:

interface Observer { void update(String message); }
class NewsAgency {
    List<Observer> observers = new ArrayList<>();
    void addObserver(Observer o) { observers.add(o); }
    void notifyObservers(String news) {
        for (Observer o : observers) o.update(news);
    }
}
class NcellUser implements Observer {
    public void update(String news) { System.out.println("Ncell Alert: " + news); }
}

Real-World Tie-In:

  • WhatsApp Notifications:
    • Your phone (Observer) subscribes to WhatsApp’s NewsAgency.
    • When a new message arrives, WhatsApp notifies all subscribed devices (your phone + tablet).
    • Why? Decouples the messaging system from devices—new devices can subscribe without changing WhatsApp’s core.

C. Factory Pattern

Problem: Defer instantiation to subclasses (avoid if-else for object creation). Use Case: Payment method selection, GUI component creation.

Example:

interface PaymentMethod { void process(); }
class KhaltiPayment implements PaymentMethod { ... }
class EsewaPayment implements PaymentMethod { ... }
class PaymentFactory {
    public PaymentMethod createPayment(String type) {
        if (type.equals("khalti")) return new KhaltiPayment();
        else if (type.equals("esewa")) return new EsewaPayment();
        else throw new IllegalArgumentException();
    }
}

Real-World Tie-In:

  • Daraz’s Checkout Page:
    • Instead of hardcoding if (payment == "khalti") { ... }, Daraz uses a PaymentFactory to create the correct payment object.
    • Why? Adding a new payment method (e.g., Fonepay) only requires a new class—not editing the checkout logic.

4. Architectural Patterns

A. Model-View-Controller (MVC)

Structure:

classDiagram
    class Model {
        +data: Object
        +businessLogic()
    }
    class View {
        +display(model: Object)
    }
    class Controller {
        +handleInput(event: Event)
        +updateModel(data: Object)
    }
    Model "1" --> "1" View : updates
    View "1" --> "1" Controller : user actions
    Controller "1" --> "1" Model : changes
    note for Model "Core data + rules"
    note for View "UI rendering"
    note for Controller "Input processing"
    caption MVC Flow with Nepali Example: Daraz’s Product Page

Real-World Tie-In:

  • eSewa’s Web App:
    • Model: User account data, transaction records.
    • View: HTML pages showing balances, transaction history.
    • Controller: Handles "Pay Bill" button clicks, validates inputs, updates the Model.
    • Why? Separates concerns—UI changes (View) don’t affect business logic (Model).

Example Workflow:

  1. User clicks "Pay Electricity Bill" (View → Controller).
  2. Controller validates input (e.g., correct meter number) and calls TransactionService (Model).
  3. Model processes payment and updates the database.
  4. Controller tells View to show "Payment Successful" message.

B. Layered Architecture

Structure:

Real-World Tie-In:

  • Nepal Stock Exchange (NEPSE) Trading System:
    • Presentation Layer: Web/mobile app showing stock prices.
    • Application Layer: Order matching logic (e.g., "Buy 100 shares of NMB at ₹100").
    • Domain Layer: Rules like "No short selling."
    • Data Layer: SQL database storing trades.
    • Why? If NEPSE adds a new trading rule (e.g., "Tax on high-frequency trades"), only the Domain Layer changes.

C. Pipe-Filter Architecture

Structure:

InputFilter1Filter2Filter3Output
Pipe-Filter Architecture: Data flows sequentially through processing stages (e.g., Ncell’s SMS Gateway)

Real-World Tie-In:

  • NTC’s Traffic Camera Processing:
    • Input: Raw video from cameras.
    • Filters:
      1. FrameExtractor: Splits video into frames.
      2. ObjectDetector: Identifies vehicles/pedestrians.
      3. ViolationChecker: Flags jaywalking/speeding.
    • Output: Alerts sent to traffic police.
    • Why? Each filter is independent—upgrading the ObjectDetector (e.g., to use AI) doesn’t break others.

5. Software Reuse

Definition: Using existing software components to build new systems faster and with fewer bugs.

A. Types of Reuse

Type Description Example
Code Reuse Copy-paste or inherit existing code Using a Logger class in multiple projects
Component Reuse Plug-in modules (e.g., libraries) React.js for UI, Spring for backend
Architecture Reuse Reusing high-level designs MVC for web apps, Microservices for scalable systems
Domain Reuse Industry-specific templates Hospital management software templates

Real-World Tie-In:

  • Khalti’s SDK for Apps:
    • Developers reuse Khalti’s pre-built PaymentComponent instead of writing payment logic from scratch.
    • Why? Saves time and ensures PCI compliance (security standards for payments).

B. Benefits of Reuse

  • Faster Development: No need to reinvent the wheel.
  • Lower Costs: Fewer bugs in reused, tested components.
  • Consistency: Uniform UI/UX across apps (e.g., all banks use similar login flows).

Example:

023466992Faster Development85Lower Costs78Reliability92Scalability70Consistency88
Percentage improvement from reuse (Nepali software industry average)

6. Anti-Patterns: What to Avoid

Definition: Common solutions that seem to work but cause long-term problems.

God ObjectSpaghetti CodeCopy-Paste ProgrammingOver-Engineering
How common anti-patterns feed into each other (e.g., in Nepali freelance projects)
Anti-Pattern Description Fix
God Object One class does everything Split into smaller classes (SOLID)
Spaghetti Code Tightly coupled, hard to modify Use design patterns (e.g., Dependency Injection)
Golden Hammer Overusing one tool/pattern Choose the right tool for the job
Big Ball of Mud No clear architecture Adopt MVC/Layered architecture

Real-World Tie-In:

  • Early Ncell Billing System:
    • ❌ Anti-Pattern: A single BillingProcessor class handled customer data, billing calculations, UI rendering, and database updates.
    • ✅ Fix: Split into CustomerService, BillingCalculator, UIRenderer, and DatabaseRepository.
    • Why? Now Ncell can update billing rules without breaking the UI.

In the Real World

  1. eSewa’s Payment System

    • Idea Used: Strategy Pattern for payment methods.
    • How? Each payment gateway (Khalti, Esewa, Fonepay) implements PaymentStrategy interface. The checkout system delegates payment processing to the selected strategy.
    • Impact: Adding a new payment method (e.g., NMB Bank) requires only a new class—not changing the checkout logic.
  2. Daraz’s Order Processing

    • Idea Used: Observer Pattern for notifications.
    • How? When an order status changes (e.g., "Shipped"), Daraz notifies the customer (email/SMS), the warehouse (pick item), and the delivery partner (assign driver).
    • Impact: Decouples order processing from notification systems—easy to add new notification channels (e.g., WhatsApp alerts).
  3. Nepal Rastra Bank’s Loan Approval

    • Idea Used: Layered Architecture.
    • How? The loan system has:
      • Presentation Layer: Web/mobile app for customers.
      • Application Layer: Business rules (e.g., "Income > 3x EMI").
      • Domain Layer: Loan types (home, car, personal).
      • Data Layer: SQL database.
    • Impact: If NRB changes loan eligibility rules, only the Domain Layer is updated—no UI or database changes.

Exam Tip

What Examiners Love to See

  1. Diagrams > Text

    • Always draw class diagrams for patterns (e.g., MVC, Observer) and sequence diagrams for interactions (e.g., payment processing).
    • Example: For the Singleton pattern, show:
      • A class with a private static instance.
      • A getInstance() method with lazy initialization.
  2. Real-World Mapping

    • Link abstract concepts to Nepali tech companies (e.g., "eSewa uses the Strategy Pattern for payments").
    • Avoid generic examples like "ATM machines"—use specific systems you’ve heard of.
  3. Code Snippets with Explanations

    • Show before/after code for SOLID violations (e.g., SRP, OCP).
    • Highlight key lines (e.g., interface, abstract class, dependency injection).
  4. Trade-offs

    • Discuss pros/cons of patterns:
      • Singleton: Simple but can cause global state issues.
      • Observer: Decouples objects but can lead to memory leaks if not managed.
  5. Common Pitfalls

    • Thread safety in Singleton (always mention synchronized or double-checked locking).
    • Over-engineering (e.g., using a Factory when a simple if-else suffices).

Past Exam Patterns

  • Calculation Questions: Expect COCOMO (Unit 2) or effort estimation using KLOC (Thousands of Lines of Code). For Unit 4, focus on design trade-offs (e.g., "Why would you choose MVC over Layered?").
  • Differentiation: Always compare two concepts (e.g., "Verify vs. Validate," "Prototyping vs. Agile").
  • Diagram-Based: Sketch UML class diagrams for patterns (e.g., Observer, Factory) or architecture diagrams (MVC, Layered).

Model Answer Structure

For a 5-mark question like "Explain the Observer Pattern with a real-world example":

  1. Definition (1 mark): "Observer is a behavioral pattern where an object (Subject) maintains a list of dependents (Observers) and notifies them of state changes."
  2. Components (1 mark):
    • Subject: Maintains state and notifies observers (e.g., NewsAgency).
    • Observer: Gets updates (e.g., NcellUser).
  3. Diagram (1 mark): Draw a UML class diagram or sequence diagram.
  4. Example (2 marks):
    • WhatsApp: Subject = WhatsApp server; Observers = your phone, tablet, laptop.
    • Code Snippet: Show addObserver(), notifyObservers(), and update() methods.

Summary Table: Key Concepts

Concept Key Idea When to Use Example
SRP One class, one responsibility Any class doing multiple jobs Split User into User + EmailValidator
OCP Extend without modifying Adding new features (e.g., payment methods) PaymentStrategy interface
Singleton Single instance Logging, configuration, DB connection DatabaseConnection
Observer Event-driven updates Notifications, UI updates WhatsApp messages, stock tickers
MVC Separate Model, View, Controller Web/mobile apps eSewa’s payment page
Layered Stacked responsibilities Enterprise systems NEPSE trading platform
Factory Defer object creation Dynamic object creation Daraz’s payment method selection

Based on the TU BSc CSIT syllabus for Software Engineering (CSC364), unit 4.

Discussion

Loading…