Section 7 of 10

Object-Oriented Programming: Classes and Objects

🎯 What You'll Learn

  • What object-oriented programming (OOP) is and why it matters
  • The difference between classes and objects
  • How to define classes with fields, properties, and methods
  • Constructors and object initialization
  • Access modifiers (public, private, protected, internal)
  • Static members vs instance members
  • The four pillars of OOP: Encapsulation, Inheritance, Polymorphism, Abstraction
  • Properties vs fields and why properties are preferred
  • Building real InvenTrack classes for our mini-ERP system

What is Object-Oriented Programming?

So far, we've been writing procedural code—variables and methods that operate on data. This works for small programs, but as applications grow, you need a better way to organize complexity. That's where Object-Oriented Programming (OOP) comes in.

💡 Key Concept

Object-Oriented Programming is a programming paradigm that organizes code around objects—bundles of data (properties) and behavior (methods) that represent real-world entities. Instead of scattered variables and functions, you group related data and operations into cohesive units called classes.

Think of a blueprint for a house. The blueprint (class) defines what a house should have: rooms, doors, windows, a roof. But the blueprint itself isn't a house—it's just the design. When you build an actual house from that blueprint, you create an object (an instance). You can build many houses from the same blueprint, each with its own specific characteristics (3 bedrooms vs 4, blue paint vs white), but all following the same design.

Why Use OOP?

  • Organization: Group related data and behavior together
  • Reusability: Create templates (classes) and reuse them throughout your application
  • Maintainability: Changes to a class affect all objects created from it
  • Abstraction: Hide complexity behind simple interfaces
  • Real-world modeling: Code mirrors the problem domain (customers, products, orders)

Classes and Objects: The Fundamentals

What is a Class?

A class is a blueprint or template that defines the structure and behavior of objects. It specifies what data an object will hold (fields/properties) and what operations it can perform (methods).

What is an Object?

An object is a specific instance of a class—an actual entity created from the blueprint. Each object has its own copy of the data defined by the class.

Your First Class

Product.cs C#
// Define a class
class Product
{
    // Fields (data)
    public string Name;
    public decimal Price;
    public int Quantity;
    
    // Method (behavior)
    public decimal GetTotalValue()
    {
        return Price * Quantity;
    }
}

// Create objects (instances)
Product laptop = new Product();
laptop.Name = "Dell XPS 15";
laptop.Price = 1299.99m;
laptop.Quantity = 5;

Product mouse = new Product();
mouse.Name = "Logitech MX Master";
mouse.Price = 99.99m;
mouse.Quantity = 20;

Console.WriteLine($"Laptop inventory value: {laptop.GetTotalValue():C}");
Console.WriteLine($"Mouse inventory value: {mouse.GetTotalValue():C}");
ℹ️ Class vs Object

Class = Blueprint/Template (defined once)
Object = Instance (created many times with new)

One Product class can create thousands of product objects, each with different data.

Constructors: Initializing Objects

A constructor is a special method that runs when you create an object. It's used to initialize the object's data. Constructors have the same name as the class and no return type.

ProductWithConstructor.cs C#
class Product
{
    public string Name;
    public decimal Price;
    public int Quantity;
    
    // Constructor
    public Product(string name, decimal price, int quantity)
    {
        Name = name;
        Price = price;
        Quantity = quantity;
    }
    
    public decimal GetTotalValue() => Price * Quantity;
}

// Much cleaner object creation!
Product laptop = new Product("Dell XPS 15", 1299.99m, 5);
Product mouse = new Product("Logitech MX Master", 99.99m, 20);

Constructor Overloading

ConstructorOverloading.cs C#
class Product
{
    public string Name;
    public decimal Price;
    public int Quantity;
    
    // Default constructor
    public Product()
    {
        Name = "Unknown";
        Price = 0m;
        Quantity = 0;
    }
    
    // Constructor with parameters
    public Product(string name, decimal price)
    {
        Name = name;
        Price = price;
        Quantity = 0;
    }
    
