📚 Technical ArticleJanuary 18, 2025

Introduction to Object-Oriented Programming in C#: Building with Classes and Objects

Discover the power of Object-Oriented Programming in C#. Learn about classes, objects, properties, constructors, and encapsulation with practical examples that make OOP concepts clear for beginners.

14 min readSenior Level

Introduction to Object-Oriented Programming in C#: Building with Classes and Objects

Up until now, you've been writing programs using variables, control flow, and methods. These are powerful tools, but as your programs grow larger, you'll find yourself wanting to group related data and behaviors together. That's where Object-Oriented Programming (OOP) comes in!

Think of OOP like building with LEGO blocks. Instead of having loose pieces scattered everywhere, you create specific, reusable components that have both structure (data) and functionality (methods) built into them. These components can then work together to build complex systems.

What is Object-Oriented Programming?

Object-Oriented Programming is a programming approach that organizes code around "objects" - entities that contain both data (properties) and behaviors (methods). Instead of having separate variables and functions floating around, you bundle them together into logical units.

Imagine you're building a video game. Instead of having separate variables for player health, player name, player level, and separate methods for player actions, you create a "Player" object that contains all player-related data and behaviors in one place.

Classes: The Blueprint

A class is like a blueprint or template for creating objects. Just like an architect's blueprint defines what a house should look like, a class defines what an object should contain and what it can do.

Here's your first class:

public class Person
{
    // Properties (data)
    public string Name;
    public int Age;
    public string Email;

    // Methods (behaviors)
    public void Introduce()
    {
        Console.WriteLine($"Hi, I'm {Name}. I'm {Age} years old.");
    }

    public void SendEmail(string message)
    {
        Console.WriteLine($"Sending email to {Email}: {message}");
    }
}

This Person class is a blueprint that defines:

  • Data: Name, Age, Email
  • Behaviors: Introduce yourself, Send an email

Objects: The Actual Things

An object is a specific instance created from a class blueprint. You can create multiple objects from the same class, each with their own unique data:

using System;

class Program
{
    static void Main()
    {
        // Creating objects from the Person class
        Person alice = new Person();
        alice.Name = "Alice Johnson";
        alice.Age = 28;
        alice.Email = "alice@email.com";

        Person bob = new Person();
        bob.Name = "Bob Smith";
        bob.Age = 35;
        bob.Email = "bob@email.com";

        // Using the objects
        alice.Introduce();  // "Hi, I'm Alice Johnson. I'm 28 years old."
        bob.Introduce();    // "Hi, I'm Bob Smith. I'm 35 years old."

        alice.SendEmail("Let's meet for coffee!");
        bob.SendEmail("Thanks for your help yesterday!");
    }
}

Notice how each object has its own separate data, but they both use the same methods defined in the class.

Properties: Better Data Management

While public fields work, C# provides a better way to manage data through properties. Properties give you more control over how data is accessed and modified:

public class BankAccount
{
    private double _balance;  // Private field - can't be accessed directly from outside

    // Property with getter and setter
    public double Balance
    {
        get { return _balance; }
        private set  // Private setter - only this class can modify the balance
        {
            if (value >= 0)
                _balance = value;
            else
                Console.WriteLine("Balance cannot be negative!");
        }
    }

    // Auto-implemented properties (C# does the work for you)
    public string AccountHolder { get; set; }
    public string AccountNumber { get; set; }

    // Methods to safely modify the balance
    public void Deposit(double amount)
    {
        if (amount > 0)
        {
            Balance += amount;
            Console.WriteLine($"Deposited ${amount}. New balance: ${Balance}");
        }
        else
        {
            Console.WriteLine("Deposit amount must be positive!");
        }
    }

    public bool Withdraw(double amount)
    {
        if (amount > 0 && amount <= Balance)
        {
            Balance -= amount;
            Console.WriteLine($"Withdrew ${amount}. New balance: ${Balance}");
            return true;
        }
        else
        {
            Console.WriteLine("Invalid withdrawal amount!");
            return false;
        }
    }
}

Constructors: Setting Up Objects

Constructors are special methods that run when you create a new object. They help you set up the object with initial values:

public class Car
{
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
    public double Mileage { get; private set; }
    public bool IsRunning { get; private set; }

    // Default constructor
    public Car()
    {
        Make = "Unknown";
        Model = "Unknown";
        Year = 2020;
        Mileage = 0;
        IsRunning = false;
    }

    // Constructor with parameters
    public Car(string make, string model, int year)
    {
        Make = make;
        Model = model;
        Year = year;
        Mileage = 0;
        IsRunning = false;
    }

    // Constructor with all parameters
    public Car(string make, string model, int year, double mileage)
    {
        Make = make;
        Model = model;
        Year = year;
        Mileage = mileage;
        IsRunning = false;
    }

