diff --git a/README.md b/README.md index 73723cb..83933e8 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,4 @@ This repository is essentially a conversation with myself where I am listing the ## Topics Covered - [DRY Principle in Coding](DRY-Principle.md) - Understanding the "Don't Repeat Yourself" principle with practical examples +- [SOLID Principles in Software Development](SOLID-Principles.md) - Comprehensive guide to the five fundamental principles of object-oriented design with TypeScript, Python, and Angular examples diff --git a/SOLID-Principles.md b/SOLID-Principles.md new file mode 100644 index 0000000..4be6f0b --- /dev/null +++ b/SOLID-Principles.md @@ -0,0 +1,1221 @@ +# SOLID Principles in Software Development + +## What are SOLID Principles? + +**SOLID** is an acronym that represents five fundamental principles of object-oriented programming and design. These principles, when applied together, make software designs more understandable, flexible, and maintainable. + +The five principles are: +- **S** - Single Responsibility Principle (SRP) +- **O** - Open/Closed Principle (OCP) +- **L** - Liskov Substitution Principle (LSP) +- **I** - Interface Segregation Principle (ISP) +- **D** - Dependency Inversion Principle (DIP) + +## History and Creator + +The SOLID principles were introduced by **Robert C. Martin** (also known as "Uncle Bob") in the early 2000s. Robert C. Martin is a renowned software engineer, author, and speaker who has made significant contributions to software development practices. + +The principles were first presented in his paper "Design Principles and Design Patterns" (2000) and later popularized in his book "Agile Software Development: Principles, Patterns, and Practices" (2003). The SOLID acronym itself was coined by Michael Feathers. + +## Why Were SOLID Principles Needed? + +### The Problem with Traditional Object-Oriented Design + +Before SOLID principles, many object-oriented systems suffered from: + +1. **Rigidity**: Hard to change because every change affects too many other parts +2. **Fragility**: Changes break unexpected parts of the system +3. **Immobility**: Hard to reuse components in other applications +4. **Viscosity**: Easier to hack than to maintain good design +5. **Needless Complexity**: Over-engineering and unnecessary abstractions + +### The Solution: SOLID Principles + +SOLID principles address these issues by providing guidelines that: + +- **Reduce coupling** between system components +- **Increase cohesion** within individual components +- **Improve maintainability** and readability +- **Enable easier testing** through better separation of concerns +- **Facilitate code reuse** and extensibility +- **Make systems more resilient to change** + +## The Five SOLID Principles + +### 1. Single Responsibility Principle (SRP) + +**Definition**: A class should have only one reason to change, meaning it should have only one job or responsibility. + +#### Why SRP Matters: +- **Easier maintenance**: Changes to one responsibility don't affect others +- **Better testability**: Smaller, focused classes are easier to test +- **Improved readability**: Code purpose is clearer +- **Reduced coupling**: Classes depend on fewer things + +#### TypeScript Examples: + +```typescript +// ❌ Bad: Violates SRP - Multiple responsibilities +class User { + constructor( + public name: string, + public email: string + ) {} + + // Responsibility 1: User data management + updateEmail(newEmail: string): void { + this.email = newEmail; + } + + // Responsibility 2: Email sending (should not be here) + sendWelcomeEmail(): void { + // Email sending logic + console.log(`Sending welcome email to ${this.email}`); + } + + // Responsibility 3: Data persistence (should not be here) + saveToDatabase(): void { + // Database logic + console.log(`Saving user ${this.name} to database`); + } +} + +// ✅ Good: Following SRP - Single responsibility per class +class User { + constructor( + public name: string, + public email: string + ) {} + + updateEmail(newEmail: string): void { + this.email = newEmail; + } +} + +class EmailService { + sendWelcomeEmail(user: User): void { + console.log(`Sending welcome email to ${user.email}`); + } +} + +class UserRepository { + save(user: User): void { + console.log(`Saving user ${user.name} to database`); + } +} +``` + +#### Python Examples: + +```python +# ❌ Bad: Violates SRP +class Employee: + def __init__(self, name: str, position: str, salary: float): + self.name = name + self.position = position + self.salary = salary + + # Responsibility 1: Employee data + def get_employee_info(self) -> str: + return f"{self.name} - {self.position}" + + # Responsibility 2: Salary calculation (should be separate) + def calculate_pay(self) -> float: + # Complex calculation logic + return self.salary * 0.8 # After taxes + + # Responsibility 3: Report generation (should be separate) + def generate_report(self) -> str: + return f"Employee Report: {self.name}, Pay: ${self.calculate_pay()}" + +# ✅ Good: Following SRP +class Employee: + def __init__(self, name: str, position: str, salary: float): + self.name = name + self.position = position + self.salary = salary + + def get_employee_info(self) -> str: + return f"{self.name} - {self.position}" + +class PayrollCalculator: + def calculate_pay(self, employee: Employee) -> float: + return employee.salary * 0.8 + +class ReportGenerator: + def __init__(self, payroll_calculator: PayrollCalculator): + self.payroll_calculator = payroll_calculator + + def generate_employee_report(self, employee: Employee) -> str: + pay = self.payroll_calculator.calculate_pay(employee) + return f"Employee Report: {employee.name}, Pay: ${pay}" +``` + +### 2. Open/Closed Principle (OCP) + +**Definition**: Software entities should be open for extension but closed for modification. You should be able to add new functionality without changing existing code. + +#### Why OCP Matters: +- **Reduces risk**: Don't modify working code +- **Enables extensibility**: Easy to add new features +- **Improves maintainability**: Changes are isolated +- **Promotes reusability**: Existing code can be reused in new contexts + +#### TypeScript Examples: + +```typescript +// ❌ Bad: Violates OCP - Need to modify existing code for new shapes +class AreaCalculator { + calculateArea(shapes: any[]): number { + let totalArea = 0; + + for (const shape of shapes) { + if (shape.type === 'rectangle') { + totalArea += shape.width * shape.height; + } else if (shape.type === 'circle') { + totalArea += Math.PI * shape.radius * shape.radius; + } + // Adding new shapes requires modifying this method + } + + return totalArea; + } +} + +// ✅ Good: Following OCP - Open for extension, closed for modification +interface Shape { + calculateArea(): number; +} + +class Rectangle implements Shape { + constructor(private width: number, private height: number) {} + + calculateArea(): number { + return this.width * this.height; + } +} + +class Circle implements Shape { + constructor(private radius: number) {} + + calculateArea(): number { + return Math.PI * this.radius * this.radius; + } +} + +// New shapes can be added without modifying existing code +class Triangle implements Shape { + constructor(private base: number, private height: number) {} + + calculateArea(): number { + return 0.5 * this.base * this.height; + } +} + +class AreaCalculator { + calculateArea(shapes: Shape[]): number { + return shapes.reduce((total, shape) => total + shape.calculateArea(), 0); + } +} +``` + +#### Python Examples: + +```python +# ❌ Bad: Violates OCP +class DiscountCalculator: + def calculate_discount(self, customer_type: str, amount: float) -> float: + if customer_type == "regular": + return amount * 0.1 + elif customer_type == "premium": + return amount * 0.2 + elif customer_type == "vip": + return amount * 0.3 + # Adding new customer types requires modifying this method + return 0 + +# ✅ Good: Following OCP +from abc import ABC, abstractmethod + +class Customer(ABC): + @abstractmethod + def calculate_discount(self, amount: float) -> float: + pass + +class RegularCustomer(Customer): + def calculate_discount(self, amount: float) -> float: + return amount * 0.1 + +class PremiumCustomer(Customer): + def calculate_discount(self, amount: float) -> float: + return amount * 0.2 + +class VIPCustomer(Customer): + def calculate_discount(self, amount: float) -> float: + return amount * 0.3 + +# New customer types can be added without modifying existing code +class CorporateCustomer(Customer): + def calculate_discount(self, amount: float) -> float: + return amount * 0.25 + +class DiscountCalculator: + def calculate_discount(self, customer: Customer, amount: float) -> float: + return customer.calculate_discount(amount) +``` + +### 3. Liskov Substitution Principle (LSP) + +**Definition**: Objects of a superclass should be replaceable with objects of its subclasses without breaking the application. Subtypes must be substitutable for their base types. + +#### Why LSP Matters: +- **Ensures polymorphism works correctly** +- **Maintains contract integrity** +- **Prevents unexpected behavior** +- **Enables safe inheritance** + +#### TypeScript Examples: + +```typescript +// ❌ Bad: Violates LSP - Square changes Rectangle behavior +class Rectangle { + constructor(protected width: number, protected height: number) {} + + setWidth(width: number): void { + this.width = width; + } + + setHeight(height: number): void { + this.height = height; + } + + getArea(): number { + return this.width * this.height; + } +} + +class Square extends Rectangle { + constructor(side: number) { + super(side, side); + } + + // Violates LSP: Changes the behavior of Rectangle + setWidth(width: number): void { + this.width = width; + this.height = width; // Side effect not expected in Rectangle + } + + setHeight(height: number): void { + this.width = height; // Side effect not expected in Rectangle + this.height = height; + } +} + +// This function expects Rectangle behavior but breaks with Square +function processRectangle(rectangle: Rectangle): void { + rectangle.setWidth(5); + rectangle.setHeight(10); + // Expected area: 50, but Square will give 100 + console.log(`Area: ${rectangle.getArea()}`); +} + +// ✅ Good: Following LSP - Using composition and proper abstraction +interface Shape { + getArea(): number; +} + +class Rectangle implements Shape { + constructor(private width: number, private height: number) {} + + getArea(): number { + return this.width * this.height; + } +} + +class Square implements Shape { + constructor(private side: number) {} + + getArea(): number { + return this.side * this.side; + } +} + +// Both can be used interchangeably through the Shape interface +function processShape(shape: Shape): void { + console.log(`Area: ${shape.getArea()}`); +} +``` + +#### Python Examples: + +```python +# ❌ Bad: Violates LSP +class Bird: + def fly(self) -> None: + print("Flying...") + +class Sparrow(Bird): + def fly(self) -> None: + print("Sparrow flying...") + +class Penguin(Bird): + def fly(self) -> None: + # Violates LSP: Penguins can't fly! + raise Exception("Penguins cannot fly!") + +def make_bird_fly(bird: Bird) -> None: + bird.fly() # This will fail for Penguin + +# ✅ Good: Following LSP - Proper abstraction +from abc import ABC, abstractmethod + +class Bird(ABC): + @abstractmethod + def move(self) -> None: + pass + +class FlyingBird(Bird): + def move(self) -> None: + self.fly() + + def fly(self) -> None: + print("Flying...") + +class SwimmingBird(Bird): + def move(self) -> None: + self.swim() + + def swim(self) -> None: + print("Swimming...") + +class Sparrow(FlyingBird): + def fly(self) -> None: + print("Sparrow flying...") + +class Penguin(SwimmingBird): + def swim(self) -> None: + print("Penguin swimming...") + +def make_bird_move(bird: Bird) -> None: + bird.move() # Works correctly for all bird types +``` + +### 4. Interface Segregation Principle (ISP) + +**Definition**: Clients should not be forced to depend on interfaces they do not use. Create specific, focused interfaces rather than large, general-purpose ones. + +#### Why ISP Matters: +- **Reduces coupling**: Classes only depend on what they need +- **Improves maintainability**: Changes to unused interface methods don't affect clients +- **Enhances flexibility**: Easier to implement only required functionality +- **Better testability**: Smaller interfaces are easier to mock + +#### TypeScript Examples: + +```typescript +// ❌ Bad: Violates ISP - Fat interface forces unnecessary dependencies +interface Worker { + work(): void; + eat(): void; + sleep(): void; + code(): void; + design(): void; +} + +class Developer implements Worker { + work(): void { console.log("Writing code"); } + eat(): void { console.log("Eating lunch"); } + sleep(): void { console.log("Sleeping"); } + code(): void { console.log("Coding applications"); } + design(): void { + // Developer forced to implement design method they don't use + throw new Error("Developers don't design"); + } +} + +class Designer implements Worker { + work(): void { console.log("Creating designs"); } + eat(): void { console.log("Eating lunch"); } + sleep(): void { console.log("Sleeping"); } + code(): void { + // Designer forced to implement code method they don't use + throw new Error("Designers don't code"); + } + design(): void { console.log("Designing interfaces"); } +} + +// ✅ Good: Following ISP - Segregated interfaces +interface Workable { + work(): void; +} + +interface Eatable { + eat(): void; +} + +interface Sleepable { + sleep(): void; +} + +interface Codeable { + code(): void; +} + +interface Designable { + design(): void; +} + +class Developer implements Workable, Eatable, Sleepable, Codeable { + work(): void { console.log("Writing code"); } + eat(): void { console.log("Eating lunch"); } + sleep(): void { console.log("Sleeping"); } + code(): void { console.log("Coding applications"); } +} + +class Designer implements Workable, Eatable, Sleepable, Designable { + work(): void { console.log("Creating designs"); } + eat(): void { console.log("Eating lunch"); } + sleep(): void { console.log("Sleeping"); } + design(): void { console.log("Designing interfaces"); } +} +``` + +#### Python Examples: + +```python +# ❌ Bad: Violates ISP +from abc import ABC, abstractmethod + +class Machine(ABC): + @abstractmethod + def print_document(self, document: str) -> None: + pass + + @abstractmethod + def scan_document(self) -> str: + pass + + @abstractmethod + def fax_document(self, document: str) -> None: + pass + +class SimplePrinter(Machine): + def print_document(self, document: str) -> None: + print(f"Printing: {document}") + + def scan_document(self) -> str: + # Forced to implement unused functionality + raise NotImplementedError("Simple printer cannot scan") + + def fax_document(self, document: str) -> None: + # Forced to implement unused functionality + raise NotImplementedError("Simple printer cannot fax") + +# ✅ Good: Following ISP +class Printable(ABC): + @abstractmethod + def print_document(self, document: str) -> None: + pass + +class Scannable(ABC): + @abstractmethod + def scan_document(self) -> str: + pass + +class Faxable(ABC): + @abstractmethod + def fax_document(self, document: str) -> None: + pass + +class SimplePrinter(Printable): + def print_document(self, document: str) -> None: + print(f"Printing: {document}") + +class AllInOnePrinter(Printable, Scannable, Faxable): + def print_document(self, document: str) -> None: + print(f"Printing: {document}") + + def scan_document(self) -> str: + return "Scanned document content" + + def fax_document(self, document: str) -> None: + print(f"Faxing: {document}") +``` + +### 5. Dependency Inversion Principle (DIP) + +**Definition**: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions. + +#### Why DIP Matters: +- **Reduces coupling**: High-level code doesn't depend on implementation details +- **Improves testability**: Easy to mock dependencies +- **Enhances flexibility**: Can swap implementations easily +- **Enables dependency injection**: Framework support for managing dependencies + +#### TypeScript Examples: + +```typescript +// ❌ Bad: Violates DIP - High-level module depends on low-level module +class MySQLDatabase { + save(data: string): void { + console.log(`Saving to MySQL: ${data}`); + } +} + +class UserService { + private database: MySQLDatabase; // Direct dependency on concrete class + + constructor() { + this.database = new MySQLDatabase(); // Tight coupling + } + + createUser(userData: string): void { + // Business logic + this.database.save(userData); + } +} + +// ✅ Good: Following DIP - Depend on abstractions +interface Database { + save(data: string): void; +} + +class MySQLDatabase implements Database { + save(data: string): void { + console.log(`Saving to MySQL: ${data}`); + } +} + +class PostgreSQLDatabase implements Database { + save(data: string): void { + console.log(`Saving to PostgreSQL: ${data}`); + } +} + +class UserService { + constructor(private database: Database) {} // Depends on abstraction + + createUser(userData: string): void { + // Business logic + this.database.save(userData); + } +} + +// Usage with dependency injection +const mysqlDb = new MySQLDatabase(); +const postgresDb = new PostgreSQLDatabase(); + +const userService1 = new UserService(mysqlDb); +const userService2 = new UserService(postgresDb); +``` + +#### Python Examples: + +```python +# ❌ Bad: Violates DIP +class EmailService: + def send_email(self, message: str) -> None: + print(f"Sending email: {message}") + +class NotificationService: + def __init__(self): + self.email_service = EmailService() # Direct dependency + + def send_notification(self, message: str) -> None: + self.email_service.send_email(message) + +# ✅ Good: Following DIP +from abc import ABC, abstractmethod + +class MessageSender(ABC): + @abstractmethod + def send(self, message: str) -> None: + pass + +class EmailService(MessageSender): + def send(self, message: str) -> None: + print(f"Sending email: {message}") + +class SMSService(MessageSender): + def send(self, message: str) -> None: + print(f"Sending SMS: {message}") + +class PushNotificationService(MessageSender): + def send(self, message: str) -> None: + print(f"Sending push notification: {message}") + +class NotificationService: + def __init__(self, message_sender: MessageSender): + self.message_sender = message_sender # Depends on abstraction + + def send_notification(self, message: str) -> None: + self.message_sender.send(message) + +# Usage +email_service = EmailService() +sms_service = SMSService() +push_service = PushNotificationService() + +notification_service1 = NotificationService(email_service) +notification_service2 = NotificationService(sms_service) +notification_service3 = NotificationService(push_service) +``` + +## Angular Project Example: Task Management Application + +Let's create a small Angular project that demonstrates SOLID principles with both bad and good code examples. + +### Project Structure + +``` +task-management/ +├── src/ +│ ├── app/ +│ │ ├── models/ +│ │ │ ├── task.model.ts +│ │ │ └── user.model.ts +│ │ ├── services/ +│ │ │ ├── interfaces/ +│ │ │ │ ├── task-storage.interface.ts +│ │ │ │ ├── notification.interface.ts +│ │ │ │ └── logger.interface.ts +│ │ │ ├── task.service.ts +│ │ │ ├── notification.service.ts +│ │ │ ├── localStorage-task-storage.service.ts +│ │ │ └── console-logger.service.ts +│ │ ├── components/ +│ │ │ ├── task-list/ +│ │ │ │ ├── task-list.component.ts +│ │ │ │ └── task-list.component.html +│ │ │ └── task-item/ +│ │ │ ├── task-item.component.ts +│ │ │ └── task-item.component.html +│ │ └── app.component.ts +│ └── main.ts +``` + +### Implementation Files + +#### Models + +```typescript +// models/task.model.ts +export interface Task { + id: string; + title: string; + description: string; + completed: boolean; + priority: 'low' | 'medium' | 'high'; + createdAt: Date; + dueDate?: Date; +} + +// models/user.model.ts +export interface User { + id: string; + name: string; + email: string; +} +``` + +#### Service Interfaces (ISP) + +```typescript +// services/interfaces/task-storage.interface.ts +export interface TaskStorage { + getTasks(): Promise; + saveTask(task: Task): Promise; + updateTask(task: Task): Promise; + deleteTask(id: string): Promise; +} + +// services/interfaces/notification.interface.ts +export interface NotificationSender { + send(message: string, type: 'info' | 'success' | 'error'): void; +} + +// services/interfaces/logger.interface.ts +export interface Logger { + log(message: string): void; + error(message: string): void; +} +``` + +#### Services Following SOLID Principles + +```typescript +// services/task.service.ts - SRP: Only handles task business logic +import { Injectable } from '@angular/core'; +import { Task } from '../models/task.model'; +import { TaskStorage } from './interfaces/task-storage.interface'; +import { NotificationSender } from './interfaces/notification.interface'; +import { Logger } from './interfaces/logger.interface'; + +/* +❌ BAD EXAMPLE (Violating SRP, DIP): +@Injectable() +export class BadTaskService { + private tasks: Task[] = []; + + constructor() { + // Violates DIP: Direct dependency on localStorage + this.loadTasksFromLocalStorage(); + } + + // Violates SRP: Mixing business logic with storage logic + addTask(title: string, description: string): void { + const task: Task = { + id: Date.now().toString(), + title, + description, + completed: false, + priority: 'medium', + createdAt: new Date() + }; + + this.tasks.push(task); + + // Violates SRP: Service shouldn't handle storage directly + localStorage.setItem('tasks', JSON.stringify(this.tasks)); + + // Violates SRP: Service shouldn't handle notifications directly + alert('Task added successfully!'); + + // Violates SRP: Service shouldn't handle logging directly + console.log(`Task added: ${task.title}`); + } + + private loadTasksFromLocalStorage(): void { + const stored = localStorage.getItem('tasks'); + this.tasks = stored ? JSON.parse(stored) : []; + } +} +*/ + +// ✅ GOOD EXAMPLE: Following SRP, DIP, OCP +@Injectable({ + providedIn: 'root' +}) +export class TaskService { + private tasks: Task[] = []; + + constructor( + private taskStorage: TaskStorage, // DIP: Depends on abstraction + private notificationService: NotificationSender, // DIP: Depends on abstraction + private logger: Logger // DIP: Depends on abstraction + ) { + this.loadTasks(); + } + + // SRP: Only business logic for task management + async addTask(title: string, description: string, priority: Task['priority'] = 'medium'): Promise { + const task: Task = { + id: this.generateId(), + title, + description, + completed: false, + priority, + createdAt: new Date() + }; + + try { + await this.taskStorage.saveTask(task); + this.tasks.push(task); + this.notificationService.send('Task added successfully!', 'success'); + this.logger.log(`Task added: ${task.title}`); + } catch (error) { + this.notificationService.send('Failed to add task', 'error'); + this.logger.error(`Failed to add task: ${error}`); + } + } + + async updateTask(updatedTask: Task): Promise { + try { + await this.taskStorage.updateTask(updatedTask); + const index = this.tasks.findIndex(t => t.id === updatedTask.id); + if (index !== -1) { + this.tasks[index] = updatedTask; + } + this.notificationService.send('Task updated successfully!', 'success'); + this.logger.log(`Task updated: ${updatedTask.title}`); + } catch (error) { + this.notificationService.send('Failed to update task', 'error'); + this.logger.error(`Failed to update task: ${error}`); + } + } + + async deleteTask(id: string): Promise { + try { + await this.taskStorage.deleteTask(id); + this.tasks = this.tasks.filter(t => t.id !== id); + this.notificationService.send('Task deleted successfully!', 'success'); + this.logger.log(`Task deleted: ${id}`); + } catch (error) { + this.notificationService.send('Failed to delete task', 'error'); + this.logger.error(`Failed to delete task: ${error}`); + } + } + + getTasks(): Task[] { + return [...this.tasks]; // Return copy to prevent external mutation + } + + getTasksByPriority(priority: Task['priority']): Task[] { + return this.tasks.filter(task => task.priority === priority); + } + + private async loadTasks(): Promise { + try { + this.tasks = await this.taskStorage.getTasks(); + } catch (error) { + this.logger.error(`Failed to load tasks: ${error}`); + } + } + + private generateId(): string { + return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } +} +``` + +#### Storage Implementation (DIP, LSP) + +```typescript +// services/localStorage-task-storage.service.ts +import { Injectable } from '@angular/core'; +import { Task } from '../models/task.model'; +import { TaskStorage } from './interfaces/task-storage.interface'; + +@Injectable({ + providedIn: 'root' +}) +export class LocalStorageTaskService implements TaskStorage { + private readonly STORAGE_KEY = 'tasks'; + + async getTasks(): Promise { + try { + const stored = localStorage.getItem(this.STORAGE_KEY); + return stored ? JSON.parse(stored) : []; + } catch (error) { + throw new Error(`Failed to load tasks from localStorage: ${error}`); + } + } + + async saveTask(task: Task): Promise { + try { + const tasks = await this.getTasks(); + tasks.push(task); + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(tasks)); + } catch (error) { + throw new Error(`Failed to save task to localStorage: ${error}`); + } + } + + async updateTask(updatedTask: Task): Promise { + try { + const tasks = await this.getTasks(); + const index = tasks.findIndex(t => t.id === updatedTask.id); + if (index === -1) { + throw new Error('Task not found'); + } + tasks[index] = updatedTask; + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(tasks)); + } catch (error) { + throw new Error(`Failed to update task in localStorage: ${error}`); + } + } + + async deleteTask(id: string): Promise { + try { + const tasks = await this.getTasks(); + const filteredTasks = tasks.filter(t => t.id !== id); + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filteredTasks)); + } catch (error) { + throw new Error(`Failed to delete task from localStorage: ${error}`); + } + } +} + +// Alternative implementation for API storage (OCP) +@Injectable() +export class ApiTaskStorageService implements TaskStorage { + constructor(private http: HttpClient) {} + + async getTasks(): Promise { + return this.http.get('/api/tasks').toPromise(); + } + + async saveTask(task: Task): Promise { + await this.http.post('/api/tasks', task).toPromise(); + } + + async updateTask(task: Task): Promise { + await this.http.put(`/api/tasks/${task.id}`, task).toPromise(); + } + + async deleteTask(id: string): Promise { + await this.http.delete(`/api/tasks/${id}`).toPromise(); + } +} +``` + +#### Additional Services + +```typescript +// services/notification.service.ts - SRP: Only handles notifications +import { Injectable } from '@angular/core'; +import { NotificationSender } from './interfaces/notification.interface'; + +@Injectable({ + providedIn: 'root' +}) +export class ToastNotificationService implements NotificationSender { + send(message: string, type: 'info' | 'success' | 'error'): void { + // In a real app, this would integrate with a toast library + const className = `toast-${type}`; + const toast = document.createElement('div'); + toast.className = className; + toast.textContent = message; + document.body.appendChild(toast); + + setTimeout(() => { + document.body.removeChild(toast); + }, 3000); + } +} + +// services/console-logger.service.ts - SRP: Only handles logging +import { Injectable } from '@angular/core'; +import { Logger } from './interfaces/logger.interface'; + +@Injectable({ + providedIn: 'root' +}) +export class ConsoleLoggerService implements Logger { + log(message: string): void { + console.log(`[LOG] ${new Date().toISOString()}: ${message}`); + } + + error(message: string): void { + console.error(`[ERROR] ${new Date().toISOString()}: ${message}`); + } +} +``` + +#### Component Implementation + +```typescript +// components/task-list/task-list.component.ts - SRP: Only handles task list UI +import { Component, OnInit } from '@angular/core'; +import { Task } from '../../models/task.model'; +import { TaskService } from '../../services/task.service'; + +@Component({ + selector: 'app-task-list', + templateUrl: './task-list.component.html' +}) +export class TaskListComponent implements OnInit { + tasks: Task[] = []; + newTaskTitle = ''; + newTaskDescription = ''; + selectedPriority: Task['priority'] = 'medium'; + + constructor(private taskService: TaskService) {} // DIP: Depends on abstraction + + ngOnInit(): void { + this.loadTasks(); + } + + async addTask(): Promise { + if (this.newTaskTitle.trim()) { + await this.taskService.addTask( + this.newTaskTitle.trim(), + this.newTaskDescription.trim(), + this.selectedPriority + ); + this.resetForm(); + this.loadTasks(); + } + } + + async onTaskUpdate(task: Task): Promise { + await this.taskService.updateTask(task); + this.loadTasks(); + } + + async onTaskDelete(taskId: string): Promise { + await this.taskService.deleteTask(taskId); + this.loadTasks(); + } + + private loadTasks(): void { + this.tasks = this.taskService.getTasks(); + } + + private resetForm(): void { + this.newTaskTitle = ''; + this.newTaskDescription = ''; + this.selectedPriority = 'medium'; + } +} +``` + +#### Dependency Injection Configuration + +```typescript +// app.module.ts - Proper DI configuration +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; + +import { AppComponent } from './app.component'; +import { TaskListComponent } from './components/task-list/task-list.component'; +import { TaskItemComponent } from './components/task-item/task-item.component'; + +import { TaskService } from './services/task.service'; +import { TaskStorage } from './services/interfaces/task-storage.interface'; +import { LocalStorageTaskService } from './services/localStorage-task-storage.service'; +import { NotificationSender } from './services/interfaces/notification.interface'; +import { ToastNotificationService } from './services/notification.service'; +import { Logger } from './services/interfaces/logger.interface'; +import { ConsoleLoggerService } from './services/console-logger.service'; + +@NgModule({ + declarations: [ + AppComponent, + TaskListComponent, + TaskItemComponent + ], + imports: [ + BrowserModule, + FormsModule + ], + providers: [ + TaskService, + // DIP: Bind abstractions to concrete implementations + { provide: TaskStorage, useClass: LocalStorageTaskService }, + { provide: NotificationSender, useClass: ToastNotificationService }, + { provide: Logger, useClass: ConsoleLoggerService } + + // Easy to swap implementations: + // { provide: TaskStorage, useClass: ApiTaskStorageService }, + // { provide: NotificationSender, useClass: EmailNotificationService }, + // { provide: Logger, useClass: FileLoggerService } + ], + bootstrap: [AppComponent] +}) +export class AppModule { } +``` + +## GitHub Projects That Enforce SOLID Principles + +### 1. **Angular Framework** (https://github.com/angular/angular) +- **Requirements**: Strict adherence to SOLID principles +- **Documentation**: Comprehensive style guide emphasizing separation of concerns +- **Examples**: Dependency injection system, modular architecture +- **Enforcement**: Detailed code review process and architectural guidelines + +### 2. **Spring Framework** (https://github.com/spring-projects/spring-framework) +- **Requirements**: DIP through extensive use of dependency injection +- **Documentation**: Inversion of Control container documentation +- **Examples**: Bean configuration, aspect-oriented programming +- **Enforcement**: Framework design inherently promotes SOLID principles + +### 3. **ASP.NET Core** (https://github.com/dotnet/aspnetcore) +- **Requirements**: Built-in dependency injection and interface-based design +- **Documentation**: Architectural guidance following SOLID principles +- **Examples**: Middleware pipeline, service registration +- **Enforcement**: Framework conventions and best practices + +### 4. **NestJS** (https://github.com/nestjs/nest) +- **Requirements**: Decorator-based dependency injection system +- **Documentation**: Extensive guides on modular architecture +- **Examples**: Guards, interceptors, pipes following ISP +- **Enforcement**: TypeScript decorators enforce proper abstractions + +### 5. **Clean Architecture Template** (https://github.com/jasontaylordev/CleanArchitecture) +- **Requirements**: Strict layered architecture following SOLID +- **Documentation**: Complete template with SOLID examples +- **Examples**: Domain-driven design implementation +- **Enforcement**: Project structure enforces separation of concerns + +### 6. **Onion Architecture** (https://github.com/amitpnk/Onion-architecture-ASP.NET-Core) +- **Requirements**: Dependency inversion at its core +- **Documentation**: Detailed explanation of each layer +- **Examples**: Repository pattern, service layer abstraction +- **Enforcement**: Project template structure + +## Best Practices for Applying SOLID Principles + +### 1. Start with Single Responsibility +- **Identify multiple reasons for change** in existing classes +- **Extract responsibilities** into separate classes +- **Focus on cohesion** within each class + +### 2. Design for Extension +- **Use interfaces and abstract classes** for extensibility +- **Prefer composition over inheritance** +- **Plan for future requirements** without over-engineering + +### 3. Respect Contracts +- **Ensure substitutability** in inheritance hierarchies +- **Maintain behavioral compatibility** in implementations +- **Document preconditions and postconditions** + +### 4. Keep Interfaces Focused +- **Create role-based interfaces** rather than large general ones +- **Group related methods** logically +- **Avoid forcing clients to depend on unused methods** + +### 5. Depend on Abstractions +- **Use dependency injection** frameworks and patterns +- **Program to interfaces** rather than concrete classes +- **Invert control flow** for better testability + +### 6. Continuous Refactoring +- **Apply SOLID principles gradually** during refactoring +- **Use automated tools** for code analysis +- **Regular code reviews** to ensure adherence + +### 7. Testing Strategy +- **Write unit tests** for individual responsibilities +- **Use mocking** for dependencies +- **Test behavior** rather than implementation + +## Common Anti-Patterns to Avoid + +### 1. God Objects +Large classes that violate SRP by handling multiple responsibilities. + +### 2. Tight Coupling +Direct dependencies on concrete classes instead of abstractions. + +### 3. Violation of LSP +Subclasses that break the expected behavior of parent classes. + +### 4. Fat Interfaces +Large interfaces that force clients to implement unused methods. + +### 5. High-Level Depending on Low-Level +Business logic directly depending on infrastructure concerns. + +## Conclusion + +SOLID principles are fundamental guidelines that lead to more maintainable, flexible, and robust software designs. When applied correctly, they: + +- **Improve code maintainability** by reducing coupling and increasing cohesion +- **Enable easier testing** through better separation of concerns +- **Facilitate code reuse** and extensibility +- **Reduce the risk of bugs** through clear responsibilities and contracts +- **Make systems more resilient to change** + +### Key Takeaways: + +1. **SRP**: One class, one responsibility, one reason to change +2. **OCP**: Open for extension, closed for modification +3. **LSP**: Subtypes must be substitutable for base types +4. **ISP**: Many specific interfaces are better than one general interface +5. **DIP**: Depend on abstractions, not concretions + +Remember, SOLID principles should be applied thoughtfully and pragmatically. The goal is not to follow them blindly, but to create better software designs that serve both current and future needs. + +**Start small, refactor gradually, and always consider the specific context of your application.** \ No newline at end of file diff --git a/angular-solid-example/README.md b/angular-solid-example/README.md new file mode 100644 index 0000000..3262966 --- /dev/null +++ b/angular-solid-example/README.md @@ -0,0 +1,164 @@ +# Angular SOLID Principles Example + +This is a practical demonstration of SOLID principles in an Angular task management application. + +## Project Structure + +``` +src/app/ +├── models/ # Data models and interfaces +│ └── index.ts # Task and User interfaces +├── services/ # Business logic and data access +│ ├── interfaces/ # Service abstractions (ISP) +│ │ └── index.ts # TaskStorage, NotificationSender, Logger interfaces +│ ├── task.service.ts # Core business logic (SRP, DIP) +│ ├── localStorage-task-storage.service.ts # Storage implementation (SRP, LSP) +│ └── notification-logger.services.ts # Notification & logging (SRP, ISP) +├── components/ # UI components +│ └── task-list/ # Task list component (SRP) +│ ├── task-list.component.ts +│ ├── task-list.component.html +│ └── task-list.component.css +└── app.module.ts # Dependency injection configuration (DIP) +``` + +## SOLID Principles Demonstrated + +### Single Responsibility Principle (SRP) +- **TaskService**: Only handles task business logic +- **LocalStorageTaskService**: Only handles localStorage operations +- **ToastNotificationService**: Only handles notifications +- **ConsoleLoggerService**: Only handles logging +- **TaskListComponent**: Only handles UI logic + +### Open/Closed Principle (OCP) +- New storage implementations can be added without modifying existing code +- New notification types can be added by implementing `NotificationSender` +- New logging mechanisms can be added by implementing `Logger` + +### Liskov Substitution Principle (LSP) +- Any `TaskStorage` implementation can be substituted without breaking the system +- All notification services implementing `NotificationSender` work interchangeably +- Logger implementations are fully substitutable + +### Interface Segregation Principle (ISP) +- **TaskStorage**: Focused on data operations only +- **NotificationSender**: Focused on sending notifications only +- **Logger**: Focused on logging operations only +- No service is forced to implement methods it doesn't need + +### Dependency Inversion Principle (DIP) +- High-level `TaskService` depends on abstractions (`TaskStorage`, `NotificationSender`, `Logger`) +- Concrete implementations are injected through Angular's DI system +- Easy to swap implementations for different environments (dev/test/prod) + +## Key Features + +### Good Code Examples (✅) +- Proper separation of concerns +- Dependency injection with abstractions +- Interface-based design +- Single responsibility per class/service + +### Bad Code Examples (❌) +- Commented examples showing SOLID violations +- Direct dependencies on concrete classes +- Mixed responsibilities in single classes +- Tight coupling between components + +## Running the Example + +1. Install Angular CLI: `npm install -g @angular/cli` +2. Create new project: `ng new task-management` +3. Copy the files from this example into your project +4. Install dependencies: `npm install` +5. Run the application: `ng serve` + +## Extension Points + +Thanks to SOLID principles, this application can be easily extended: + +### New Storage Options +```typescript +// Add API storage +export class ApiTaskStorageService implements TaskStorage { + // Implementation for REST API +} + +// Add database storage +export class DatabaseTaskStorageService implements TaskStorage { + // Implementation for database +} +``` + +### New Notification Types +```typescript +// Add email notifications +export class EmailNotificationService implements NotificationSender { + // Implementation for email +} + +// Add SMS notifications +export class SmsNotificationService implements NotificationSender { + // Implementation for SMS +} +``` + +### New Logging Options +```typescript +// Add file logging +export class FileLoggerService implements Logger { + // Implementation for file logging +} + +// Add remote logging +export class RemoteLoggerService implements Logger { + // Implementation for remote logging service +} +``` + +## Testing Benefits + +SOLID principles make testing much easier: + +```typescript +// Easy to mock dependencies for unit testing +describe('TaskService', () => { + let service: TaskService; + let mockStorage: jasmine.SpyObj; + let mockNotification: jasmine.SpyObj; + let mockLogger: jasmine.SpyObj; + + beforeEach(() => { + const storageSpy = jasmine.createSpyObj('TaskStorage', ['saveTask', 'getTasks']); + const notificationSpy = jasmine.createSpyObj('NotificationSender', ['send']); + const loggerSpy = jasmine.createSpyObj('Logger', ['log', 'error']); + + TestBed.configureTestingModule({ + providers: [ + TaskService, + { provide: TaskStorage, useValue: storageSpy }, + { provide: NotificationSender, useValue: notificationSpy }, + { provide: Logger, useValue: loggerSpy } + ] + }); + + service = TestBed.inject(TaskService); + mockStorage = TestBed.inject(TaskStorage) as jasmine.SpyObj; + mockNotification = TestBed.inject(NotificationSender) as jasmine.SpyObj; + mockLogger = TestBed.inject(Logger) as jasmine.SpyObj; + }); + + it('should add task successfully', async () => { + mockStorage.saveTask.and.returnValue(Promise.resolve()); + + await service.addTask('Test Task', 'Test Description'); + + expect(mockStorage.saveTask).toHaveBeenCalled(); + expect(mockNotification.send).toHaveBeenCalledWith('Task added successfully!', 'success'); + expect(mockLogger.log).toHaveBeenCalled(); + }); +}); +``` + +This example demonstrates how SOLID principles lead to more maintainable, testable, and extensible code. \ No newline at end of file diff --git a/angular-solid-example/src/app/app.module.ts b/angular-solid-example/src/app/app.module.ts new file mode 100644 index 0000000..9409634 --- /dev/null +++ b/angular-solid-example/src/app/app.module.ts @@ -0,0 +1,98 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; + +import { AppComponent } from './app.component'; +import { TaskListComponent } from './components/task-list/task-list.component'; + +// Services +import { TaskService } from './services/task.service'; + +// Service Interfaces and Implementations (DIP Configuration) +import { TaskStorage } from './services/interfaces'; +import { LocalStorageTaskService } from './services/localStorage-task-storage.service'; +import { NotificationSender, Logger } from './services/interfaces'; +import { ToastNotificationService, ConsoleLoggerService } from './services/notification-logger.services'; + +/* +❌ BAD EXAMPLE (Violating DIP): +If we configured services like this, we would violate DIP: + +@NgModule({ + providers: [ + TaskService, + LocalStorageTaskService, // Direct concrete dependency + ToastNotificationService, // Direct concrete dependency + ConsoleLoggerService // Direct concrete dependency + ] +}) + +This makes it impossible to swap implementations and violates the +Dependency Inversion Principle. +*/ + +// ✅ GOOD EXAMPLE: Following DIP with proper abstraction binding +@NgModule({ + declarations: [ + AppComponent, + TaskListComponent + ], + imports: [ + BrowserModule, + FormsModule + ], + providers: [ + TaskService, + + // DIP: Bind abstractions to concrete implementations + // This allows easy swapping of implementations + { provide: TaskStorage, useClass: LocalStorageTaskService }, + { provide: NotificationSender, useClass: ToastNotificationService }, + { provide: Logger, useClass: ConsoleLoggerService } + + // Easy to swap implementations for different environments: + // Production might use: + // { provide: TaskStorage, useClass: ApiTaskStorageService }, + // { provide: NotificationSender, useClass: EmailNotificationService }, + // { provide: Logger, useClass: FileLoggerService } + // + // Testing might use: + // { provide: TaskStorage, useClass: MockTaskStorageService }, + // { provide: NotificationSender, useClass: MockNotificationService }, + // { provide: Logger, useClass: MockLoggerService } + ], + bootstrap: [AppComponent] +}) +export class AppModule { } + +/* +Benefits of this DIP configuration: + +1. **Testability**: Easy to inject mock services for unit testing +2. **Flexibility**: Can swap implementations without changing business logic +3. **Maintainability**: Changes to concrete implementations don't affect consumers +4. **Environment-specific configuration**: Different implementations for dev/test/prod +5. **Following SOLID**: Adheres to Dependency Inversion Principle + +Example of environment-specific configuration: + +// environment.prod.ts +export const environment = { + production: true, + providers: [ + { provide: TaskStorage, useClass: ApiTaskStorageService }, + { provide: NotificationSender, useClass: EmailNotificationService }, + { provide: Logger, useClass: FileLoggerService } + ] +}; + +// environment.dev.ts +export const environment = { + production: false, + providers: [ + { provide: TaskStorage, useClass: LocalStorageTaskService }, + { provide: NotificationSender, useClass: ToastNotificationService }, + { provide: Logger, useClass: ConsoleLoggerService } + ] +}; +*/ \ No newline at end of file diff --git a/angular-solid-example/src/app/components/task-list/task-list.component.css b/angular-solid-example/src/app/components/task-list/task-list.component.css new file mode 100644 index 0000000..311f072 --- /dev/null +++ b/angular-solid-example/src/app/components/task-list/task-list.component.css @@ -0,0 +1,258 @@ +/* Task List Component Styles */ +/* Following consistent design principles */ + +.task-list-container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.add-task-form { + background: #f8f9fa; + padding: 20px; + border-radius: 8px; + margin-bottom: 30px; + border: 1px solid #e9ecef; +} + +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-weight: 600; + color: #333; +} + +.form-control { + width: 100%; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + transition: border-color 0.15s ease-in-out; +} + +.form-control:focus { + outline: none; + border-color: #007bff; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); +} + +.btn { + padding: 8px 16px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.15s ease-in-out; + text-decoration: none; + display: inline-block; +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-primary { + background-color: #007bff; + color: white; +} + +.btn-primary:hover:not(:disabled) { + background-color: #0056b3; +} + +.btn-success { + background-color: #28a745; + color: white; +} + +.btn-success:hover { + background-color: #1e7e34; +} + +.btn-warning { + background-color: #ffc107; + color: #212529; +} + +.btn-warning:hover { + background-color: #e0a800; +} + +.btn-danger { + background-color: #dc3545; + color: white; +} + +.btn-danger:hover { + background-color: #c82333; +} + +.btn-sm { + padding: 4px 8px; + font-size: 12px; + margin-right: 5px; +} + +.priority-section { + margin-bottom: 30px; +} + +.priority-section h3 { + color: #333; + border-bottom: 2px solid #007bff; + padding-bottom: 10px; + margin-bottom: 15px; +} + +.tasks-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 15px; +} + +.task-card { + background: white; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 15px; + transition: all 0.15s ease-in-out; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.task-card:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + transform: translateY(-1px); +} + +.task-card.completed { + opacity: 0.7; + background-color: #f8f9fa; +} + +.task-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.task-header h4 { + margin: 0; + color: #333; + font-size: 16px; +} + +.task-card.completed .task-header h4 { + text-decoration: line-through; +} + +.priority-badge { + padding: 3px 8px; + border-radius: 12px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; +} + +.priority-high { + background-color: #dc3545; + color: white; +} + +.priority-medium { + background-color: #ffc107; + color: #212529; +} + +.priority-low { + background-color: #28a745; + color: white; +} + +.task-description { + color: #666; + margin-bottom: 10px; + line-height: 1.4; +} + +.task-meta { + font-size: 12px; + color: #888; + margin-bottom: 15px; +} + +.task-actions { + display: flex; + gap: 5px; +} + +.no-tasks { + text-align: center; + color: #888; + font-style: italic; + padding: 20px; +} + +.task-stats { + background: #f8f9fa; + padding: 20px; + border-radius: 8px; + border: 1px solid #e9ecef; +} + +.task-stats h3 { + margin-top: 0; + color: #333; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 15px; +} + +.stat-item { + text-align: center; + padding: 15px; + background: white; + border-radius: 6px; + border: 1px solid #e9ecef; +} + +.stat-number { + display: block; + font-size: 24px; + font-weight: bold; + color: #007bff; +} + +.stat-label { + display: block; + font-size: 12px; + color: #666; + text-transform: uppercase; + margin-top: 5px; +} + +/* Responsive design */ +@media (max-width: 768px) { + .task-list-container { + padding: 10px; + } + + .tasks-grid { + grid-template-columns: 1fr; + } + + .stats-grid { + grid-template-columns: repeat(3, 1fr); + } +} \ No newline at end of file diff --git a/angular-solid-example/src/app/components/task-list/task-list.component.html b/angular-solid-example/src/app/components/task-list/task-list.component.html new file mode 100644 index 0000000..3252349 --- /dev/null +++ b/angular-solid-example/src/app/components/task-list/task-list.component.html @@ -0,0 +1,119 @@ + + + +
+