    // Full constructor
    public Product(string name, decimal price, int quantity)
    {
        Name = name;
        Price = price;
        Quantity = quantity;
    }
}

Product p1 = new Product();
Product p2 = new Product("Widget", 19.99m);
Product p3 = new Product("Gadget", 29.99m, 100);

Access Modifiers: Controlling Visibility

Access modifiers control who can see and use your class members. This is crucial for encapsulation—hiding internal details and exposing only what's necessary.

Modifier Visibility When to Use
public Accessible from anywhere Public API of your class
private Only within the same class Internal implementation details
protected Same class and derived classes Shared with child classes
internal Same assembly (project) Internal to your library
protected internal Same assembly OR derived classes Combination of both
AccessModifiers.cs C#
class BankAccount
{
    private decimal balance;  // Hidden from outside
    
    public BankAccount(decimal initialBalance)
    {
        balance = initialBalance;
    }
    
    public void Deposit(decimal amount)
    {
        if (amount > 0)
            balance += amount;
    }
    
    public bool Withdraw(decimal amount)
    {
        if (amount > 0 && amount <= balance)
        {
            balance -= amount;
            return true;
        }
        return false;
    }
    
    public decimal GetBalance() => balance;
}

BankAccount account = new BankAccount(1000m);
account.Deposit(500m);
// account.balance = 5000000;  // ERROR! Can't access private field
Console.WriteLine(account.GetBalance());  // OK - public method
💡 Best Practice

Make fields private by default. Expose data through public properties or methods. This gives you control over how data is accessed and modified.

Properties: The Right Way to Expose Data

While you can use public fields, C# provides a better way: properties. Properties look like fields from the outside but act like methods internally, giving you control over getting and setting values.

Auto-Implemented Properties

Properties.cs C#
class Product
{
    // Auto-implemented properties (compiler creates hidden field)
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Quantity { get; set; }
    
    public Product(string name, decimal price, int quantity)
    {
        Name = name;
        Price = price;
        Quantity = quantity;
    }
}

Product p = new Product("Widget", 19.99m, 100);
Console.WriteLine(p.Name);   // Uses get accessor
p.Price = 24.99m;         // Uses set accessor

Properties with Validation Logic

PropertiesWithLogic.cs C#
class Product
{
    private string name;
    private decimal price;
    private int quantity;
    
    public string Name
    {
        get => name;
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("Name cannot be empty");
            name = value;
        }
    }
    
    public decimal Price
    {
        get => price;
        set
        {
            if (value < 0)
                throw new ArgumentException("Price cannot be negative");
            price = value;
        }
    }
    
    public int Quantity
    {
        get => quantity;
        set
        {
            if (value < 0)
                throw new ArgumentException("Quantity cannot be negative");
            quantity = value;
        }
    }
}

Read-Only and Init-Only Properties

ReadOnlyProperties.cs C#
class Product
{
    // Read-only property (can only be set in constructor)
    public string Sku { get; }
    
    // Init-only property (can be set during object initialization)
    public string Name { get; init; }
    public decimal Price { get; set; }
    
    // Computed property (no setter, calculated on-the-fly)
    public decimal PriceWithTax => Price * 1.125m;
    
    public Product(string sku)
    {
        Sku = sku;  // Can set in constructor
    }
}

Product p = new Product("SKU-001") 
{ 
    Name = "Widget",  // OK - init property
    Price = 100m 
};
// p.Name = "Gadget";  // ERROR! Can't set init property after initialization
// p.Sku = "SKU-002";  // ERROR! No setter
p.Price = 120m;        // OK - has setter

Static Members: Shared Across All Instances

By default, each object has its own copy of fields and properties. But sometimes you want data or behavior that's shared across all instances of a class. That's where static comes in.

StaticMembers.cs C#
class Product
{
    // Static field - shared by all Product instances
    private static int nextId = 1;
    public static decimal TaxRate = 0.125m;
    
