Introduction
Welcome to Module 5 of our Complete C# Course: From Beginner to Advanced! After mastering core OOP principles in Module 4, it’s time to explore advanced OOP concepts that enable you to write flexible, maintainable, and scalable C# code. In this module, we’ll cover sealed classes and methods, partial classes and methods, generics, namespaces and assemblies, access modifiers, extension methods, indexers, and C# 9+ records. We’ll apply these concepts in a practical inventory management system for a retail store, inspired by real-world applications like stock tracking or e-commerce platforms. With detailed examples, best practices, and pros/cons, you’ll learn to build professional-grade C# applications. Let’s dive in!
1. Sealed Classes and Methods
Sealed classes prevent inheritance, while sealed methods prevent overriding in derived classes.
Example: Sealed Product Class
using System;
namespace InventorySystem
{
sealed class Product
{
public string Name { get; set; }
public virtual void DisplayInfo()
{
Console.WriteLine($"Product: {Name}");
}
}
// This would cause a compile error
// class SpecialProduct : Product { }
class Program
{
static void Main(string[] args)
{
Product product = new Product { Name = "Laptop" };
product.DisplayInfo(); // Product: Laptop
}
}
}
Real-World Use: Protecting critical classes (e.g., payment processors) from unintended modifications.
Pros:
Prevents unwanted inheritance, ensuring design integrity.
Improves performance by allowing compiler optimizations.
Cons:
Limits extensibility, reducing flexibility.
Cannot be undone without changing the class.
Best Practices:
Use sealed for classes/methods that shouldn’t be extended.
Combine with virtual methods for controlled extensibility.
Alternatives:
Private constructors for similar restrictions.
Composition over inheritance for flexibility.
2. Partial Classes and Methods
Partial classes split a class definition across multiple files, while partial methods allow optional implementation.
Example: Partial Inventory Class
// File: Inventory.cs
using System.Collections.Generic;
namespace InventorySystem
{
partial class Inventory
{
private List<Product> products = new List<Product>();
public void AddProduct(Product product)
{
products.Add(product);
OnProductAdded(); // Calls partial method
}
partial void OnProductAdded(); // Declaration
}
}
// File: Inventory.Logging.cs
namespace InventorySystem
{
partial class Inventory
{
partial void OnProductAdded()
{
Console.WriteLine("Product added to inventory.");
}
}
}
namespace InventorySystem
{
class Product
{
public string Name { get; set; }
}
class Program
{
static void Main(string[] args)
{
Inventory inventory = new Inventory();
inventory.AddProduct(new Product { Name = "Laptop" });
// Output: Product added to inventory.
}
}
}
Real-World Use: Separating generated code (e.g., UI designers) from custom logic in large projects.
Pros:
Simplifies large class management across files.
Partial methods allow optional behavior without overhead.
Cons:
Can make code harder to navigate.
Partial methods are private and void-only, limiting flexibility.
Best Practices:
Use partial classes for large or generated codebases.
Keep partial method implementations optional and lightweight.
Alternatives:
Separate classes with composition.
Extension methods for adding behavior.
3. Generics (Generic Methods, Classes, and Constraints)
Generics enable type-safe, reusable code for different data types.
Example: Generic Inventory Repository
using System;
using System.Collections.Generic;
namespace InventorySystem
{
class InventoryRepository<T> where T : class
{
private List<T> items = new List<T>();
public void Add(T item) => items.Add(item);
public T Find(Predicate<T> predicate) => items.Find(predicate);
}
class Product
{
public string Name { get; set; }
public double Price { get; set; }
}
class Program
{
static void Main(string[] args)
{
InventoryRepository<Product> repository = new InventoryRepository<Product>();
repository.Add(new Product { Name = "Laptop", Price = 999.99 });
Product found = repository.Find(p => p.Name == "Laptop");
Console.WriteLine($"Found: {found?.Name}, {found?.Price:C}");
}
}
}
Real-World Use: Building reusable data access layers or collections for products, users, etc.
Pros:
Type safety without casting.
Reusable code for multiple types.
Cons:
Increases complexity for simple scenarios.
Constraints can limit flexibility.
Best Practices:
Use constraints (e.g., where T : class) to enforce type requirements.
Prefer generics over object for type safety.
Alternatives:
Non-generic collections (e.g., ArrayList, less safe).
Inheritance for specific type hierarchies.
4. Namespaces and Assemblies
Namespaces organize code logically, while assemblies are physical units (DLLs or EXEs) containing compiled code.
Example: Organized Inventory System
// File: Models/Product.cs
namespace InventorySystem.Models
{
public class Product
{
public string Name { get; set; }
}
}
// File: Services/InventoryService.cs
namespace InventorySystem.Services
{
public class InventoryService
{
public void AddProduct(Models.Product product)
{
Console.WriteLine($"Added: {product.Name}");
}
}
}
// File: Program.cs
using System;
using InventorySystem.Models;
using InventorySystem.Services;
namespace InventorySystem
{
class Program
{
static void Main(string[] args)
{
InventoryService service = new InventoryService();
Product product = new Product { Name = "Laptop" };
service.AddProduct(product); // Added: Laptop
}
}
}
Real-World Use: Structuring large projects like e-commerce platforms with separate namespaces for models, services, etc.
Pros:
Namespaces prevent naming conflicts.
Assemblies enable modular deployment and reuse.
Cons:
Overuse of namespaces can complicate navigation.
Assemblies add deployment complexity.
Best Practices:
Use hierarchical namespaces (e.g., Company.Project.Module).
Keep assemblies focused on specific functionality.
Alternatives:
Single namespace for small projects.
Source generators for code organization.
5. Access Modifiers in Depth
Access modifiers control visibility: public, private, protected, internal, protected internal, private protected.
Example: Controlled Access
using System;
namespace InventorySystem
{
class Inventory
{
private List<Product> products = new List<Product>(); // Private
protected int MaxCapacity { get; set; } = 100; // Protected
internal bool IsFull => products.Count >= MaxCapacity; // Internal
public void AddProduct(Product product) // Public
{
if (!IsFull)
products.Add(product);
else
Console.WriteLine("Inventory full!");
}
}
class Product
{
public string Name { get; set; }
}
class Program
{
static void Main(string[] args)
{
Inventory inventory = new Inventory();
inventory.AddProduct(new Product { Name = "Laptop" });
Console.WriteLine($"Is full? {inventory.IsFull}"); // Is full? False
}
}
}
Real-World Use: Protecting sensitive data (e.g., user info) while exposing public APIs.
Pros:
Enhances encapsulation and security.
internal limits access within assemblies.
Cons:
Complex modifiers (e.g., private protected) can confuse developers.
Overuse of public exposes implementation.
Best Practices:
Default to private for fields; expose via properties/methods.
Use internal for assembly-scoped APIs.
Alternatives:
Encapsulation via interfaces.
Sealed classes for restricted access.
6. Extension Methods
Extension methods add functionality to existing types without modifying them.
Example: Product Extensions
using System;
namespace InventorySystem
{
static class ProductExtensions
{
public static string GetFormattedName(this Product product)
{
return $"Product: {product.Name.ToUpper()}";
}
}
class Product
{
public string Name { get; set; }
}
class Program
{
static void Main(string[] args)
{
Product product = new Product { Name = "Laptop" };
Console.WriteLine(product.GetFormattedName()); // Product: LAPTOP
}
}
}
Real-World Use: Adding utility methods to third-party classes or built-in types.
Pros:
Enhances existing types without inheritance.
Improves code readability with fluent APIs.
Cons:
Can clutter namespaces if overused.
Not discoverable via IntelliSense unless imported.
Best Practices:
Place in static classes with clear names (e.g., ProductExtensions).
Use sparingly to avoid confusion.
Alternatives:
Inheritance or composition for custom types.
Helper classes for utility functions.
7. Indexers
Indexers allow objects to be indexed like arrays.
Example: Inventory Indexer
using System;
using System.Collections.Generic;
namespace InventorySystem
{
class Inventory
{
private List<Product> products = new List<Product>();
public Product this[int index]
{
get => products[index];
set => products[index] = value;
}
public void Add(Product product) => products.Add(product);
}
class Product
{
public string Name { get; set; }
}
class Program
{
static void Main(string[] args)
{
Inventory inventory = new Inventory();
inventory.Add(new Product { Name = "Laptop" });
inventory.Add(new Product { Name = "Phone" });
Console.WriteLine(inventory[0].Name); // Laptop
inventory[0] = new Product { Name = "Tablet" };
Console.WriteLine(inventory[0].Name); // Tablet
}
}
}
Real-World Use: Accessing items in collections like shopping carts or playlists.
Pros:
Provides array-like syntax for custom types.
Enhances usability for collections.
Cons:
Can hide complex logic behind simple syntax.
Limited to single-parameter indexing in most cases.
Best Practices:
Use for natural collection-like classes.
Validate indices to prevent exceptions.
Alternatives:
Methods like GetItem or SetItem.
Dictionaries for key-based access.
8. Records (C# 9+)
Records provide immutable data types with value-based equality.
Example: Immutable Product Record
using System;
namespace InventorySystem
{
public record Product(string Name, double Price);
class Program
{
static void Main(string[] args)
{
Product p1 = new Product("Laptop", 999.99);
Product p2 = new Product("Laptop", 999.99);
Console.WriteLine($"Equal? {p1 == p2}"); // True
Console.WriteLine($"Details: {p1}"); // Product { Name = Laptop, Price = 999.99 }
}
}
}
Real-World Use: Representing immutable data like order details or configuration.
Pros:
Simplifies immutable data with concise syntax.
Automatic equality and ToString implementation.
Cons:
Less flexible than classes for mutable state.
C# 9+ requirement limits compatibility.
Best Practices:
Use for immutable data or DTOs.
Combine with with expressions for non-destructive updates.
Alternatives:
Classes with manual equality implementation.
Structs for lightweight immutable data.
Interactive Example: Inventory Management System
Let’s build a console-based inventory management system to apply advanced OOP concepts.
using System;
using System.Collections.Generic;
// File: Models/Product.cs
namespace InventorySystem.Models
{
public record Product(string Name, double Price);
}
// File: Services/Inventory.cs
namespace InventorySystem.Services
{
partial class Inventory<T> where T : class
{
private List<T> items = new List<T>();
public static int TotalItems { get; private set; }
public T this[int index]
{
get => items[index];
set => items[index] = value;
}
public void Add(T item)
{
items.Add(item);
TotalItems++;
OnItemAdded();
}
partial void OnItemAdded();
}
}
// File: Services/Inventory.Logging.cs
namespace InventorySystem.Services
{
partial class Inventory<T>
{
partial void OnItemAdded()
{
Console.WriteLine("Item added to inventory.");
}
}
}
// File: Extensions/ProductExtensions.cs
namespace InventorySystem.Extensions
{
static class ProductExtensions
{
public static string GetFormattedDetails(this Models.Product product)
{
return $"Product: {product.Name}, Price: {product.Price:C}";
}
}
}
// File: Program.cs
using System;
using InventorySystem.Models;
using InventorySystem.Services;
using InventorySystem.Extensions;
namespace InventorySystem
{
class Program
{
static void Main(string[] args)
{
Inventory<Product> inventory = new Inventory<Product>();
while (true)
{
Console.WriteLine("\nInventory Management System");
Console.WriteLine("1. Add Product");
Console.WriteLine("2. View Products");
Console.WriteLine("3. Update Product");
Console.WriteLine("4. Exit");
Console.Write("Choose an option: ");
string choice = Console.ReadLine();
if (choice == "4") break;
switch (choice)
{
case "1":
Console.Write("Enter product name: ");
string name = Console.ReadLine();
Console.Write("Enter price: ");
if (!double.TryParse(Console.ReadLine(), out double price) || price < 0)
{
Console.WriteLine("Invalid price!");
continue;
}
inventory.Add(new Product(name, price));
break;
case "2":
for (int i = 0; i < inventory.TotalItems; i++)
{
Console.WriteLine($"[{i}] {inventory[i].GetFormattedDetails()}");
}
if (inventory.TotalItems == 0)
Console.WriteLine("No products in inventory.");
break;
case "3":
Console.Write("Enter index to update: ");
if (!int.TryParse(Console.ReadLine(), out int index) || index < 0 || index >= inventory.TotalItems)
{
Console.WriteLine("Invalid index!");
continue;
}
Console.Write("Enter new name: ");
string newName = Console.ReadLine();
Console.Write("Enter new price: ");
if (!double.TryParse(Console.ReadLine(), out double newPrice) || newPrice < 0)
{
Console.WriteLine("Invalid price!");
continue;
}
inventory[index] = new Product(newName, newPrice);
Console.WriteLine("Product updated.");
break;
default:
Console.WriteLine("Invalid option!");
break;
}
}
Console.WriteLine($"Total items: {Inventory<Product>.TotalItems}");
}
}
}
How It Works:
Sealed Classes: Not used here to allow flexibility, but could seal Product.
Partial Classes: Inventory split for core logic and logging.
Generics: Inventory<T> supports any type with class constraint.
Namespaces: Organized into Models, Services, Extensions.
Access Modifiers: Private fields, public properties/methods, internal statics.
Extension Methods: GetFormattedDetails enhances Product.
Indexers: Access Inventory items like an array.
Records: Product uses record for immutable data.
Why It’s Useful: Mimics inventory systems in retail or warehouse management.
Setup: Create a new Console App in Visual Studio or run dotnet new console. Organize files as shown.
Best Standards for Module 5
Sealed Classes/Methods: Use for final implementations; avoid over-restricting extensibility.
Partial Classes/Methods: Split large classes or separate generated code; keep files cohesive.
Generics: Use for type-safe, reusable code; apply constraints for clarity.
Namespaces/Assemblies: Follow Company.Project.Module naming; keep assemblies focused.
Access Modifiers: Default to private; use internal for assembly-scoped APIs.
Extension Methods: Add utility methods sparingly; use clear naming.
Indexers: Provide array-like access for collection classes; validate indices.
Records: Use for immutable data; leverage with expressions for updates.
Conclusion
You’ve just mastered advanced OOP concepts in C#! By learning sealed classes, partial classes, generics, namespaces, access modifiers, extension methods, indexers, and records, you’re equipped to build scalable, maintainable applications. The inventory management system demonstrates how these concepts power real-world solutions.
What’s Next? In Module 6, we’ll explore collections, LINQ, and data manipulation for handling complex datasets. Keep practicing, and try adding a product category or search feature to the inventory system!
Interactive Challenge: Enhance the inventory system to support product categories or export inventory to a file. Share your solution with #C#Hero!
No comments:
Post a Comment
Thanks for your valuable comment...........
Md. Mominul Islam