    // Methods
    public void Start()
    {
        if (!IsRunning)
        {
            IsRunning = true;
            Console.WriteLine($"The {Year} {Make} {Model} is now running.");
        }
        else
        {
            Console.WriteLine("Car is already running!");
        }
    }

    public void Stop()
    {
        if (IsRunning)
        {
            IsRunning = false;
            Console.WriteLine($"The {Year} {Make} {Model} has been stopped.");
        }
        else
        {
            Console.WriteLine("Car is already stopped!");
        }
    }

    public void Drive(double miles)
    {
        if (IsRunning && miles > 0)
        {
            Mileage += miles;
            Console.WriteLine($"Drove {miles} miles. Total mileage: {Mileage}");
        }
        else
        {
            Console.WriteLine("Cannot drive - car is not running or invalid distance!");
        }
    }
}

// Usage
class Program
{
    static void Main()
    {
        // Different ways to create Car objects
        Car car1 = new Car();  // Uses default constructor
        Car car2 = new Car("Toyota", "Camry", 2022);
        Car car3 = new Car("Honda", "Accord", 2020, 15000);

        // Using the cars
        car2.Start();
        car2.Drive(50);
        car2.Stop();
    }
}

Encapsulation: Protecting Your Data

Encapsulation is one of the core principles of OOP. It means hiding the internal details of how an object works and only exposing what's necessary. This protects data from being accidentally corrupted.

public class Student
{
    private double[] _grades;
    private int _gradeCount;

    public string Name { get; set; }
    public string StudentId { get; set; }

    // Read-only property - calculated from private data
    public double GPA
    {
        get
        {
            if (_gradeCount == 0) return 0.0;

            double total = 0;
            for (int i = 0; i < _gradeCount; i++)
            {
                total += _grades[i];
            }
            return total / _gradeCount;
        }
    }

    public int TotalGrades => _gradeCount;  // Expression-bodied property

    public Student(string name, string studentId)
    {
        Name = name;
        StudentId = studentId;
        _grades = new double[10];  // Can store up to 10 grades
        _gradeCount = 0;
    }

    // Public method to safely add grades
    public bool AddGrade(double grade)
    {
        if (grade < 0 || grade > 100)
        {
            Console.WriteLine("Grade must be between 0 and 100!");
            return false;
        }

        if (_gradeCount >= _grades.Length)
        {
            Console.WriteLine("Cannot add more grades - storage full!");
            return false;
        }

        _grades[_gradeCount] = grade;
        _gradeCount++;
        Console.WriteLine($"Added grade {grade}. New GPA: {GPA:F2}");
        return true;
    }

    public void DisplayGrades()
    {
        Console.WriteLine($"Student: {Name} (ID: {StudentId})");
        Console.WriteLine("Grades:");
        for (int i = 0; i < _gradeCount; i++)
        {
            Console.WriteLine($"  Grade {i + 1}: {_grades[i]}");
        }
        Console.WriteLine($"Current GPA: {GPA:F2}");
    }
}

Real-World Example: A Simple Library System

Let's build a more complex example that demonstrates all these concepts working together:

using System;
using System.Collections.Generic;

public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
    public string ISBN { get; set; }
    public bool IsAvailable { get; private set; }
    public DateTime? DueDate { get; private set; }  // Nullable DateTime

    public Book(string title, string author, string isbn)
    {
        Title = title;
        Author = author;
        ISBN = isbn;
        IsAvailable = true;
        DueDate = null;
    }

    public bool CheckOut(int daysToReturn = 14)
    {
        if (IsAvailable)
        {
            IsAvailable = false;
            DueDate = DateTime.Now.AddDays(daysToReturn);
            Console.WriteLine($"'{Title}' checked out. Due: {DueDate:MM/dd/yyyy}");
            return true;
        }
        else
        {
            Console.WriteLine($"'{Title}' is not available for checkout.");
            return false;
        }
    }

    public void Return()
    {
        if (!IsAvailable)
        {
            IsAvailable = true;
            bool wasLate = DateTime.Now > DueDate;
            DueDate = null;

            if (wasLate)
            {
                Console.WriteLine($"'{Title}' returned LATE. Please pay fine.");
            }
            else
            {
                Console.WriteLine($"'{Title}' returned on time. Thank you!");
            }
        }
        else
        {
            Console.WriteLine($"'{Title}' was not checked out.");
        }
    }

    public void DisplayInfo()
    {
        Console.WriteLine($"'{Title}' by {Author}");
        Console.WriteLine($"ISBN: {ISBN}");
        Console.WriteLine($"Status: {(IsAvailable ? "Available" : $"Checked out until {DueDate:MM/dd/yyyy}")}");
    }
}

public class LibraryMember
{
    private List<Book> _checkedOutBooks;