    // Instance fields - each object has its own copy
    public int Id { get; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    
    public Product(string name, decimal price)
    {
        Id = nextId++;  // Auto-increment ID
        Name = name;
        Price = price;
    }
    
    // Static method - called on the class, not an instance
    public static decimal CalculateTax(decimal amount)
    {
        return amount * TaxRate;
    }
    
    // Instance method
    public decimal GetPriceWithTax()
    {
        return Price * (1 + TaxRate);
    }
}

Product p1 = new Product("Widget", 100m);
Product p2 = new Product("Gadget", 200m);

Console.WriteLine(p1.Id);  // 1
Console.WriteLine(p2.Id);  // 2

// Static method - called on class
decimal tax = Product.CalculateTax(100m);

// Change static field - affects all instances
Product.TaxRate = 0.15m;
Console.WriteLine(p1.GetPriceWithTax());  // Uses new tax rate
Console.WriteLine(p2.GetPriceWithTax());  // Uses new tax rate
Instance Members Static Members
Each object has its own copy Shared across all objects
Accessed via object reference (p1.Name) Accessed via class name (Product.TaxRate)
Can access instance and static members Can only access static members
Requires object creation No object needed
ℹ️ When to Use Static

Use static for utility methods (Math.Sqrt), shared configuration, counters, or anything that doesn't depend on instance data. Examples: Console.WriteLine, int.Parse, DateTime.Now.

The Four Pillars of OOP

Object-Oriented Programming is built on four fundamental principles. Understanding these will help you design better, more maintainable software.

1. Encapsulation: Hiding Implementation Details

Encapsulation means bundling data and methods together while hiding internal details. You expose a clean public interface and keep implementation private.

Encapsulation.cs C#
class InventoryItem
{
    private int quantity;  // Hidden
    private int reorderLevel = 10;
    
    public string ProductName { get; set; }
    
    // Controlled access through properties
    public int Quantity
    {
        get => quantity;
        set
        {
            if (value < 0)
                throw new ArgumentException("Quantity cannot be negative");
            quantity = value;
            CheckReorderStatus();
        }
    }
    
    public bool NeedsReorder { get; private set; }
    
    // Private helper method
    private void CheckReorderStatus()
    {
        NeedsReorder = quantity < reorderLevel;
    }
    
    // Public methods for controlled operations
    public void AddStock(int amount)
    {
        if (amount > 0)
            Quantity += amount;
    }
    
    public bool RemoveStock(int amount)
    {
        if (amount > 0 && amount <= quantity)
        {
            Quantity -= amount;
            return true;
        }
        return false;
    }
}
💡 Encapsulation Benefits

Control: Validate data before accepting it
Flexibility: Change internal implementation without breaking external code
Security: Prevent direct manipulation of sensitive data
Maintainability: Clear separation between interface and implementation

2. Inheritance: Building on Existing Classes

Inheritance allows you to create new classes based on existing ones, reusing and extending their functionality. The new class (child/derived) inherits members from the base class (parent).

Inheritance.cs C#
// Base class
class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Sku { get; set; }
    
    public virtual decimal CalculateTotal(int quantity)
    {
        return Price * quantity;
    }
    
    public virtual void DisplayInfo()
    {
        Console.WriteLine($"Product: {Name}, Price: {Price:C}");
    }
}

// Derived class - inherits from Product
class DigitalProduct : Product
{
    public string DownloadUrl { get; set; }
    public int FileSizeMB { get; set; }
    
    // Override base method
    public override void DisplayInfo()
    {
        base.DisplayInfo();  // Call parent method
        Console.WriteLine($"Download: {DownloadUrl}, Size: {FileSizeMB}MB");
    }
}

// Another derived class
class PhysicalProduct : Product
{
    public decimal Weight { get; set; }
    public decimal ShippingCost { get; set; }
    
    public override decimal CalculateTotal(int quantity)
    {
        return (Price * quantity) + ShippingCost;
    }
}

