Chapter 1: Object Oriented Programming Deep Dive
Series: Low Level Design for .NET Developers | Level: Intermediate to Advanced
📖 Introduction
Before diving into design patterns and complex architectures, we must build a rock-solid foundation in Object Oriented Programming (OOP). While you've likely used OOP in your daily work, Low Level Design (LLD) interviews and real-world system design require a deeper understanding of not just how to use OOP, but why and when to use each concept.
This chapter will cover:
- The 4 Pillars of OOP with advanced scenarios
- Encapsulation - Protecting object integrity
- Inheritance - IS-A relationships and their pitfalls
- Polymorphism - Runtime vs compile-time
- Abstraction - Interfaces vs Abstract classes
- Composition over Inheritance - The golden rule
- Practical .NET examples and common pitfalls
1. The Four Pillars of OOP - Quick Overview
| Pillar | Purpose | .NET Implementation |
|---|---|---|
| Encapsulation | Hide internal state, expose controlled interface | private fields, public properties/methods |
| Inheritance | Code reuse, IS-A relationships | class Child : Parent |
| Polymorphism | One interface, multiple implementations | virtual/override, interfaces |
| Abstraction | Hide complexity, show essential features | abstract classes, interfaces |
2. Encapsulation - The Art of Hiding
2.1 What is Encapsulation?
Encapsulation is the bundling of data and methods that operate on that data within a single unit (class), while restricting direct access to some components. The primary goal is to protect invariants - the business rules that must always hold true for an object.
2.2 Why Encapsulation Matters
Consider a simple bank account. The balance should never be negative. Without encapsulation, anyone could set it to an invalid state.
❌ The Wrong Way - Exposed State
public class BankAccount
{
public decimal Balance { get; set; } // Public setter - dangerous!
public string AccountNumber { get; set; }
}
// Usage - Breaking business rules
var account = new BankAccount();
account.Balance = -500; // Compiles! But invalid state
account.AccountNumber = null; // Also invalid
This code compiles but violates basic banking rules. The object is in an invalid state and there's no way to prevent it.
✅ The Right Way - Protected State with Invariants
public class BankAccount
{
// Private field - only accessible within this class
private decimal _balance;
private string _accountNumber;
// Read-only property - external code can see, but not modify directly
public decimal Balance
{
get { return _balance; }
}
public string AccountNumber
{
get { return _accountNumber; }
private set { _accountNumber = value; } // Private setter
}
// Constructor - validates initial state
public BankAccount(string accountNumber, decimal initialDeposit)
{
if (string.IsNullOrWhiteSpace(accountNumber))
throw new ArgumentException("Account number cannot be empty");
if (initialDeposit < 0)
throw new ArgumentException("Initial deposit cannot be negative");
_accountNumber = accountNumber;
_balance = initialDeposit;
}
// Controlled methods to modify state
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Deposit amount must be positive");
_balance += amount;
Console.WriteLine($"Deposited {amount:C}. New balance: {_balance:C}");
}
public void Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Withdrawal amount must be positive");
if (amount > _balance)
throw new InvalidOperationException("Insufficient funds");
_balance -= amount;
Console.WriteLine($"Withdrew {amount:C}. New balance: {_balance:C}");
}
}
// Usage
var account = new BankAccount("ACC123", 1000);
account.Deposit(500); // Works: Deposited $500.00
account.Withdraw(200); // Works: Withdrew $200.00
// account.Balance = 9999; // Compiler error - no setter
// account.Withdraw(2000); // Runtime error: Insufficient funds
2.3 Advanced Encapsulation - Using Properties with Validation
public class Customer
{
private string _email;
private int _age;
public string Name { get; set; } // Auto-property for simple cases
public string Email
{
get { return _email; }
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Email cannot be empty");
if (!value.Contains("@"))
throw new ArgumentException("Invalid email format");
_email = value;
}
}
public int Age
{
get { return _age; }
set
{
if (value < 0 || value > 150)
throw new ArgumentException("Age must be between 0 and 150");
_age = value;
}
}
// Read-only calculated property
public bool IsAdult
{
get { return _age >= 18; }
}
}
// Usage
var customer = new Customer();
customer.Name = "John Doe";
customer.Email = "john@example.com"; // Valid email
customer.Age = 25;
Console.WriteLine($"Is adult: {customer.IsAdult}"); // True
// customer.Email = "invalid"; // Throws ArgumentException
// customer.Age = 200; // Throws ArgumentException
💡 Key Insight: Encapsulation is not just about making fields private. It's about protecting invariants - the business rules that must always be true. Always validate state changes at the point of entry.
3. Inheritance - The IS-A Relationship
3.1 Understanding Inheritance
Inheritance allows a class to inherit properties and methods from another class. It represents an IS-A relationship. A Car is a Vehicle. A Dog is an Animal.
3.2 Basic Inheritance Example
// Base class (Parent)
public class Vehicle
{
public string LicensePlate { get; set; }
public string Brand { get; set; }
public int Year { get; set; }
public void StartEngine()
{
Console.WriteLine("Engine starting...");
}
public virtual void Honk()
{
Console.WriteLine("Beep beep!");
}
}
// Derived class (Child) - Car IS-A Vehicle
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public string FuelType { get; set; }
// Override base class method
public override void Honk()
{
Console.WriteLine("Car horn: Honk honk!");
}
// New method specific to Car
public void OpenTrunk()
{
Console.WriteLine("Trunk opened");
}
}
// Derived class - Motorcycle IS-A Vehicle
public class Motorcycle : Vehicle
{
public bool HasSidecar { get; set; }
public override void Honk()
{
Console.WriteLine("Motorcycle horn: Meep meep!");
}
public void Wheelie()
{
Console.WriteLine("Doing a wheelie!");
}
}
// Usage
Car myCar = new Car();
myCar.LicensePlate = "ABC123"; // Inherited from Vehicle
myCar.Brand = "Toyota"; // Inherited from Vehicle
myCar.NumberOfDoors = 4; // Specific to Car
myCar.StartEngine(); // Inherited method
myCar.Honk(); // Overridden method
myCar.OpenTrunk(); // Specific method
Motorcycle myBike = new Motorcycle();
myBike.Honk(); // Output: Motorcycle horn: Meep meep!
3.3 The Problem with Deep Inheritance
While inheritance is powerful, deep inheritance hierarchies become fragile and hard to maintain.
❌ Bad - Deep Inheritance Hierarchy
public class Animal { }
public class Mammal : Animal { }
public class Canine : Mammal { }
public class Dog : Canine { }
public class Labrador : Dog { }
public class GoldenRetriever : Labrador { }
// Too deep! Changes at top affect everything below
3.4 The Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states that objects of a derived class should be able to replace objects of the base class without affecting the correctness of the program.
❌ Violating LSP
public class Bird
{
public virtual void Fly()
{
Console.WriteLine("Flying...");
}
}
public class Penguin : Bird
{
public override void Fly()
{
throw new NotImplementedException("Penguins can't fly!");
}
}
// Client code expecting all Birds to fly will break
public void MakeBirdFly(Bird bird)
{
bird.Fly(); // Throws exception for Penguin!
}
✅ Fix - Proper Abstraction
public abstract class Bird
{
public abstract void Eat();
public abstract void Sleep();
}
public interface IFlyable
{
void Fly();
}
public class Sparrow : Bird, IFlyable
{
public override void Eat() => Console.WriteLine("Sparrow eating");
public override void Sleep() => Console.WriteLine("Sparrow sleeping");
public void Fly() => Console.WriteLine("Sparrow flying");
}
public class Penguin : Bird
{
public override void Eat() => Console.WriteLine("Penguin eating");
public override void Sleep() => Console.WriteLine("Penguin sleeping");
// Penguin doesn't implement IFlyable - no expectation to fly
}
// Usage - Now we know which birds can fly
public void MakeBirdFly(IFlyable flyingBird)
{
flyingBird.Fly(); // Only called with birds that can fly
}
⚠️ Warning: Inheritance is often overused. Before using inheritance, ask: "Is this truly an IS-A relationship? Will this relationship hold forever?" If not, consider composition instead.
4. Polymorphism - Many Forms
Polymorphism allows objects of different types to be treated as objects of a common base type, with each responding to the same method call in its own way.
4.1 Compile-Time Polymorphism (Method Overloading)
public class Calculator
{
// Same method name, different parameters
public int Add(int a, int b)
{
return a + b;
}
public int Add(int a, int b, int c)
{
return a + b + c;
}
public double Add(double a, double b)
{
return a + b;
}
public decimal Add(decimal a, decimal b)
{
return a + b;
}
}
// Usage - Compiler determines which method to call
Calculator calc = new Calculator();
Console.WriteLine(calc.Add(5, 10)); // Calls int version
Console.WriteLine(calc.Add(5, 10, 15)); // Calls three-parameter version
Console.WriteLine(calc.Add(5.5, 10.5)); // Calls double version
4.2 Runtime Polymorphism (Method Overriding)
// Base class
public abstract class Shape
{
public abstract double CalculateArea();
public abstract double CalculatePerimeter();
public virtual void Display()
{
Console.WriteLine("This is a shape");
}
}
// Derived class - Circle
public class Circle : Shape
{
public double Radius { get; set; }
public Circle(double radius)
{
Radius = radius;
}
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
public override double CalculatePerimeter()
{
return 2 * Math.PI * Radius;
}
public override void Display()
{
Console.WriteLine($"Circle with radius {Radius}");
}
}
// Derived class - Rectangle
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double CalculateArea()
{
return Width * Height;
}
public override double CalculatePerimeter()
{
return 2 * (Width + Height);
}
public override void Display()
{
Console.WriteLine($"Rectangle {Width} x {Height}");
}
}
// Usage - Runtime polymorphism in action
List<Shape> shapes = new List<Shape>();
shapes.Add(new Circle(5));
shapes.Add(new Rectangle(4, 6));
shapes.Add(new Circle(3));
foreach (Shape shape in shapes)
{
shape.Display(); // Different output based on actual type
Console.WriteLine($"Area: {shape.CalculateArea():F2}");
Console.WriteLine($"Perimeter: {shape.CalculatePerimeter():F2}");
Console.WriteLine();
}
// Output:
// Circle with radius 5
// Area: 78.54
// Perimeter: 31.42
//
// Rectangle 4 x 6
// Area: 24.00
// Perimeter: 20.00
4.3 Polymorphism with Interfaces
public interface IPaymentProcessor
{
void ProcessPayment(decimal amount);
bool ValidatePayment();
}
public class CreditCardProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing credit card payment of {amount:C}");
}
public bool ValidatePayment()
{
Console.WriteLine("Validating credit card details");
return true;
}
}
public class PayPalProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing PayPal payment of {amount:C}");
}
public bool ValidatePayment()
{
Console.WriteLine("Validating PayPal account");
return true;
}
}
public class CryptoProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing cryptocurrency payment of {amount:C}");
}
public bool ValidatePayment()
{
Console.WriteLine("Validating wallet address");
return true;
}
}
// Checkout service works with any IPaymentProcessor
public class CheckoutService
{
private readonly IPaymentProcessor _paymentProcessor;
public CheckoutService(IPaymentProcessor paymentProcessor)
{
_paymentProcessor = paymentProcessor;
}
public void CompletePurchase(decimal amount)
{
if (_paymentProcessor.ValidatePayment())
{
_paymentProcessor.ProcessPayment(amount);
Console.WriteLine("Purchase completed successfully!");
}
else
{
Console.WriteLine("Payment validation failed");
}
}
}
// Usage
var checkout = new CheckoutService(new CreditCardProcessor());
checkout.CompletePurchase(99.99m);
checkout = new CheckoutService(new PayPalProcessor());
checkout.CompletePurchase(49.99m);
5. Abstraction - Hiding Complexity
Abstraction means hiding complex implementation details and showing only the essential features to the user.
5.1 Abstract Classes vs Interfaces
| Feature | Abstract Class | Interface |
|---|---|---|
| Default Implementation | Can have concrete methods | No implementation (until C# 8.0+ defaults) |
| Fields | Can have fields | Cannot have fields (until C# 8.0+) |
| Constructors | Can have constructors | Cannot have constructors |
| Access Modifiers | All access modifiers | Public by default, limited modifiers |
| Multiple Inheritance | A class can inherit only one abstract class | A class can implement multiple interfaces |
| When to Use | When sharing common code and state | When defining a contract/behavior |
5.2 Practical Example - Abstract Class with Shared State
public abstract class FileHandler
{
// Shared fields
protected string FilePath;
protected FileStream FileStream;
// Constructor - can initialize shared state
protected FileHandler(string filePath)
{
FilePath = filePath;
}
// Abstract methods - must be implemented by derived classes
public abstract void Open();
public abstract void Close();
public abstract string Read();
// Concrete method - shared implementation
public virtual bool Exists()
{
return System.IO.File.Exists(FilePath);
}
// Concrete method with validation
public void ValidateFile()
{
if (!Exists())
{
throw new FileNotFoundException($"File not found: {FilePath}");
}
}
}
public class TextFileHandler : FileHandler
{
public TextFileHandler(string filePath) : base(filePath) { }
public override void Open()
{
FileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
Console.WriteLine($"Opened text file: {FilePath}");
}
public override void Close()
{
FileStream?.Close();
Console.WriteLine($"Closed text file: {FilePath}");
}
public override string Read()
{
using (var reader = new StreamReader(FilePath))
{
return reader.ReadToEnd();
}
}
}
5.3 Interface - Defining Contracts
public interface ILoggable
{
string LogMessage { get; }
LogLevel Severity { get; }
DateTime Timestamp { get; }
}
public interface IPersistable
{
void Save();
void Load(int id);
bool IsPersisted { get; }
}
// A class can implement multiple interfaces
public class Order : ILoggable, IPersistable
{
public int Id { get; set; }
public decimal Amount { get; set; }
public string CustomerName { get; set; }
// ILoggable implementation
public string LogMessage => $"Order {Id} for {CustomerName}: ${Amount}";
public LogLevel Severity => Amount > 1000 ? LogLevel.High : LogLevel.Normal;
public DateTime Timestamp => DateTime.Now;
// IPersistable implementation
public void Save()
{
Console.WriteLine($"Saving order {Id} to database");
}
public void Load(int id)
{
Console.WriteLine($"Loading order {id} from database");
Id = id;
}
public bool IsPersisted => Id > 0;
}
public enum LogLevel
{
Low,
Normal,
High,
Critical
}
💡 Best Practice: Use abstract classes when you need to share implementation details (fields, constructors, concrete methods). Use interfaces when you need to define a contract that multiple unrelated classes can implement.
6. Composition Over Inheritance - The Golden Rule
One of the most important principles in object-oriented design: Favor composition over inheritance. Composition gives you more flexibility and leads to more maintainable code.
6.1 Why Composition is Better
Inheritance creates a tight coupling between parent and child classes. Composition allows you to build complex objects by combining simpler ones, making your code more flexible and easier to change.
6.2 Example: Vehicle with Multiple Behaviors
❌ Inheritance-Based Approach (Rigid)
public class Vehicle
{
public virtual void Start() { }
public virtual void Stop() { }
public virtual void Fly() { throw new NotImplementedException(); }
public virtual void Sail() { throw new NotImplementedException(); }
}
public class Car : Vehicle
{
public override void Start() => Console.WriteLine("Car starting");
public override void Stop() => Console.WriteLine("Car stopping");
// Car doesn't need Fly() or Sail(), but they exist
}
public class AmphibiousCar : Vehicle
{
public override void Start() => Console.WriteLine("Starting engine");
public override void Stop() => Console.WriteLine("Stopping engine");
public override void Sail() => Console.WriteLine("Sailing on water");
// Works, but what about a flying car? Add more methods?
}
// This approach doesn't scale. Each new behavior requires modifying base class
✅ Composition-Based Approach (Flexible)
// Behaviors as separate interfaces
public interface IEngine
{
void Start();
void Stop();
}
public interface IFlyable
{
void TakeOff();
void Land();
void Fly();
}
public interface ISailable
{
void Launch();
void Dock();
void Sail();
}
// Concrete behaviors
public class CarEngine : IEngine
{
public void Start() => Console.WriteLine("Car engine started");
public void Stop() => Console.WriteLine("Car engine stopped");
}
public class JetEngine : IEngine
{
public void Start() => Console.WriteLine("Jet engine ignited");
public void Stop() => Console.WriteLine("Jet engine shut down");
}
public class FlyingBehavior : IFlyable
{
public void TakeOff() => Console.WriteLine("Taking off");
public void Land() => Console.WriteLine("Landing");
public void Fly() => Console.WriteLine("Flying");
}
public class SailingBehavior : ISailable
{
public void Launch() => Console.WriteLine("Launching boat");
public void Dock() => Console.WriteLine("Docking");
public void Sail() => Console.WriteLine("Sailing");
}
// Vehicle composes behaviors
public class Vehicle
{
protected IEngine _engine;
public Vehicle(IEngine engine)
{
_engine = engine;
}
public void Start() => _engine.Start();
public void Stop() => _engine.Stop();
}
// Car composes car-specific behaviors
public class Car : Vehicle
{
public Car() : base(new CarEngine()) { }
public void Drive()
{
Console.WriteLine("Driving on road");
}
}
// Amphibious vehicle composes multiple behaviors
public class AmphibiousVehicle : Vehicle
{
private readonly ISailable _sailingBehavior;
public AmphibiousVehicle() : base(new CarEngine())
{
_sailingBehavior = new SailingBehavior();
}
public void Drive() => Console.WriteLine("Driving on road");
public void Sail() => _sailingBehavior.Sail();
public void Launch() => _sailingBehavior.Launch();
}
// Flying car composes flying behavior
public class FlyingCar : Vehicle
{
private readonly IFlyable _flyingBehavior;
public FlyingCar() : base(new JetEngine())
{
_flyingBehavior = new FlyingBehavior();
}
public void Drive() => Console.WriteLine("Driving on road");
public void Fly() => _flyingBehavior.Fly();
public void TakeOff() => _flyingBehavior.TakeOff();
public void Land() => _flyingBehavior.Land();
}
// Usage - Very flexible!
var flyingCar = new FlyingCar();
flyingCar.Start(); // Jet engine ignited
flyingCar.Drive(); // Driving on road
flyingCar.TakeOff(); // Taking off
flyingCar.Fly(); // Flying
flyingCar.Land(); // Landing
var amphibious = new AmphibiousVehicle();
amphibious.Start(); // Car engine started
amphibious.Drive(); // Driving on road
amphibious.Launch(); // Launching boat
amphibious.Sail(); // Sailing
6.3 The Strategy Pattern - Composition in Action
// This is a preview of the Strategy Pattern (detailed in Chapter 5)
public interface IPricingStrategy
{
decimal CalculatePrice(decimal basePrice);
}
public class RegularPricing : IPricingStrategy
{
public decimal CalculatePrice(decimal basePrice) => basePrice;
}
public class DiscountPricing : IPricingStrategy
{
private readonly decimal _discountPercentage;
public DiscountPricing(decimal discountPercentage)
{
_discountPercentage = discountPercentage;
}
public decimal CalculatePrice(decimal basePrice) => basePrice * (1 - _discountPercentage / 100);
}
public class PremiumPricing : IPricingStrategy
{
public decimal CalculatePrice(decimal basePrice) => basePrice * 1.2m; // 20% premium
}
public class Product
{
public string Name { get; set; }
public decimal BasePrice { get; set; }
private IPricingStrategy _pricingStrategy;
public Product(string name, decimal basePrice, IPricingStrategy pricingStrategy)
{
Name = name;
BasePrice = basePrice;
_pricingStrategy = pricingStrategy;
}
public void SetPricingStrategy(IPricingStrategy strategy)
{
_pricingStrategy = strategy;
}
public decimal GetCurrentPrice()
{
return _pricingStrategy.CalculatePrice(BasePrice);
}
}
// Usage - Can change pricing strategy at runtime
var product = new Product("Laptop", 1000m, new RegularPricing());
Console.WriteLine(product.GetCurrentPrice()); // 1000
product.SetPricingStrategy(new DiscountPricing(15));
Console.WriteLine(product.GetCurrentPrice()); // 850
product.SetPricingStrategy(new PremiumPricing());
Console.WriteLine(product.GetCurrentPrice()); // 1200
7. Common OOP Pitfalls and How to Avoid Them
7.1 The God Object Anti-Pattern
A class that knows too much or does too much.
❌ Bad - God Object
public class ApplicationManager
{
// Handles users
public void CreateUser() { }
public void DeleteUser() { }
public void AuthenticateUser() { }
// Handles products
public void AddProduct() { }
public void UpdateProduct() { }
public void DeleteProduct() { }
// Handles orders
public void CreateOrder() { }
public void ProcessOrder() { }
public void CancelOrder() { }
// Handles payments
public void ProcessPayment() { }
public void RefundPayment() { }
// Handles reporting
public void GenerateReport() { }
public void ExportData() { }
}
✅ Good - Single Responsibility
public class UserService { }
public class ProductService { }
public class OrderService { }
public class PaymentService { }
public class ReportingService { }
7.2 Feature Envy
A method that seems more interested in another class than its own.
❌ Bad - Feature Envy
public class Customer
{
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
}
public class ShippingService
{
// This method uses more data from Address than Customer
public string GetShippingLabel(Customer customer)
{
return $"{customer.Name}\n{customer.Address.Street}\n{customer.Address.City}, {customer.Address.ZipCode}";
}
}
✅ Good - Move Behavior to Data Class
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; }
public override string ToString()
{
return $"{Street}\n{City}, {ZipCode}";
}
}
public class Customer
{
public string Name { get; set; }
public Address Address { get; set; }
public string GetShippingLabel()
{
return $"{Name}\n{Address}";
}
}
7.3 Inappropriate Intimacy
Classes that know too much about each other's private details.
❌ Bad - Too Much Intimacy
public class Order
{
private List<OrderItem> _items = new List<OrderItem>();
private decimal _discount;
public void AddItem(OrderItem item) => _items.Add(item);
// Exposing internal collection directly
public List<OrderItem> Items => _items; // Danger! External code can modify
public decimal Discount => _discount;
}
// External code can manipulate internal state
var order = new Order();
order.Items.Add(new OrderItem()); // Bypasses AddItem validation
order.Discount = 50; // Can't modify because no setter? Actually can't, but Items exposes too much
✅ Good - Controlled Access
public class Order
{
private readonly List<OrderItem> _items = new List<OrderItem>();
private decimal _discount;
public void AddItem(OrderItem item)
{
// Validation logic
if (item.Quantity <= 0)
throw new ArgumentException("Quantity must be positive");
_items.Add(item);
RecalculateTotal();
}
public void RemoveItem(int itemId)
{
var item = _items.FirstOrDefault(i => i.Id == itemId);
if (item != null)
{
_items.Remove(item);
RecalculateTotal();
}
}
// Expose read-only collection
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
public void ApplyDiscount(decimal percentage)
{
if (percentage < 0 || percentage > 100)
throw new ArgumentException("Discount must be between 0 and 100");
_discount = percentage;
RecalculateTotal();
}
public decimal Total { get; private set; }
private void RecalculateTotal()
{
var subtotal = _items.Sum(i => i.Price * i.Quantity);
Total = subtotal * (1 - _discount / 100);
}
}
8. Practice Exercises
Test your understanding with these exercises. Try to implement them before looking at the solutions.
Exercise 1: Library System
Design classes for a library system with Books, Members, and Librarians. Focus on encapsulation and proper access modifiers.
Click to see solution
public class Book
{
public string ISBN { get; }
public string Title { get; }
public string Author { get; }
public bool IsAvailable { get; private set; }
public DateTime? DueDate { get; private set; }
public Member BorrowedBy { get; private set; }
public Book(string isbn, string title, string author)
{
ISBN = isbn;
Title = title;
Author = author;
IsAvailable = true;
}
public void Borrow(Member member, DateTime dueDate)
{
if (!IsAvailable)
throw new InvalidOperationException("Book is already borrowed");
IsAvailable = false;
BorrowedBy = member;
DueDate = dueDate;
}
public void Return()
{
IsAvailable = true;
BorrowedBy = null;
DueDate = null;
}
}
public class Member
{
public int Id { get; }
public string Name { get; }
public string Email { get; private set; }
private List<Book> _borrowedBooks = new List<Book>();
public IReadOnlyCollection<Book> BorrowedBooks => _borrowedBooks.AsReadOnly();
public Member(int id, string name, string email)
{
Id = id;
Name = name;
SetEmail(email);
}
public void SetEmail(string email)
{
if (string.IsNullOrWhiteSpace(email) || !email.Contains("@"))
throw new ArgumentException("Invalid email");
Email = email;
}
public void BorrowBook(Book book, DateTime dueDate)
{
if (_borrowedBooks.Count >= 5)
throw new InvalidOperationException("Maximum 5 books per member");
book.Borrow(this, dueDate);
_borrowedBooks.Add(book);
}
public void ReturnBook(Book book)
{
if (!_borrowedBooks.Contains(book))
throw new InvalidOperationException("Book not borrowed by this member");
book.Return();
_borrowedBooks.Remove(book);
}
}
Exercise 2: Shape Hierarchy with Area Calculation
Create a shape hierarchy using inheritance and polymorphism. Include Circle, Rectangle, Triangle, and Square.
Click to see solution
public abstract class Shape
{
public abstract double CalculateArea();
public abstract double CalculatePerimeter();
public abstract string GetShapeType();
}
public class Circle : Shape
{
public double Radius { get; }
public Circle(double radius)
{
if (radius <= 0)
throw new ArgumentException("Radius must be positive");
Radius = radius;
}
public override double CalculateArea() => Math.PI * Radius * Radius;
public override double CalculatePerimeter() => 2 * Math.PI * Radius;
public override string GetShapeType() => "Circle";
}
public class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double width, double height)
{
if (width <= 0 || height <= 0)
throw new ArgumentException("Dimensions must be positive");
Width = width;
Height = height;
}
public override double CalculateArea() => Width * Height;
public override double CalculatePerimeter() => 2 * (Width + Height);
public override string GetShapeType() => "Rectangle";
}
public class Square : Rectangle
{
public Square(double side) : base(side, side) { }
public override string GetShapeType() => "Square";
}
public class Triangle : Shape
{
public double SideA { get; }
public double SideB { get; }
public double SideC { get; }
public Triangle(double sideA, double sideB, double sideC)
{
if (sideA <= 0 || sideB <= 0 || sideC <= 0)
throw new ArgumentException("Sides must be positive");
if (sideA + sideB <= sideC || sideA + sideC <= sideB || sideB + sideC <= sideA)
throw new ArgumentException("Invalid triangle sides");
SideA = sideA;
SideB = sideB;
SideC = sideC;
}
public override double CalculateArea()
{
// Heron's formula
double s = (SideA + SideB + SideC) / 2;
return Math.Sqrt(s * (s - SideA) * (s - SideB) * (s - SideC));
}
public override double CalculatePerimeter() => SideA + SideB + SideC;
public override string GetShapeType() => "Triangle";
}
9. Summary and Key Takeaways
- Encapsulation protects object invariants. Always validate state changes at the point of entry.
- Inheritance represents IS-A relationships. Use it sparingly and avoid deep hierarchies.
- Polymorphism enables flexible, extensible code. Use interfaces and abstract classes to define contracts.
- Abstraction hides complexity. Use abstract classes for shared state and implementation, interfaces for contracts.
- Composition over Inheritance leads to more flexible, maintainable code.
- Always follow the Single Responsibility Principle - each class should have one reason to change.
- Watch out for common anti-patterns: God Objects, Feature Envy, and Inappropriate Intimacy.
10. What's Next?
In Chapter 2, we'll dive deep into the SOLID Principles - the foundation of maintainable and scalable software design. We'll explore each principle with real-world .NET examples and learn how to apply them to create clean, robust code.
Chapter 2 Preview:
- Single Responsibility Principle (SRP) - Why your classes should do one thing
- Open/Closed Principle (OCP) - Open for extension, closed for modification
- Liskov Substitution Principle (LSP) - Subtypes must be substitutable
- Interface Segregation Principle (ISP) - Don't force clients to depend on what they don't use
- Dependency Inversion Principle (DIP) - Depend on abstractions, not concretions
📝 Practice Assignments:
- Refactor a messy class from your current project to follow proper encapsulation principles
- Create a class hierarchy for a parking lot system (vehicles, parking spots, tickets)
- Implement a simple e-commerce system using composition over inheritance
- Write unit tests to verify the invariants in your encapsulated classes
Happy Coding! 🚀