Object Oriented Programming for LLD - IndianTechnoEra
Latest update Android YouTube

Object Oriented Programming for LLD

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:

  1. Refactor a messy class from your current project to follow proper encapsulation principles
  2. Create a class hierarchy for a parking lot system (vehicles, parking spots, tickets)
  3. Implement a simple e-commerce system using composition over inheritance
  4. Write unit tests to verify the invariants in your encapsulated classes

Happy Coding! 🚀

Post a Comment

Feel free to ask your query...
Cookie Consent
We serve cookies on this site to analyze traffic, remember your preferences, and optimize your experience.
Oops!
It seems there is something wrong with your internet connection. Please connect to the internet and start browsing again.
AdBlock Detected!
We have detected that you are using adblocking plugin in your browser.
The revenue we earn by the advertisements is used to manage this website, we request you to whitelist our website in your adblocking plugin.
Site is Blocked
Sorry! This site is not available in your country.