    public string Name { get; set; }
    public string MemberId { get; set; }
    public DateTime MemberSince { get; set; }

    public int BooksCheckedOut => _checkedOutBooks.Count;
    public bool CanCheckOutMore => _checkedOutBooks.Count < 5;  // Limit of 5 books

    public LibraryMember(string name, string memberId)
    {
        Name = name;
        MemberId = memberId;
        MemberSince = DateTime.Now;
        _checkedOutBooks = new List<Book>();
    }

    public bool CheckOutBook(Book book)
    {
        if (!CanCheckOutMore)
        {
            Console.WriteLine($"{Name} has reached the checkout limit!");
            return false;
        }

        if (book.CheckOut())
        {
            _checkedOutBooks.Add(book);
            Console.WriteLine($"{Name} successfully checked out '{book.Title}'");
            return true;
        }
        return false;
    }

    public bool ReturnBook(Book book)
    {
        if (_checkedOutBooks.Contains(book))
        {
            book.Return();
            _checkedOutBooks.Remove(book);
            Console.WriteLine($"{Name} returned '{book.Title}'");
            return true;
        }
        else
        {
            Console.WriteLine($"{Name} doesn't have '{book.Title}' checked out.");
            return false;
        }
    }

    public void DisplayMemberInfo()
    {
        Console.WriteLine($"Member: {Name} (ID: {MemberId})");
        Console.WriteLine($"Member since: {MemberSince:MM/dd/yyyy}");
        Console.WriteLine($"Books checked out: {BooksCheckedOut}/5");

        if (_checkedOutBooks.Count > 0)
        {
            Console.WriteLine("Currently checked out books:");
            foreach (Book book in _checkedOutBooks)
            {
                Console.WriteLine($"  - '{book.Title}' (Due: {book.DueDate:MM/dd/yyyy})");
            }
        }
    }
}

// Using the library system
class Program
{
    static void Main()
    {
        Console.WriteLine("=== Library Management System ===\n");

        // Create books
        Book book1 = new Book("The C# Programming Language", "Anders Hejlsberg", "978-0321741769");
        Book book2 = new Book("Clean Code", "Robert Martin", "978-0132350884");
        Book book3 = new Book("Design Patterns", "Gang of Four", "978-0201633612");

        // Create library member
        LibraryMember member = new LibraryMember("Sarah Wilson", "LIB001");

        // Demonstrate the system
        member.DisplayMemberInfo();
        Console.WriteLine();

        // Check out books
        member.CheckOutBook(book1);
        member.CheckOutBook(book2);
        Console.WriteLine();

        // Display updated member info
        member.DisplayMemberInfo();
        Console.WriteLine();

        // Display book info
        book1.DisplayInfo();
        Console.WriteLine();

        // Return a book
        member.ReturnBook(book1);
        Console.WriteLine();

        // Final member status
        member.DisplayMemberInfo();
    }
}

The Four Pillars of OOP (Preview)