DigitalProduct ebook = new DigitalProduct
{
    Name = "C# Programming Guide",
    Price = 29.99m,
    DownloadUrl = "https://example.com/ebook.pdf",
    FileSizeMB = 15
};

PhysicalProduct book = new PhysicalProduct
{
    Name = "C# in Depth",
    Price = 49.99m,
    Weight = 1.2m,
    ShippingCost = 5.99m
};

ebook.DisplayInfo();
Console.WriteLine(book.CalculateTotal(2));  // Includes shipping
Keyword Purpose
virtual Marks a method/property in base class as overridable
override Provides new implementation in derived class
base Calls the parent class's version of a method
sealed Prevents further inheritance

3. Polymorphism: Many Forms

Polymorphism means "many forms." It allows objects of different types to be treated uniformly through a common interface, while each type provides its own specific behavior.

Polymorphism.cs C#
// Polymorphism in action - same interface, different behaviors
Product[] products = new Product[]
{
    new DigitalProduct { Name = "Software", Price = 99.99m },
    new PhysicalProduct { Name = "Book", Price = 29.99m, ShippingCost = 5m },
    new Product { Name = "Generic", Price = 19.99m }
};

// Each calls its own version of CalculateTotal
foreach (Product p in products)
{
    Console.WriteLine($"{p.Name}: {p.CalculateTotal(1):C}");
    p.DisplayInfo();
}
Think of a remote control. The "power" button works on any device (TV, stereo, AC), but each device responds differently. The interface (button) is the same, but the behavior (what happens when you press it) varies. That's polymorphism!

4. Abstraction: Simplifying Complexity

Abstraction means hiding complex implementation details and showing only the essential features. You define what something does, not how it does it.

Abstraction.cs C#
// Abstract class - cannot be instantiated directly
abstract class PaymentMethod
{
    public string TransactionId { get; set; }
    
    // Abstract method - must be implemented by derived classes
    public abstract bool ProcessPayment(decimal amount);
    
    // Concrete method - shared by all payment methods
    public void LogTransaction(decimal amount)
    {
        Console.WriteLine($"Transaction {TransactionId}: {amount:C}");
    }
}

class CreditCardPayment : PaymentMethod
{
    public string CardNumber { get; set; }
    
    public override bool ProcessPayment(decimal amount)
    {
        Console.WriteLine($"Processing credit card payment: {amount:C}");
        // Credit card processing logic here
        LogTransaction(amount);
        return true;
    }
}

class PayPalPayment : PaymentMethod
{
    public string Email { get; set; }
    
    public override bool ProcessPayment(decimal amount)
    {
        Console.WriteLine($"Processing PayPal payment: {amount:C}");
        // PayPal processing logic here
        LogTransaction(amount);
        return true;
    }
}

// Usage - we don't care HOW payment is processed
PaymentMethod payment = new CreditCardPayment 
{ 
    TransactionId = "TXN-001",
    CardNumber = "****1234"
};
payment.ProcessPayment(99.99m);
ℹ️ Abstract vs Virtual

Abstract: No implementation in base class, derived classes MUST implement
Virtual: Has implementation in base class, derived classes CAN override

Building InvenTrack: Practical OOP Example

Let's apply everything we've learned to build core classes for our InvenTrack mini-ERP system.

InvenTrack.cs C#
// Product class with full OOP principles
class Product
{
    private static int nextId = 1;
    private decimal price;
    private int quantityInStock;
    
    // Properties
    public int Id { get; }
    public string Sku { get; init; }
    public string Name { get; set; }
    public string Description { get; set; }
    
    public decimal Price
    {
        get => price;
        set
        {
            if (value < 0)
                throw new ArgumentException("Price cannot be negative");
            price = value;
        }
    }
    
    public int QuantityInStock
    {
        get => quantityInStock;
        private set => quantityInStock = value;
    }
    
    public int ReorderLevel { get; set; } = 10;
    public bool IsLowStock => quantityInStock < ReorderLevel;
    