Task Management - SOLID Principles Demo

+ + +
+

Add New Task

+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+
+

{{ priority | titlecase }} Priority Tasks

+ +
+
+ +
+

{{ task.title }}

+ + {{ task.priority }} + +
+ +

{{ task.description }}

+ +
+ Created: {{ task.createdAt | date:'short' }} +
+ Due: {{ task.dueDate | date:'short' }} +
+
+ +
+ + + +
+
+
+ +
+ No {{ priority }} priority tasks +
+
+
+ + +
+

Task Statistics

+
+
+ {{ tasks.length }} + Total Tasks +
+
+ {{ tasks.filter(t => t.completed).length }} + Completed +
+
+ {{ tasks.filter(t => !t.completed).length }} + Pending +
+
+
+
\ No newline at end of file diff --git a/angular-solid-example/src/app/components/task-list/task-list.component.ts b/angular-solid-example/src/app/components/task-list/task-list.component.ts new file mode 100644 index 0000000..41a2773 --- /dev/null +++ b/angular-solid-example/src/app/components/task-list/task-list.component.ts @@ -0,0 +1,117 @@ +import { Component, OnInit } from '@angular/core'; +import { Task } from '../../models'; +import { TaskService } from '../../services/task.service'; + +/* +❌ BAD EXAMPLE (Violating SRP): +This component would violate SRP if it handled: +- UI logic +- Business logic +- Data access +- Validation + +@Component({ + selector: 'app-bad-task-list', + template: `...` +}) +export class BadTaskListComponent { + tasks: Task[] = []; + + constructor() { + // Violates SRP: Component handling data access directly + this.loadTasksFromLocalStorage(); + } + + addTask(title: string, description: string): void { + // Violates SRP: Component handling business logic + const task: Task = { + id: Date.now().toString(), + title, + description, + completed: false, + priority: 'medium', + createdAt: new Date() + }; + + // Violates SRP: Component handling validation + if (!this.validateTask(task)) { + alert('Invalid task data'); + return; + } + + this.tasks.push(task); + + // Violates SRP: Component handling persistence + localStorage.setItem('tasks', JSON.stringify(this.tasks)); + + // Violates SRP: Component handling notifications + alert('Task added successfully!'); + } + + private loadTasksFromLocalStorage(): void { + const stored = localStorage.getItem('tasks'); + this.tasks = stored ? JSON.parse(stored) : []; + } + + private validateTask(task: Task): boolean { + return task.title.length > 0 && task.description.length > 0; + } +} +*/ + +// ✅ GOOD EXAMPLE: Following SRP +// Component only handles UI logic and delegates to services +@Component({ + selector: 'app-task-list', + templateUrl: './task-list.component.html', + styleUrls: ['./task-list.component.css'] +}) +export class TaskListComponent implements OnInit { + tasks: Task[] = []; + newTaskTitle = ''; + newTaskDescription = ''; + selectedPriority: Task['priority'] = 'medium'; + + constructor(private taskService: TaskService) {} // DIP: Depends on abstraction + + ngOnInit(): void { + this.loadTasks(); + } + + async addTask(): Promise { + if (this.newTaskTitle.trim()) { + await this.taskService.addTask( + this.newTaskTitle.trim(), + this.newTaskDescription.trim(), + this.selectedPriority + ); + this.resetForm(); + this.loadTasks(); + } + } + + async onTaskUpdate(task: Task): Promise { + await this.taskService.updateTask(task); + this.loadTasks(); + } + + async onTaskDelete(taskId: string): Promise { + await this.taskService.deleteTask(taskId); + this.loadTasks(); + } + + filterTasksByPriority(priority: Task['priority']): Task[] { + return this.tasks.filter(task => task.priority === priority); + } + + // SRP: Component only handles UI-related logic + private loadTasks(): void { + this.tasks = this.taskService.getTasks(); + } + + private resetForm(): void { + this.newTaskTitle = ''; + this.newTaskDescription = ''; + this.selectedPriority = 'medium'; + } +} \ No newline at end of file diff --git a/angular-solid-example/src/app/models/index.ts b/angular-solid-example/src/app/models/index.ts new file mode 100644 index 0000000..7f3ca0d --- /dev/null +++ b/angular-solid-example/src/app/models/index.ts @@ -0,0 +1,17 @@ +// Task model interface +export interface Task { + id: string; + title: string; + description: string; + completed: boolean; + priority: 'low' | 'medium' | 'high'; + createdAt: Date; + dueDate?: Date; +} + +// User model interface +export interface User { + id: string; + name: string; + email: string; +} \ No newline at end of file diff --git a/angular-solid-example/src/app/services/interfaces/index.ts b/angular-solid-example/src/app/services/interfaces/index.ts new file mode 100644 index 0000000..13417fb --- /dev/null +++ b/angular-solid-example/src/app/services/interfaces/index.ts @@ -0,0 +1,20 @@ +import { Task } from '../models'; + +// ISP: Focused interface for task storage operations +export interface TaskStorage { + getTasks(): Promise; + saveTask(task: Task): Promise; + updateTask(task: Task): Promise; + deleteTask(id: string): Promise; +} + +// ISP: Focused interface for notifications +export interface NotificationSender { + send(message: string, type: 'info' | 'success' | 'error'): void; +} + +// ISP: Focused interface for logging +export interface Logger { + log(message: string): void; + error(message: string): void; +} \ No newline at end of file diff --git a/angular-solid-example/src/app/services/localStorage-task-storage.service.ts b/angular-solid-example/src/app/services/localStorage-task-storage.service.ts new file mode 100644 index 0000000..39d57f3 --- /dev/null +++ b/angular-solid-example/src/app/services/localStorage-task-storage.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@angular/core'; +import { Task } from '../models'; +import { TaskStorage } from './interfaces'; + +// SRP: Only responsible for localStorage operations +// LSP: Can be substituted wherever TaskStorage is expected +@Injectable({ + providedIn: 'root' +}) +export class LocalStorageTaskService implements TaskStorage { + private readonly STORAGE_KEY = 'tasks'; + + async getTasks(): Promise { + try { + const stored = localStorage.getItem(this.STORAGE_KEY); + return stored ? JSON.parse(stored) : []; + } catch (error) { + throw new Error(`Failed to load tasks from localStorage: ${error}`); + } + } + + async saveTask(task: Task): Promise { + try { + const tasks = await this.getTasks(); + tasks.push(task); + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(tasks)); + } catch (error) { + throw new Error(`Failed to save task to localStorage: ${error}`); + } + } + + async updateTask(updatedTask: Task): Promise { + try { + const tasks = await this.getTasks(); + const index = tasks.findIndex(t => t.id === updatedTask.id); + if (index === -1) { + throw new Error('Task not found'); + } + tasks[index] = updatedTask; + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(tasks)); + } catch (error) { + throw new Error(`Failed to update task in localStorage: ${error}`); + } + } + + async deleteTask(id: string): Promise { + try { + const tasks = await this.getTasks(); + const filteredTasks = tasks.filter(t => t.id !== id); + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filteredTasks)); + } catch (error) { + throw new Error(`Failed to delete task from localStorage: ${error}`); + } + } +} \ No newline at end of file diff --git a/angular-solid-example/src/app/services/notification-logger.services.ts b/angular-solid-example/src/app/services/notification-logger.services.ts new file mode 100644 index 0000000..ec6d53b --- /dev/null +++ b/angular-solid-example/src/app/services/notification-logger.services.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@angular/core'; +import { NotificationSender, Logger } from './interfaces'; + +// SRP: Only handles toast notifications +@Injectable({ + providedIn: 'root' +}) +export class ToastNotificationService implements NotificationSender { + send(message: string, type: 'info' | 'success' | 'error'): void { + // In a real app, this would integrate with a toast library like ngx-toastr + const className = `toast-${type}`; + const toast = document.createElement('div'); + toast.className = className; + toast.textContent = message; + toast.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 10px 20px; + border-radius: 4px; + color: white; + font-weight: bold; + z-index: 1000; + background-color: ${this.getBackgroundColor(type)}; + `; + + document.body.appendChild(toast); + + setTimeout(() => { + if (document.body.contains(toast)) { + document.body.removeChild(toast); + } + }, 3000); + } + + private getBackgroundColor(type: 'info' | 'success' | 'error'): string { + switch (type) { + case 'success': return '#4CAF50'; + case 'error': return '#F44336'; + case 'info': return '#2196F3'; + default: return '#2196F3'; + } + } +} + +// SRP: Only handles console logging +@Injectable({ + providedIn: 'root' +}) +export class ConsoleLoggerService implements Logger { + log(message: string): void { + console.log(`[LOG] ${new Date().toISOString()}: ${message}`); + } + + error(message: string): void { + console.error(`[ERROR] ${new Date().toISOString()}: ${message}`); + } +} \ No newline at end of file diff --git a/angular-solid-example/src/app/services/task.service.ts b/angular-solid-example/src/app/services/task.service.ts new file mode 100644 index 0000000..47cb191 --- /dev/null +++ b/angular-solid-example/src/app/services/task.service.ts @@ -0,0 +1,132 @@ +import { Injectable } from '@angular/core'; +import { Task } from '../models'; +import { TaskStorage, NotificationSender, Logger } from './interfaces'; + +/* +❌ BAD EXAMPLE (Violating SRP, DIP): +This service violates multiple SOLID principles: +- SRP: Mixes business logic with storage and notification concerns +- DIP: Directly depends on localStorage and alert() instead of abstractions + +@Injectable() +export class BadTaskService { + private tasks: Task[] = []; + + constructor() { + // Violates DIP: Direct dependency on localStorage + this.loadTasksFromLocalStorage(); + } + + addTask(title: string, description: string): void { + const task: Task = { + id: Date.now().toString(), + title, + description, + completed: false, + priority: 'medium', + createdAt: new Date() + }; + + this.tasks.push(task); + + // Violates SRP: Service shouldn't handle storage directly + localStorage.setItem('tasks', JSON.stringify(this.tasks)); + + // Violates SRP: Service shouldn't handle notifications directly + alert('Task added successfully!'); + + // Violates SRP: Service shouldn't handle logging directly + console.log(`Task added: ${task.title}`); + } + + private loadTasksFromLocalStorage(): void { + const stored = localStorage.getItem('tasks'); + this.tasks = stored ? JSON.parse(stored) : []; + } +} +*/ + +// ✅ GOOD EXAMPLE: Following SRP, DIP, OCP +@Injectable({ + providedIn: 'root' +}) +export class TaskService { + private tasks: Task[] = []; + + constructor( + private taskStorage: TaskStorage, // DIP: Depends on abstraction + private notificationService: NotificationSender, // DIP: Depends on abstraction + private logger: Logger // DIP: Depends on abstraction + ) { + this.loadTasks(); + } + + // SRP: Only business logic for task management + async addTask(title: string, description: string, priority: Task['priority'] = 'medium'): Promise { + const task: Task = { + id: this.generateId(), + title, + description, + completed: false, + priority, + createdAt: new Date() + }; + + try { + await this.taskStorage.saveTask(task); + this.tasks.push(task); + this.notificationService.send('Task added successfully!', 'success'); + this.logger.log(`Task added: ${task.title}`); + } catch (error) { + this.notificationService.send('Failed to add task', 'error'); + this.logger.error(`Failed to add task: ${error}`); + } + } + + async updateTask(updatedTask: Task): Promise { + try { + await this.taskStorage.updateTask(updatedTask); + const index = this.tasks.findIndex(t => t.id === updatedTask.id); + if (index !== -1) { + this.tasks[index] = updatedTask; + } + this.notificationService.send('Task updated successfully!', 'success'); + this.logger.log(`Task updated: ${updatedTask.title}`); + } catch (error) { + this.notificationService.send('Failed to update task', 'error'); + this.logger.error(`Failed to update task: ${error}`); + } + } + + async deleteTask(id: string): Promise { + try { + await this.taskStorage.deleteTask(id); + this.tasks = this.tasks.filter(t => t.id !== id); + this.notificationService.send('Task deleted successfully!', 'success'); + this.logger.log(`Task deleted: ${id}`); + } catch (error) { + this.notificationService.send('Failed to delete task', 'error'); + this.logger.error(`Failed to delete task: ${error}`); + } + } + + getTasks(): Task[] { + return [...this.tasks]; // Return copy to prevent external mutation + } + + getTasksByPriority(priority: Task['priority']): Task[] { + return this.tasks.filter(task => task.priority === priority); + } + + private async loadTasks(): Promise { + try { + this.tasks = await this.taskStorage.getTasks(); + } catch (error) { + this.logger.error(`Failed to load tasks: ${error}`); + } + } + + private generateId(): string { + return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } +} \ No newline at end of file