While we've focused on the basics, OOP has four main principles:

  1. Encapsulation ✅ - We covered this! Bundling data and methods, hiding internal details
  2. Inheritance - Creating new classes based on existing ones (we'll cover this in advanced topics)
  3. Polymorphism - Objects of different types responding to the same interface (advanced topic)
  4. Abstraction - Hiding complex implementation details behind simple interfaces (advanced topic)

When to Use Classes vs Methods

You might wonder: "When should I create a class versus just using methods?" Here's a simple guide:

Use Classes When:

  • You have related data that belongs together
  • You need to create multiple similar objects
  • You want to protect data from being accessed incorrectly
  • You're modeling real-world entities (Person, Car, Account, etc.)

Use Methods When:

  • You're performing a single, specific calculation
  • You're doing utility operations that don't need persistent data
  • You're working with static operations (Math.Max, Console.WriteLine)
// ✅ Good use of a class - related data and behaviors
public class ShoppingCart
{
    private List<Item> items;
    public double Total { get; private set; }

    public void AddItem(Item item) { /* implementation */ }
    public void RemoveItem(Item item) { /* implementation */ }
    public void Checkout() { /* implementation */ }
}

// ✅ Good use of methods - utility functions
public static class MathHelper
{
    public static double CalculateDistance(double x1, double y1, double x2, double y2)
    {
        return Math.Sqrt(Math.Pow(x2 - x1, 2) + Math.Pow(y2 - y1, 2));
    }

    public static bool IsPrime(int number)
    {
        // Prime checking logic
        return true; // simplified
    }
}

Best Practices for Classes

1. Single Responsibility

Each class should have one main purpose:

// ❌ This class does too many things
public class BadUserClass
{
    public string Name { get; set; }
    public void SendEmail() { }
    public void SaveToDatabase() { }
    public void ProcessPayment() { }
    public void GenerateReport() { }
}

// ✅ Focused classes with single responsibilities
public class User
{
    public string Name { get; set; }
    public string Email { get; set; }
}

public class EmailService
{
    public void SendEmail(User user, string message) { }
}

public class UserRepository
{
    public void SaveUser(User user) { }
}

2. Use Properties Instead of Public Fields

// ❌ Public fields - no control
public class BadPerson
{
    public string Name;
    public int Age;  // What if someone sets this to -5?
}

// ✅ Properties with validation
public class GoodPerson
{
    private int _age;

    public string Name { get; set; }

    public int Age
    {
        get { return _age; }
        set
        {
            if (value >= 0 && value <= 150)
                _age = value;
            else
                throw new ArgumentException("Age must be between 0 and 150");
        }
    }
}

3. Initialize Objects Properly

// ✅ Good constructor that ensures valid state
public class Rectangle
{
    public double Width { get; private set; }
    public double Height { get; private set; }

    public Rectangle(double width, double height)
    {
        if (width <= 0 || height <= 0)
            throw new ArgumentException("Width and height must be positive");

        Width = width;
        Height = height;
    }

    public double Area => Width * Height;
    public double Perimeter => 2 * (Width + Height);
}

Common Beginner Mistakes

1. Making Everything Public

// ❌ Everything is public - no encapsulation
public class BadBankAccount
{
    public double balance;  // Anyone can modify this directly!
    public string pin;      // Security risk!
}

// ✅ Proper encapsulation
public class GoodBankAccount
{
    private double _balance;
    private string _pin;

    public double Balance => _balance;  // Read-only access

    public bool Withdraw(double amount, string enteredPin)
    {
        if (enteredPin != _pin) return false;
        if (amount > _balance) return false;

        _balance -= amount;
        return true;
    }
}

2. Not Using Constructors

// ❌ Object starts in invalid state
Person person = new Person();
// person.Name is null, person.Age is 0 - is this valid?

// ✅ Constructor ensures valid initial state
Person person = new Person("John Doe", 25);
// Object is guaranteed to be properly initialized

3. Creating God Classes

// ❌ One class that does everything
public class GameManager
{
    // Player properties
    public string PlayerName { get; set; }
    public int PlayerHealth { get; set; }

    // Enemy properties
    public string EnemyName { get; set; }
    public int EnemyHealth { get; set; }

    // UI properties
    public string WindowTitle { get; set; }

    // File operations
    public void SaveGame() { }
    public void LoadGame() { }

    // Combat logic
    public void Attack() { }
    public void Defend() { }

    // And 50 more methods...
}

// ✅ Separate focused classes
public class Player { /* Player-specific code */ }
public class Enemy { /* Enemy-specific code */ }
public class GameUI { /* UI-specific code */ }
public class SaveSystem { /* Save/load logic */ }
public class CombatSystem { /* Combat logic */ }

Practice Exercises

Try building these to practice OOP concepts:

Exercise 1: Library Book Management

Create classes for:

  • Book (title, author, ISBN, availability)
  • Library (collection of books, add/remove/search functionality)
  • Member (name, ID, borrowed books list)

Exercise 2: Simple E-commerce System

Create classes for:

  • Product (name, price, stock quantity)
  • ShoppingCart (add/remove items, calculate total)
  • Customer (name, email, order history)

Exercise 3: Basic RPG Character System

Create classes for:

  • Character (name, health, level, experience)
  • Inventory (items, add/remove functionality)
  • Weapon (damage, durability)

What's Next?

Congratulations! You've taken a major step into professional programming by learning Object-Oriented Programming. You now understand how to create classes, objects, properties, and constructors - the building blocks of larger applications.

In our final article in this series, we'll cover Error Handling and Debugging - essential skills for dealing with the unexpected situations that arise in real programming projects.

OOP is how most modern software is built. You're now thinking like a professional developer, organizing code into logical, reusable components that model real-world concepts!

Key Takeaways

  • Classes are blueprints that define structure and behavior
  • Objects are instances created from class blueprints
  • Properties provide controlled access to data with getters and setters
  • Constructors initialize objects with proper starting values
  • Encapsulation protects data by hiding internal implementation details
  • Single responsibility keeps classes focused and maintainable
  • Proper initialization ensures objects start in valid states
  • Access modifiers (public, private) control what can be accessed from outside

You're now ready to build sophisticated, well-organized applications! 🚀

Found this helpful?

Last updated: January 18, 202514 min read
Senior Level Content

Level Up Your Engineering Skills

Join thousands of senior engineers who get weekly insights on system design, architecture patterns, and advanced programming techniques.

No spam. Unsubscribe at any time. We respect your privacy.

#csharp#oop#classes#objects#properties#beginners#fundamentals