    // Constructor
    public Product(string sku, string name, decimal price, int initialStock)
    {
        Id = nextId++;
        Sku = sku;
        Name = name;
        Price = price;
        QuantityInStock = initialStock;
    }
    
    // Methods
    public void AddStock(int quantity)
    {
        if (quantity <= 0)
            throw new ArgumentException("Quantity must be positive");
        QuantityInStock += quantity;
    }
    
    public bool RemoveStock(int quantity)
    {
        if (quantity <= 0 || quantity > QuantityInStock)
            return false;
        QuantityInStock -= quantity;
        return true;
    }
    
    public decimal GetInventoryValue() => Price * QuantityInStock;
}

// Customer class
class Customer
{
    private static int nextId = 1000;
    
    public int Id { get; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
    public decimal CreditLimit { get; set; }
    public decimal CurrentBalance { get; private set; }
    
    public Customer(string name, string email)
    {
        Id = nextId++;
        Name = name;
        Email = email;
        CreditLimit = 1000m;
    }
    
    public bool CanPurchase(decimal amount)
    {
        return (CurrentBalance + amount) <= CreditLimit;
    }
    
    public void AddCharge(decimal amount)
    {
        CurrentBalance += amount;
    }
    
    public void MakePayment(decimal amount)
    {
        if (amount > CurrentBalance)
            amount = CurrentBalance;
        CurrentBalance -= amount;
    }
}

// SalesOrder class
class SalesOrder
{
    private static int nextOrderNumber = 1;
    
    public int OrderNumber { get; }
    public Customer Customer { get; }
    public DateTime OrderDate { get; }
    public List<OrderLine> Lines { get; }
    
    public decimal Total => Lines.Sum(l => l.GetLineTotal());
    
    public SalesOrder(Customer customer)
    {
        OrderNumber = nextOrderNumber++;
        Customer = customer;
        OrderDate = DateTime.Now;
        Lines = new List<OrderLine>();
    }
    
    public void AddLine(Product product, int quantity)
    {
        Lines.Add(new OrderLine(product, quantity));
    }
}

class OrderLine
{
    public Product Product { get; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; }
    
    public OrderLine(Product product, int quantity)
    {
        Product = product;
        Quantity = quantity;
        UnitPrice = product.Price;  // Lock in price at order time
    }
    
    public decimal GetLineTotal() => UnitPrice * Quantity;
}

// Usage example
Product laptop = new Product("LAP-001", "Dell XPS 15", 1299.99m, 10);
Product mouse = new Product("MOU-001", "Logitech MX", 99.99m, 50);

Customer customer = new Customer("Akwasi Asante", "akwasi@example.com");

SalesOrder order = new SalesOrder(customer);
order.AddLine(laptop, 1);
order.AddLine(mouse, 2);

Console.WriteLine($"Order #{{order.OrderNumber}} for {customer.Name}");
Console.WriteLine($"Total: {order.Total:C}");
🚀 Real-World Design

Notice how these classes model real business entities. Each class has a single, clear responsibility. Data is protected with private fields and exposed through properties. Methods provide controlled operations. This is professional, production-quality code structure!

Key Takeaways

  • Classes are blueprints; objects are instances created from those blueprints
  • Constructors initialize objects when they're created
  • Access modifiers (public, private, protected, internal) control visibility
  • Properties are preferred over public fields—they provide encapsulation and validation
  • Static members are shared across all instances; instance members are unique per object
  • Encapsulation: Bundle data and methods, hide implementation details
  • Inheritance: Build new classes based on existing ones (use virtual/override)
  • Polymorphism: Treat different types uniformly through a common interface
  • Abstraction: Hide complexity, define contracts with abstract classes/methods
  • OOP helps you model real-world domains in code, making applications more maintainable and scalable
🎯 Next Steps

You now understand the fundamentals of object-oriented programming! In the next section, we'll explore Collections and Generics—how to work with groups of objects efficiently and type-safely. You'll learn about Lists, Dictionaries, and how to write reusable code that works with any type.