Table of Contents
Introduction to Object-Oriented Design
1.1 Why OOP Matters in Modern Software Development
1.2 Overview of Module 2
1.3 Real-World Relevance of OOP Principles
Core OOP Principles
2.1 Abstraction
2.1.1 Definition and Importance
2.1.2 Real-World Example: Inventory Management System
2.1.3 C# Example with ASP.NET
2.1.4 Java Example
2.1.5 Best Practices, Pros, Cons, and Alternatives
2.2 Encapsulation
2.2.1 Definition and Importance
2.2.2 Real-World Example: User Account Management
2.2.3 C# Example with ASP.NET and SQL Server
2.2.4 Java Example
2.2.5 Best Practices, Pros, Cons, and Alternatives
2.3 Inheritance
2.3.1 Definition and Importance
2.3.2 Real-World Example: E-Commerce Product Hierarchy
2.3.3 C# Example with ASP.NET
2.3.4 Java Example
2.3.5 Best Practices, Pros, Cons, and Alternatives
2.4 Polymorphism
2.4.1 Definition and Importance
2.4.2 Real-World Example: Payment Processing
2.4.3 C# Example with ASP.NET
2.4.4 Java Example
2.4.5 Best Practices, Pros, Cons, and Alternatives
Interfaces and Abstract Classes
3.1 Understanding Interfaces
3.1.1 Real-World Example: Plugin Architecture
3.1.2 C# Example with ASP.NET
3.1.3 Java Example
3.2 Understanding Abstract Classes
3.2.1 Real-World Example: Vehicle Management System
3.2.2 C# Example with ASP.NET
3.2.3 Java Example
3.3 Interfaces vs. Abstract Classes
3.3.1 Key Differences
3.3.2 When to Use Each
Coupling and Cohesion
4.1 Defining Coupling
4.1.1 Types of Coupling
4.1.2 Real-World Example: Order Processing System
4.1.3 C# Example with ASP.NET and SQL Server
4.1.4 Java Example
4.2 Defining Cohesion
4.2.1 Types of Cohesion
4.2.2 Real-World Example: Customer Management
4.2.3 C# Example with ASP.NET
4.2.4 Java Example
4.3 Best Practices for Low Coupling and High Cohesion
Dependency Inversion and Inversion of Control (IoC)
5.1 Dependency Inversion Principle (DIP)
5.1.1 Definition and Importance
5.1.2 Real-World Example: Notification System
5.1.3 C# Example with ASP.NET Core DI
5.1.4 Java Example with Spring IoC
5.2 Inversion of Control (IoC)
5.2.1 Definition and Importance
5.2.2 Real-World Example: Service Orchestration
5.2.3 C# Example with ASP.NET Core
5.2.4 Java Example with Spring
5.3 Best Practices, Pros, Cons, and Alternatives
Composition Over Inheritance
6.1 Why Composition Over Inheritance?
6.1.1 Real-World Example: UI Component System
6.1.2 C# Example with ASP.NET
6.1.3 Java Example
6.2 Best Practices, Pros, Cons, and Alternatives
Modern OOP Features in .NET 7+ and Java 21+
7.1 .NET 7+ Features
7.1.1 Record Types
7.1.2 Pattern Matching Enhancements
7.1.3 C# Example with ASP.NET
7.2 Java 21+ Features
7.2.1 Record Classes
7.2.2 Sealed Classes
7.2.3 Java Example
7.3 Integrating Modern Features with OOP Principles
Exception Handling in OOP
8.1 Importance of Exception Handling
8.2 C# Exception Handling with ASP.NET and SQL Server
8.3 Java Exception Handling
8.4 Best Practices for Exception Handling
Conclusion and Next Steps
9.1 Recap of Module 2
9.2 How to Apply These Principles in Your Projects
9.3 Preview of Module 3: Creational Design Patterns
1. Introduction to Object-Oriented Design
1.1 Why OOP Matters in Modern Software Development
Object-Oriented Programming (OOP) is the backbone of modern software engineering, enabling developers to build scalable, maintainable, and reusable systems. By modeling real-world entities as objects, OOP provides a structured approach to solving complex problems. In .NET 7+ and Java 21+, OOP principles are enhanced with modern features, making them more powerful for building enterprise-grade applications.
1.2 Overview of Module 2
This module dives into the core principles of OOP—abstraction, encapsulation, inheritance, and polymorphism—while exploring advanced concepts like interfaces, abstract classes, coupling, cohesion, dependency inversion, inversion of control (IoC), and composition over inheritance. We’ll also cover modern OOP features in .NET 7+ and Java 21+, with practical C# ASP.NET and SQL Server examples, alongside Java equivalents.
1.3 Real-World Relevance of OOP Principles
OOP principles are used in real-world applications like e-commerce platforms, banking systems, and inventory management. For example, in an ASP.NET-based e-commerce system, OOP allows developers to model products, orders, and customers as objects, ensuring modularity and ease of maintenance.
2. Core OOP Principles
2.1 Abstraction
2.1.1 Definition and Importance
Abstraction involves hiding complex implementation details and exposing only the necessary functionality to the user. It simplifies code by focusing on what an object does rather than how it does it. In .NET and Java, abstraction is achieved using interfaces and abstract classes.
2.1.2 Real-World Example: Inventory Management System
In an inventory management system, a user interacts with a product’s details (e.g., name, price) without needing to know how the data is stored in a SQL Server database or how stock levels are updated.
2.1.3 C# Example with ASP.NET
Let’s create an abstraction for an inventory service in an ASP.NET Core application.
public interface IInventoryService
{
Task<Product> GetProductAsync(int id);
Task UpdateStockAsync(int id, int quantity);
}
public class InventoryService : IInventoryService
{
private readonly SqlConnection _connection;
public InventoryService(string connectionString)
{
_connection = new SqlConnection(connectionString);
}
public async Task<Product> GetProductAsync(int id)
{
try
{
await _connection.OpenAsync();
using var command = new SqlCommand("SELECT * FROM Products WHERE Id = @Id", _connection);
command.Parameters.AddWithValue("@Id", id);
using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return new Product
{
Id = reader.GetInt32("Id"),
Name = reader.GetString("Name"),
Price = reader.GetDecimal("Price"),
Stock = reader.GetInt32("Stock")
};
}
throw new KeyNotFoundException("Product not found");
}
catch (SqlException ex)
{
throw new Exception("Database error occurred", ex);
}
finally
{
await _connection.CloseAsync();
}
}
public async Task UpdateStockAsync(int id, int quantity)
{
try
{
await _connection.OpenAsync();
using var command = new SqlCommand("UPDATE Products SET Stock = Stock + @Quantity WHERE Id = @Id", _connection);
command.Parameters.AddWithValue("@Id", id);
command.Parameters.AddWithValue("@Quantity", quantity);
await command.ExecuteNonQueryAsync();
}
catch (SqlException ex)
{
throw new Exception("Failed to update stock", ex);
}
finally
{
await _connection.CloseAsync();
}
}
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}
2.1.4 Java Example
A Java equivalent using a similar abstraction:
public interface InventoryService {
Product getProduct(int id) throws Exception;
void updateStock(int id, int quantity) throws Exception;
}
public class InventoryServiceImpl implements InventoryService {
private final Connection connection;
public InventoryServiceImpl(String url, String user, String password) throws SQLException {
this.connection = DriverManager.getConnection(url, user, password);
}
@Override
public Product getProduct(int id) throws Exception {
try {
PreparedStatement stmt = connection.prepareStatement("SELECT * FROM Products WHERE id = ?");
stmt.setInt(1, id);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return new Product(
rs.getInt("id"),
rs.getString("name"),
rs.getDouble("price"),
rs.getInt("stock")
);
}
throw new Exception("Product not found");
} catch (SQLException e) {
throw new Exception("Database error", e);
} finally {
connection.close();
}
}
@Override
public void updateStock(int id, int quantity) throws Exception {
try {
PreparedStatement stmt = connection.prepareStatement("UPDATE Products SET stock = stock + ? WHERE id = ?");
stmt.setInt(1, quantity);
stmt.setInt(2, id);
stmt.executeUpdate();
} catch (SQLException e) {
throw new Exception("Failed to update stock", e);
} finally {
connection.close();
}
}
}
public class Product {
private int id;
private String name;
private double price;
private int stock;
public Product(int id, String name, double price, int stock) {
this.id = id;
this.name = name;
this.price = price;
this.stock = stock;
}
// Getters and setters
}
2.1.5 Best Practices, Pros, Cons, and Alternatives
Best Practices:
Define clear interfaces for abstractions.
Use meaningful method names that describe functionality.
Handle exceptions gracefully to ensure robustness.
Pros:
Simplifies complex systems.
Enhances code reusability.
Improves maintainability by hiding implementation details.
Cons:
Over-abstraction can lead to unnecessary complexity.
May introduce performance overhead in some cases.
Alternatives:
Functional programming for simpler systems.
Procedural programming for small-scale applications.
2.2 Encapsulation
2.2.1 Definition and Importance
Encapsulation hides an object’s internal state and exposes only the necessary methods to interact with it, ensuring data integrity and security. In .NET and Java, encapsulation is achieved using private fields and public properties or methods.
2.2.2 Real-World Example: User Account Management
In a user account system, sensitive data like passwords should be hidden, and access should be controlled via methods.
2.2.3 C# Example with ASP.NET and SQL Server
Here’s an encapsulated User class in an ASP.NET Core application:
public class User
{
private string _passwordHash;
private readonly SqlConnection _connection;
public User(string connectionString)
{
_connection = new SqlConnection(connectionString);
}
public int Id { get; private set; }
public string Username { get; private set; }
public string Email { get; private set; }
public async Task CreateUserAsync(string username, string email, string password)
{
try
{
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
throw new ArgumentException("Invalid user data");
_passwordHash = HashPassword(password);
await _connection.OpenAsync();
using var command = new SqlCommand(
"INSERT INTO Users (Username, Email, PasswordHash) OUTPUT INSERTED.Id VALUES (@Username, @Email, @PasswordHash)",
_connection);
command.Parameters.AddWithValue("@Username", username);
command.Parameters.AddWithValue("@Email", email);
command.Parameters.AddWithValue("@PasswordHash", _passwordHash);
Id = (int)await command.ExecuteScalarAsync();
Username = username;
Email = email;
}
catch (SqlException ex)
{
throw new Exception("Failed to create user", ex);
}
finally
{
await _connection.CloseAsync();
}
}
private string HashPassword(string password)
{
// Simplified hashing for demonstration
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(password));
}
public bool ValidatePassword(string password)
{
return _passwordHash == HashPassword(password);
}
}
2.2.4 Java Example
A Java equivalent for user account management:
public class User {
private int id;
private String username;
private String email;
private String passwordHash;
private final Connection connection;
public User(String url, String user, String password) throws SQLException {
this.connection = DriverManager.getConnection(url, user, password);
}
public int getId() { return id; }
public String getUsername() { return username; }
public String getEmail() { return email; }
public void createUser(String username, String email, String password) throws Exception {
if (username == null || email == null || password == null) {
throw new IllegalArgumentException("Invalid user data");
}
try {
this.passwordHash = hashPassword(password);
PreparedStatement stmt = connection.prepareStatement(
"INSERT INTO Users (username, email, password_hash) VALUES (?, ?, ?)",
Statement.RETURN_GENERATED_KEYS
);
stmt.setString(1, username);
stmt.setString(2, email);
stmt.setString(3, passwordHash);
stmt.executeUpdate();
ResultSet rs = stmt.getGeneratedKeys();
if (rs.next()) {
this.id = rs.getInt(1);
}
this.username = username;
this.email = email;
} catch (SQLException e) {
throw new Exception("Failed to create user", e);
} finally {
connection.close();
}
}
private String hashPassword(String password) {
return Base64.getEncoder().encodeToString(password.getBytes());
}
public boolean validatePassword(String password) {
return passwordHash.equals(hashPassword(password));
}
}
2.2.5 Best Practices, Pros, Cons, and Alternatives
Best Practices:
Use private fields with public getters/setters.
Validate inputs to ensure data integrity.
Avoid exposing internal state directly.
Pros:
Enhances security by controlling data access.
Improves maintainability by hiding implementation details.
Cons:
Can increase code verbosity.
Overuse of getters/setters may break encapsulation.
Alternatives:
Immutable objects for simpler state management.
Functional programming with pure functions.
2.3 Inheritance
2.3.1 Definition and Importance
Inheritance allows a class to inherit properties and methods from another class, promoting code reuse. However, it can lead to tight coupling if overused.
2.3.2 Real-World Example: E-Commerce Product Hierarchy
In an e-commerce system, products like Book and Electronics can inherit from a base Product class.
2.3.3 C# Example with ASP.NET
Here’s an inheritance example for products:
public abstract class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public virtual async Task SaveToDatabaseAsync(SqlConnection connection)
{
try
{
await connection.OpenAsync();
using var command = new SqlCommand(
"INSERT INTO Products (Name, Price) VALUES (@Name, @Price)",
connection);
command.Parameters.AddWithValue("@Name", Name);
command.Parameters.AddWithValue("@Price", Price);
await command.ExecuteNonQueryAsync();
}
catch (SqlException ex)
{
throw new Exception("Failed to save product", ex);
}
finally
{
await connection.CloseAsync();
}
}
}
public class Book : Product
{
public string Author { get; set; }
public override async Task SaveToDatabaseAsync(SqlConnection connection)
{
try
{
await base.SaveToDatabaseAsync(connection);
using var command = new SqlCommand(
"INSERT INTO Books (ProductId, Author) VALUES (@ProductId, @Author)",
connection);
command.Parameters.AddWithValue("@ProductId", Id);
command.Parameters.AddWithValue("@Author", Author);
await command.ExecuteNonQueryAsync();
}
catch (SqlException ex)
{
throw new Exception("Failed to save book", ex);
}
}
}
2.3.4 Java Example
A Java equivalent:
public abstract class Product {
protected int id;
protected String name;
protected double price;
public int getId() { return id; }
public String getName() { return name; }
public double getPrice() { return price; }
public void setId(int id) { this.id = id; }
public void setName(String name) { this.name = name; }
public void setPrice(double price) { this.price = price; }
public void saveToDatabase(Connection connection) throws SQLException {
try {
PreparedStatement stmt = connection.prepareStatement(
"INSERT INTO Products (name, price) VALUES (?, ?)"
);
stmt.setString(1, name);
stmt.setDouble(2, price);
stmt.executeUpdate();
} catch (SQLException e) {
throw new SQLException("Failed to save product", e);
}
}
}
public class Book extends Product {
private String author;
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
@Override
public void saveToDatabase(Connection connection) throws SQLException {
try {
super.saveToDatabase(connection);
PreparedStatement stmt = connection.prepareStatement(
"INSERT INTO Books (product_id, author) VALUES (?, ?)"
);
stmt.setInt(1, id);
stmt.setString(2, author);
stmt.executeUpdate();
} catch (SQLException e) {
throw new SQLException("Failed to save book", e);
}
}
}
2.3.5 Best Practices, Pros, Cons, and Alternatives
Best Practices:
Use inheritance only when there’s a clear “is-a” relationship.
Prefer shallow inheritance hierarchies.
Override methods thoughtfully to extend behavior.
Pros:
Promotes code reuse.
Simplifies modeling hierarchical relationships.
Cons:
Tight coupling between parent and child classes.
Fragile base class problem.
Alternatives:
Composition for greater flexibility.
Interfaces for defining contracts without implementation.
2.4 Polymorphism
2.4.1 Definition and Importance
Polymorphism allows objects of different classes to be treated as instances of a common base class or interface, enabling flexible and extensible code.
2.4.2 Real-World Example: Payment Processing
In an e-commerce system, different payment methods (e.g., credit card, PayPal) can share a common interface.
2.4.3 C# Example with ASP.NET
Here’s a polymorphic payment processing example:
public interface IPaymentProcessor
{
Task ProcessPaymentAsync(decimal amount);
}
public class CreditCardProcessor : IPaymentProcessor
{
public async Task ProcessPaymentAsync(decimal amount)
{
try
{
// Simulate API call to payment gateway
await Task.Delay(1000);
Console.WriteLine($"Processed credit card payment of {amount:C}");
}
catch (Exception ex)
{
throw new Exception("Credit card payment failed", ex);
}
}
}
public class PayPalProcessor : IPaymentProcessor
{
public async Task ProcessPaymentAsync(decimal amount)
{
try
{
// Simulate API call to PayPal
await Task.Delay(1000);
Console.WriteLine($"Processed PayPal payment of {amount:C}");
}
catch (Exception ex)
{
throw new Exception("PayPal payment failed", ex);
}
}
}
public class PaymentService
{
private readonly IPaymentProcessor _processor;
public PaymentService(IPaymentProcessor processor)
{
_processor = processor;
}
public async Task ProcessOrderPaymentAsync(decimal amount)
{
await _processor.ProcessPaymentAsync(amount);
}
}
2.4.4 Java Example
A Java equivalent:
public interface PaymentProcessor {
void processPayment(double amount) throws Exception;
}
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) throws Exception {
try {
// Simulate API call
Thread.sleep(1000);
System.out.println("Processed credit card payment of $" + amount);
} catch (Exception e) {
throw new Exception("Credit card payment failed", e);
}
}
}
public class PayPalProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) throws Exception {
try {
// Simulate API call
Thread.sleep(1000);
System.out.println("Processed PayPal payment of $" + amount);
} catch (Exception e) {
throw new Exception("PayPal payment failed", e);
}
}
}
public class PaymentService {
private final PaymentProcessor processor;
public PaymentService(PaymentProcessor processor) {
this.processor = processor;
}
public void processOrderPayment(double amount) throws Exception {
processor.processPayment(amount);
}
}
2.4.5 Best Practices, Pros, Cons, and Alternatives
Best Practices:
Use interfaces to define polymorphic behavior.
Ensure polymorphic classes follow the Liskov Substitution Principle.
Handle exceptions specific to each implementation.
Pros:
Enhances flexibility and extensibility.
Simplifies adding new behaviors.
Cons:
Can increase complexity if overused.
Requires careful design to avoid type-checking.
Alternatives:
Strategy pattern for runtime behavior switching.
Functional programming with lambda expressions.
3. Interfaces and Abstract Classes
3.1 Understanding Interfaces
3.1.1 Real-World Example: Plugin Architecture
In a content management system, plugins (e.g., analytics, SEO) can implement a common interface to integrate seamlessly.
3.1.2 C# Example with ASP.NET
Here’s an interface for plugins:
public interface IPlugin
{
Task ExecuteAsync();
}
public class AnalyticsPlugin : IPlugin
{
public async Task ExecuteAsync()
{
try
{
// Simulate analytics tracking
await Task.Delay(500);
Console.WriteLine("Analytics plugin executed");
}
catch (Exception ex)
{
throw new Exception("Analytics plugin failed", ex);
}
}
}
public class SeoPlugin : IPlugin
{
public async Task ExecuteAsync()
{
try
{
// Simulate SEO optimization
await Task.Delay(500);
Console.WriteLine("SEO plugin executed");
}
catch (Exception ex)
{
throw new Exception("SEO plugin failed", ex);
}
}
}
3.1.3 Java Example
A Java equivalent:
public interface Plugin {
void execute() throws Exception;
}
public class AnalyticsPlugin implements Plugin {
@Override
public void execute() throws Exception {
try {
Thread.sleep(500);
System.out.println("Analytics plugin executed");
} catch (Exception e) {
throw new Exception("Analytics plugin failed", e);
}
}
}
public class SeoPlugin implements Plugin {
@Override
public void execute() throws Exception {
try {
Thread.sleep(500);
System.out.println("SEO plugin executed");
} catch (Exception e) {
throw new Exception("SEO plugin failed", e);
}
}
}
3.2 Understanding Abstract Classes
3.2.1 Real-World Example: Vehicle Management System
In a vehicle management system, a base Vehicle class can define shared behavior, while subclasses like Car and Truck provide specific implementations.
3.2.2 C# Example with ASP.NET
Here’s an abstract class example:
public abstract class Vehicle
{
public string LicensePlate { get; set; }
public abstract decimal CalculateRentalCost(int days);
public async Task SaveToDatabaseAsync(SqlConnection connection)
{
try
{
await connection.OpenAsync();
using var command = new SqlCommand(
"INSERT INTO Vehicles (LicensePlate) VALUES (@LicensePlate)",
connection);
command.Parameters.AddWithValue("@LicensePlate", LicensePlate);
await command.ExecuteNonQueryAsync();
}
catch (SqlException ex)
{
throw new Exception("Failed to save vehicle", ex);
}
finally
{
await connection.CloseAsync();
}
}
}
public class Car : Vehicle
{
public override decimal CalculateRentalCost(int days)
{
return days * 50.0m; // $50 per day
}
}
3.2.3 Java Example
A Java equivalent:
public abstract class Vehicle {
protected String licensePlate;
public String getLicensePlate() { return licensePlate; }
public void setLicensePlate(String licensePlate) { this.licensePlate = licensePlate; }
public abstract double calculateRentalCost(int days);
public void saveToDatabase(Connection connection) throws SQLException {
try {
PreparedStatement stmt = connection.prepareStatement(
"INSERT INTO Vehicles (license_plate) VALUES (?)"
);
stmt.setString(1, licensePlate);
stmt.executeUpdate();
} catch (SQLException e) {
throw new SQLException("Failed to save vehicle", e);
}
}
}
public class Car extends Vehicle {
@Override
public double calculateRentalCost(int days) {
return days * 50.0; // $50 per day
}
}
3.3 Interfaces vs. Abstract Classes
3.3.1 Key Differences
Interfaces: Define contracts without implementation; support multiple inheritance.
Abstract Classes: Provide partial implementation; support single inheritance.
C# Specific: Interfaces can include default implementations in C# 8+.
Java Specific: Interfaces support default methods since Java 8.
3.3.2 When to Use Each
Use interfaces for loose coupling and multiple inheritance.
Use abstract classes for shared implementation and state.
4. Coupling and Cohesion
4.1 Defining Coupling
4.1.1 Types of Coupling
Tight Coupling: Classes are highly dependent, making changes difficult.
Loose Coupling: Classes interact via interfaces, reducing dependencies.
4.1.2 Real-World Example: Order Processing System
In an order processing system, tight coupling occurs if the order service directly depends on a specific payment processor. Loose coupling uses interfaces.
4.1.3 C# Example with ASP.NET and SQL Server
Here’s a loosely coupled order service:
public interface IPaymentService
{
Task ProcessPaymentAsync(decimal amount);
}
public class OrderService
{
private readonly IPaymentService _paymentService;
private readonly SqlConnection _connection;
public OrderService(IPaymentService paymentService, string connectionString)
{
_paymentService = paymentService;
_connection = new SqlConnection(connectionString);
}
public async Task CreateOrderAsync(int productId, decimal amount)
{
try
{
await _connection.OpenAsync();
using var command = new SqlCommand(
"INSERT INTO Orders (ProductId, Amount) VALUES (@ProductId, @Amount)",
_connection);
command.Parameters.AddWithValue("@ProductId", productId);
command.Parameters.AddWithValue("@Amount", amount);
await command.ExecuteNonQueryAsync();
await _paymentService.ProcessPaymentAsync(amount);
}
catch (SqlException ex)
{
throw new Exception("Failed to create order", ex);
}
finally
{
await _connection.CloseAsync();
}
}
}
4.1.4 Java Example
A Java equivalent:
public interface PaymentService {
void processPayment(double amount) throws Exception;
}
public class OrderService {
private final PaymentService paymentService;
private final Connection connection;
public OrderService(PaymentService paymentService, String url, String user, String password) throws SQLException {
this.paymentService = paymentService;
this.connection = DriverManager.getConnection(url, user, password);
}
public void createOrder(int productId, double amount) throws Exception {
try {
PreparedStatement stmt = connection.prepareStatement(
"INSERT INTO Orders (product_id, amount) VALUES (?, ?)"
);
stmt.setInt(1, productId);
stmt.setDouble(2, amount);
stmt.executeUpdate();
paymentService.processPayment(amount);
} catch (SQLException e) {
throw new Exception("Failed to create order", e);
} finally {
connection.close();
}
}
}
4.2 Defining Cohesion
4.2.1 Types of Cohesion
High Cohesion: A class has a single, well-defined responsibility.
Low Cohesion: A class handles multiple unrelated tasks.
4.2.2 Real-World Example: Customer Management
A CustomerService class should focus solely on customer-related operations, not payment processing.
4.2.3 C# Example with ASP.NET
Here’s a highly cohesive customer service:
public class CustomerService
{
private readonly SqlConnection _connection;
public CustomerService(string connectionString)
{
_connection = new SqlConnection(connectionString);
}
public async Task AddCustomerAsync(string name, string email)
{
try
{
await _connection.OpenAsync();
using var command = new SqlCommand(
"INSERT INTO Customers (Name, Email) VALUES (@Name, @Email)",
_connection);
command.Parameters.AddWithValue("@Name", name);
command.Parameters.AddWithValue("@Email", email);
await command.ExecuteNonQueryAsync();
}
catch (SqlException ex)
{
throw new Exception("Failed to add customer", ex);
}
finally
{
await _connection.CloseAsync();
}
}
}
4.2.4 Java Example
A Java equivalent:
public class CustomerService {
private final Connection connection;
public CustomerService(String url, String user, String password) throws SQLException {
this.connection = DriverManager.getConnection(url, user, password);
}
public void addCustomer(String name, String email) throws Exception {
try {
PreparedStatement stmt = connection.prepareStatement(
"INSERT INTO Customers (name, email) VALUES (?, ?)"
);
stmt.setString(1, name);
stmt.setString(2, email);
stmt.executeUpdate();
} catch (SQLException e) {
throw new Exception("Failed to add customer", e);
} finally {
connection.close();
}
}
}
4.3 Best Practices for Low Coupling and High Cohesion
Use interfaces to reduce coupling.
Ensure each class has a single responsibility (Single Responsibility Principle).
Avoid god classes that handle multiple tasks.
5. Dependency Inversion and Inversion of Control (IoC)
5.1 Dependency Inversion Principle (DIP)
5.1.1 Definition and Importance
DIP states that high-level modules should not depend on low-level modules; both should depend on abstractions. This promotes flexibility and testability.
5.1.2 Real-World Example: Notification System
In a notification system, a service can send emails or SMS without depending on specific implementations.
5.1.3 C# Example with ASP.NET Core DI
Here’s a DIP example using ASP.NET Core’s built-in DI:
public interface INotificationService
{
Task SendNotificationAsync(string message);
}
public class EmailNotificationService : INotificationService
{
public async Task SendNotificationAsync(string message)
{
try
{
// Simulate email sending
await Task.Delay(500);
Console.WriteLine($"Email sent: {message}");
}
catch (Exception ex)
{
throw new Exception("Email notification failed", ex);
}
}
}
public class OrderController : ControllerBase
{
private readonly INotificationService _notificationService;
public OrderController(INotificationService notificationService)
{
_notificationService = notificationService;
}
[HttpPost]
public async Task<IActionResult> CreateOrder()
{
try
{
await _notificationService.SendNotificationAsync("Order created");
return Ok();
}
catch (Exception ex)
{
return StatusCode(500, $"Error: {ex.Message}");
}
}
}
// Startup.cs or Program.cs
builder.Services.AddScoped<INotificationService, EmailNotificationService>();
5.1.4 Java Example with Spring IoC
A Java equivalent using Spring:
public interface NotificationService {
void sendNotification(String message) throws Exception;
}
@Component
public class EmailNotificationService implements NotificationService {
@Override
public void sendNotification(String message) throws Exception {
try {
Thread.sleep(500);
System.out.println("Email sent: " + message);
} catch (Exception e) {
throw new Exception("Email notification failed", e);
}
}
}
@RestController
public class OrderController {
private final NotificationService notificationService;
@Autowired
public OrderController(NotificationService notificationService) {
this.notificationService = notificationService;
}
@PostMapping("/order")
public ResponseEntity<String> createOrder() {
try {
notificationService.sendNotification("Order created");
return ResponseEntity.ok("Success");
} catch (Exception e) {
return ResponseEntity.status(500).body("Error: " + e.getMessage());
}
}
}
5.2 Inversion of Control (IoC)
5.2.1 Definition and Importance
IoC delegates the control of object creation and dependency management to a container, reducing tight coupling.
5.2.2 Real-World Example: Service Orchestration
In a microservices architecture, an IoC container manages service dependencies, allowing seamless integration.
5.2.3 C# Example with ASP.NET Core
Using ASP.NET Core’s DI container:
public interface IOrderService
{
Task CreateOrderAsync();
}
public class OrderService : IOrderService
{
private readonly INotificationService _notificationService;
public OrderService(INotificationService notificationService)
{
_notificationService = notificationService;
}
public async Task CreateOrderAsync()
{
try
{
// Simulate order creation
await _notificationService.SendNotificationAsync("Order created");
}
catch (Exception ex)
{
throw new Exception("Order creation failed", ex);
}
}
}
// Program.cs
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<INotificationService, EmailNotificationService>();
5.2.4 Java Example with Spring
A Spring equivalent:
public interface OrderService {
void createOrder() throws Exception;
}
@Service
public class OrderServiceImpl implements OrderService {
private final NotificationService notificationService;
@Autowired
public OrderServiceImpl(NotificationService notificationService) {
this.notificationService = notificationService;
}
@Override
public void createOrder() throws Exception {
try {
notificationService.sendNotification("Order created");
} catch (Exception e) {
throw new Exception("Order creation failed", e);
}
}
}
5.3 Best Practices, Pros, Cons, and Alternatives
Best Practices:
Use DI frameworks like ASP.NET Core DI or Spring.
Inject dependencies via constructors.
Avoid service locators, which hide dependencies.
Pros:
Enhances testability and flexibility.
Simplifies dependency management.
Cons:
Increases complexity for small projects.
Requires learning DI frameworks.
Alternatives:
Manual dependency injection for small applications.
Factory pattern for custom object creation.
6. Composition Over Inheritance
6.1 Why Composition Over Inheritance?
6.1.1 Real-World Example: UI Component System
In a UI framework, components like buttons can use composition to include behaviors (e.g., click handling) instead of inheriting from a base class.
6.1.2 C# Example with ASP.NET
Here’s a composition-based button component:
public interface IClickHandler
{
Task HandleClickAsync();
}
public class LoggingClickHandler : IClickHandler
{
public async Task HandleClickAsync()
{
try
{
await Task.Delay(100); // Simulate logging
Console.WriteLine("Click logged");
}
catch (Exception ex)
{
throw new Exception("Click logging failed", ex);
}
}
}
public class Button
{
private readonly IClickHandler _clickHandler;
public Button(IClickHandler clickHandler)
{
_clickHandler = clickHandler;
}
public async Task OnClickAsync()
{
await _clickHandler.HandleClickAsync();
}
}
6.1.3 Java Example
A Java equivalent:
public interface ClickHandler {
void handleClick() throws Exception;
}
public class LoggingClickHandler implements ClickHandler {
@Override
public void handleClick() throws Exception {
try {
Thread.sleep(100);
System.out.println("Click logged");
} catch (Exception e) {
throw new Exception("Click logging failed", e);
}
}
}
public class Button {
private final ClickHandler clickHandler;
public Button(ClickHandler clickHandler) {
this.clickHandler = clickHandler;
}
public void onClick() throws Exception {
clickHandler.handleClick();
}
}
6.2 Best Practices, Pros, Cons, and Alternatives
Best Practices:
Use composition for flexible behavior addition.
Combine with interfaces for loose coupling.
Avoid deep inheritance hierarchies.
Pros:
Increases flexibility and maintainability.
Reduces coupling compared to inheritance.
Cons:
Can increase code complexity.
Requires careful design to avoid boilerplate.
Alternatives:
Inheritance for clear “is-a” relationships.
Decorator pattern for dynamic behavior addition.
7. Modern OOP Features in .NET 7+ and Java 21+
7.1 .NET 7+ Features
7.1.1 Record Types
Record types provide immutable data structures with value-based equality.
public record Product(int Id, string Name, decimal Price);
7.1.2 Pattern Matching Enhancements
Pattern matching simplifies type checking and property access.
public async Task ProcessProductAsync(object item)
{
if (item is Product { Price: > 100 } product)
{
Console.WriteLine($"Expensive product: {product.Name}");
}
}
7.1.3 C# Example with ASP.NET
Using records and pattern matching in an ASP.NET Core controller:
public record Product(int Id, string Name, decimal Price);
[ApiController]
[Route("api/products")]
public class ProductController : ControllerBase
{
private readonly SqlConnection _connection;
public ProductController(string connectionString)
{
_connection = new SqlConnection(connectionString);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetProductAsync(int id)
{
try
{
await _connection.OpenAsync();
using var command = new SqlCommand("SELECT * FROM Products WHERE Id = @Id", _connection);
command.Parameters.AddWithValue("@Id", id);
using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
var product = new Product(
reader.GetInt32("Id"),
reader.GetString("Name"),
reader.GetDecimal("Price")
);
return product switch
{
{ Price: > 100 } => Ok(new { product, Category = "Premium" }),
_ => Ok(new { product, Category = "Standard" })
};
}
return NotFound();
}
catch (SqlException ex)
{
return StatusCode(500, $"Database error: {ex.Message}");
}
finally
{
await _connection.CloseAsync();
}
}
}
7.2 Java 21+ Features
7.2.1 Record Classes
Java records provide concise syntax for immutable data classes.
public record Product(int id, String name, double price) {}
7.2.2 Sealed Classes
Sealed classes restrict which classes can extend or implement them.
public sealed interface PaymentProcessor permits CreditCardProcessor, PayPalProcessor {
void processPayment(double amount) throws Exception;
}
public final class CreditCardProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) throws Exception {
System.out.println("Processed credit card payment of $" + amount);
}
}
public final class PayPalProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) throws Exception {
System.out.println("Processed PayPal payment of $" + amount);
}
}
7.2.3 Java Example
Using records and sealed classes:
public record Product(int id, String name, double price) {}
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final Connection connection;
public ProductController(String url, String user, String password) throws SQLException {
this.connection = DriverManager.getConnection(url, user, password);
}
@GetMapping("/{id}")
public ResponseEntity<?> getProduct(@PathVariable int id) {
try {
PreparedStatement stmt = connection.prepareStatement("SELECT * FROM Products WHERE id = ?");
stmt.setInt(1, id);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
Product product = new Product(
rs.getInt("id"),
rs.getString("name"),
rs.getDouble("price")
);
String category = product.price() > 100 ? "Premium" : "Standard";
return ResponseEntity.ok(Map.of("product", product, "category", category));
}
return ResponseEntity.notFound().build();
} catch (SQLException e) {
return ResponseEntity.status(500).body("Database error: " + e.getMessage());
} finally {
try {
connection.close();
} catch (SQLException e) {
// Log error
}
}
}
}
7.3 Integrating Modern Features with OOP Principles
Use records for immutable data models (e.g., DTOs).
Leverage pattern matching and sealed classes for type-safe polymorphic behavior.
Combine with DI for flexible, testable systems.
8. Exception Handling in OOP
8.1 Importance of Exception Handling
Exception handling ensures robust applications by gracefully handling errors, preventing crashes, and providing meaningful feedback.
8.2 C# Exception Handling with ASP.NET and SQL Server
Here’s an example with comprehensive exception handling:
public class OrderService
{
private readonly SqlConnection _connection;
public OrderService(string connectionString)
{
_connection = new SqlConnection(connectionString);
}
public async Task CreateOrderAsync(int productId, decimal amount)
{
try
{
if (productId <= 0 || amount <= 0)
throw new ArgumentException("Invalid product ID or amount");
await _connection.OpenAsync();
using var command = new SqlCommand(
"INSERT INTO Orders (ProductId, Amount) VALUES (@ProductId, @Amount)",
_connection);
command.Parameters.AddWithValue("@ProductId", productId);
command.Parameters.AddWithValue("@Amount", amount);
await command.ExecuteNonQueryAsync();
}
catch (ArgumentException ex)
{
throw new InvalidOperationException("Invalid input provided", ex);
}
catch (SqlException ex)
{
throw new Exception("Database error occurred while creating order", ex);
}
catch (Exception ex)
{
throw new Exception("Unexpected error occurred", ex);
}
finally
{
await _connection.CloseAsync();
}
}
}
8.3 Java Exception Handling
A Java equivalent:
public class OrderService {
private final Connection connection;
public OrderService(String url, String user, String password) throws SQLException {
this.connection = DriverManager.getConnection(url, user, password);
}
public void createOrder(int productId, double amount) throws Exception {
try {
if (productId <= 0 || amount <= 0) {
throw new IllegalArgumentException("Invalid product ID or amount");
}
PreparedStatement stmt = connection.prepareStatement(
"INSERT INTO Orders (product_id, amount) VALUES (?, ?)"
);
stmt.setInt(1, productId);
stmt.setDouble(2, amount);
stmt.executeUpdate();
} catch (IllegalArgumentException e) {
throw new Exception("Invalid input provided", e);
} catch (SQLException e) {
throw new Exception("Database error occurred while creating order", e);
} catch (Exception e) {
throw new Exception("Unexpected error occurred", e);
} finally {
connection.close();
}
}
}
8.4 Best Practices for Exception Handling
Catch specific exceptions before general ones.
Provide meaningful error messages.
Use finally blocks or try-with-resources to clean up resources.
Log exceptions for debugging.
🚀 Expand Your Learning Journey
📘 Master Software Design Patterns: Complete Course Outline (.NET & Java) | 🎯 Free Learning Zone
📘 Master Software Design Patterns: Complete Course Outline (.NET & Java) 🎯 Visit Free Learning Zone
No comments:
Post a Comment
Thanks for your valuable comment...........
Md. Mominul Islam