Skip to main content

3 - Object-Oriented Programming

Object-Oriented Programming (OOP) is a programming paradigm that uses "objects" to design applications and computer programs. It utilizes several techniques from previously established paradigms, including modularity, polymorphism, and encapsulation. C# was designed from the ground up as an object-oriented language, making it an excellent choice for implementing OOP principles.

🧩 Visual Learning: What is Object-Oriented Programming?

Object-Oriented Programming is a way of organizing code that models real-world objects and their interactions. Think of it as creating digital "things" that have:

  1. Characteristics (what they know) - called properties or fields
  2. Behaviors (what they can do) - called methods

Real-World Analogy

Think about a car in the real world:

CAR
├── CHARACTERISTICS (Properties)
│ ├── Make: "Toyota"
│ ├── Model: "Corolla"
│ ├── Year: 2023
│ └── CurrentSpeed: 0 mph

└── BEHAVIORS (Methods)
├── Accelerate()
├── Brake()
└── TurnOnHeadlights()

In OOP, we create a digital version of this car with the same structure.

💡 Concept Breakdown: Objects and Classes

Classes are like blueprints or templates. They define what a type of object will look like.

Objects are specific instances created from a class. If a class is a blueprint for a house, an object is an actual house built from that blueprint.

For example:

  • The Car class is a blueprint for creating car objects
  • myCar and yourCar could be two different objects created from the same Car class
  • They share the same structure but can have different values (different make, model, etc.)

What is Object-Oriented Programming?

Object-Oriented Programming is a programming paradigm based on the concept of "objects", which can contain data and code: data in the form of fields (often known as attributes or properties), and code in the form of procedures (often known as methods).

⚠️ Common Pitfalls for Beginners

When learning OOP, beginners often struggle with:

  1. Confusing classes and objects - Remember: a class is the blueprint, an object is the thing created from the blueprint
  2. Not understanding encapsulation - It's about protecting data by controlling access to it
  3. Overcomplicating designs - Start simple and add complexity as needed
  4. Forgetting that methods should do one thing well - Each method should have a single, clear purpose
// A simple example of an object-oriented approach in C#
public class Car
{
// Data (fields and properties)
private string _make;
private string _model;
private int _year;
private double _currentSpeed;

// Constructor to initialize the object
public Car(string make, string model, int year)
{
// Validate input data
if (string.IsNullOrEmpty(make))
throw new ArgumentException("Make cannot be null or empty", nameof(make));
if (string.IsNullOrEmpty(model))
throw new ArgumentException("Model cannot be null or empty", nameof(model));
if (year < 1885) // First automobile was invented in 1885
throw new ArgumentException("Year must be 1885 or later", nameof(year));

_make = make;
_model = model;
_year = year;
_currentSpeed = 0;
}

// Properties to access data in a controlled manner
public string Make => _make;
public string Model => _model;
public int Year => _year;
public double CurrentSpeed => _currentSpeed;

// Methods that define behavior
public void Accelerate(double amount)
{
if (amount < 0)
throw new ArgumentException("Acceleration amount cannot be negative", nameof(amount));

_currentSpeed += amount;
Console.WriteLine($"{_make} {_model} is accelerating. Current speed: {_currentSpeed} mph");
}

public void Brake(double amount)
{
if (amount < 0)
throw new ArgumentException("Braking amount cannot be negative", nameof(amount));

_currentSpeed = Math.Max(0, _currentSpeed - amount);
Console.WriteLine($"{_make} {_model} is braking. Current speed: {_currentSpeed} mph");
}

public string GetDescription()
{
return $"{_year} {_make} {_model}";
}
}

Using the Car Class

// Creating and using a Car object
Car myCar = new Car("Toyota", "Corolla", 2023);

// Accessing properties
Console.WriteLine($"My car: {myCar.GetDescription()}");
Console.WriteLine($"Current speed: {myCar.CurrentSpeed} mph");

// Using methods to change the object's state
myCar.Accelerate(25);
myCar.Accelerate(15);
myCar.Brake(10);

// Output:
// My car: 2023 Toyota Corolla
// Current speed: 0 mph
// Toyota Corolla is accelerating. Current speed: 25 mph
// Toyota Corolla is accelerating. Current speed: 40 mph
// Toyota Corolla is braking. Current speed: 30 mph

The Four Pillars of OOP

The four main principles of OOP are fundamental concepts that guide how we design and implement object-oriented systems. Each principle addresses a specific aspect of software design and together they form the foundation of OOP.

1. Encapsulation

Encapsulation is the bundling of data with the methods that operate on that data, while restricting direct access to some of the object's components. It is the mechanism that binds together code and the data it manipulates, keeping both safe from outside interference and misuse.

/// <summary>
/// Represents a bank account with basic deposit and withdrawal functionality.
/// Demonstrates encapsulation by hiding the balance field and controlling access through methods.
/// </summary>
public class BankAccount
{
// Private field - hidden from outside code
private decimal _balance;

/// <summary>
/// Gets the current balance of the account.
/// </summary>
/// <remarks>
/// This is a read-only property, preventing external code from directly modifying the balance.
/// </remarks>
public decimal Balance => _balance; // Read-only property

/// <summary>
/// Deposits the specified amount into the account.
/// </summary>
/// <param name="amount">The amount to deposit.</param>
/// <exception cref="ArgumentException">Thrown when amount is zero or negative.</exception>
public void Deposit(decimal amount)
{
// Validate the deposit amount
if (amount <= 0)
throw new ArgumentException("Deposit amount must be positive", nameof(amount));

// Update the internal state
_balance += amount;
}

/// <summary>
/// Withdraws the specified amount from the account if sufficient funds are available.
/// </summary>
/// <param name="amount">The amount to withdraw.</param>
/// <returns>true if the withdrawal was successful; otherwise, false.</returns>
/// <exception cref="ArgumentException">Thrown when amount is zero or negative.</exception>
public bool Withdraw(decimal amount)
{
// Validate the withdrawal amount
if (amount <= 0)
throw new ArgumentException("Withdrawal amount must be positive", nameof(amount));

// Check if sufficient funds are available
if (amount > _balance)
return false; // Insufficient funds

// Update the internal state
_balance -= amount;
return true;
}
}

Encapsulation Explained

In the BankAccount example:

  1. The _balance field is private, meaning it can only be accessed from within the class.
  2. The Balance property provides read-only access to the balance, preventing external code from directly modifying it.
  3. The Deposit and Withdraw methods provide a controlled interface for modifying the balance, ensuring that all changes follow business rules (e.g., no negative deposits).
  4. Data validation occurs before any state changes, maintaining the integrity of the object.

Encapsulation helps create more robust and maintainable code by:

  • Hiding implementation details
  • Preventing invalid state changes
  • Allowing the internal implementation to change without affecting client code
  • Reducing dependencies between different parts of an application

2. Inheritance

Inheritance is a mechanism whereby a new class (derived class) is created from an existing class (base class), inheriting its attributes and behaviors while allowing for new or modified attributes and behaviors.

/// <summary>
/// Represents a generic animal with basic properties and behaviors.
/// </summary>
public class Animal
{
/// <summary>
/// Gets or sets the name of the animal.
/// </summary>
public string Name { get; set; }

/// <summary>
/// Initializes a new instance of the Animal class with the specified name.
/// </summary>
/// <param name="name">The name of the animal.</param>
public Animal(string name)
{
Name = name;
}

/// <summary>
/// Makes the animal produce a sound.
/// This is a virtual method that derived classes can override.
/// </summary>
public virtual void MakeSound()
{
Console.WriteLine("The animal makes a sound");
}
}

/// <summary>
/// Represents a dog, which is a specific type of animal.
/// Demonstrates inheritance by extending the Animal class.
/// </summary>
public class Dog : Animal // Dog inherits from Animal
{
/// <summary>
/// Gets or sets the breed of the dog.
/// </summary>
public string Breed { get; set; }

/// <summary>
/// Initializes a new instance of the Dog class with the specified name and breed.
/// </summary>
/// <param name="name">The name of the dog.</param>
/// <param name="breed">The breed of the dog.</param>
public Dog(string name, string breed) : base(name) // Call the base class constructor
{
Breed = breed;
}

/// <summary>
/// Makes the dog bark, overriding the base class implementation.
/// </summary>
public override void MakeSound()
{
Console.WriteLine($"{Name} the {Breed} says: Woof!");
}

/// <summary>
/// Makes the dog fetch an object.
/// This is a new method not present in the base class.
/// </summary>
public void Fetch()
{
Console.WriteLine($"{Name} is fetching the ball!");
}
}

Inheritance Explained

In the example above:

  1. Dog inherits from Animal, gaining all its public and protected members.
  2. The Dog constructor calls the Animal constructor using the base keyword.
  3. Dog overrides the MakeSound method to provide dog-specific behavior.
  4. Dog extends the base class by adding a new Fetch method and Breed property.

Inheritance enables:

  • Code reuse
  • Establishing "is-a" relationships between classes
  • Creating hierarchies of related classes
  • Specializing behavior in derived classes

3. Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common base class. It enables one interface to be used for a general class of actions, with the specific action being determined by the exact nature of the situation.

/// <summary>
/// Represents a geometric shape with area calculation capability.
/// </summary>
public class Shape
{
/// <summary>
/// Calculates the area of the shape.
/// This is a virtual method that derived classes can override.
/// </summary>
/// <returns>The area of the shape.</returns>
public virtual double CalculateArea()
{
return 0; // Base implementation
}
}

/// <summary>
/// Represents a circle, which is a specific type of shape.
/// </summary>
public class Circle : Shape
{
/// <summary>
/// Gets or sets the radius of the circle.
/// </summary>
public double Radius { get; set; }

/// <summary>
/// Initializes a new instance of the Circle class with the specified radius.
/// </summary>
/// <param name="radius">The radius of the circle.</param>
public Circle(double radius)
{
Radius = radius;
}

/// <summary>
/// Calculates the area of the circle using the formula πr².
/// </summary>
/// <returns>The area of the circle.</returns>
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}

/// <summary>
/// Represents a rectangle, which is a specific type of shape.
/// </summary>
public class Rectangle : Shape
{
/// <summary>
/// Gets or sets the width of the rectangle.
/// </summary>
public double Width { get; set; }

/// <summary>
/// Gets or sets the height of the rectangle.
/// </summary>
public double Height { get; set; }

/// <summary>
/// Initializes a new instance of the Rectangle class with the specified width and height.
/// </summary>
/// <param name="width">The width of the rectangle.</param>
/// <param name="height">The height of the rectangle.</param>
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}

/// <summary>
/// Calculates the area of the rectangle using the formula width × height.
/// </summary>
/// <returns>The area of the rectangle.</returns>
public override double CalculateArea()
{
return Width * Height;
}
}

/// <summary>
/// Demonstrates polymorphism by working with different shapes through a common interface.
/// </summary>
public class ShapeDemo
{
/// <summary>
/// Prints the area of any shape.
/// </summary>
/// <param name="shape">The shape whose area to print.</param>
public void PrintArea(Shape shape)
{
// The same method works for any Shape-derived class
Console.WriteLine($"Area: {shape.CalculateArea()}");
}

/// <summary>
/// Demonstrates polymorphic behavior with different shape types.
/// </summary>
public void DemonstratePolymorphism()
{
// Create different shapes
Shape circle = new Circle(5);
Shape rectangle = new Rectangle(4, 6);

// Use the same method for different shape types
PrintArea(circle); // Output: Area: 78.53981633974483
PrintArea(rectangle); // Output: Area: 24
}
}

Polymorphism Explained

In this example:

  1. Both Circle and Rectangle inherit from Shape and override the CalculateArea method.
  2. The PrintArea method accepts any Shape object, demonstrating polymorphic behavior.
  3. At runtime, the correct implementation of CalculateArea is called based on the actual object type.

Polymorphism enables:

  • Writing more flexible and reusable code
  • Treating objects of different types uniformly through a common interface
  • Extending functionality without modifying existing code
  • Implementing the "Open/Closed Principle" (open for extension, closed for modification)

4. Abstraction

Abstraction is the concept of hiding complex implementation details and showing only the necessary features of an object. It helps in reducing programming complexity and effort.

/// <summary>
/// Represents a database connection with abstract and concrete operations.
/// This abstract class cannot be instantiated directly.
/// </summary>
public abstract class Database
{
/// <summary>
/// Establishes a connection to the database.
/// This is an abstract method that must be implemented by derived classes.
/// </summary>
public abstract void Connect();

/// <summary>
/// Closes the connection to the database.
/// This is an abstract method that must be implemented by derived classes.
/// </summary>
public abstract void Disconnect();

/// <summary>
/// Executes a SQL query on the database.
/// This is a concrete method that uses the abstract methods.
/// </summary>
/// <param name="query">The SQL query to execute.</param>
public void ExecuteQuery(string query)
{
// Template method pattern - defines the skeleton of an algorithm
Connect();
Console.WriteLine($"Executing query: {query}");
// Process query logic here
Disconnect();
}
}

/// <summary>
/// Represents a connection to a SQL Server database.
/// </summary>
public class SqlDatabase : Database
{
/// <summary>
/// Establishes a connection to a SQL Server database.
/// </summary>
public override void Connect()
{
Console.WriteLine("Connecting to SQL Server database...");
// SQL Server-specific connection logic
}

/// <summary>
/// Closes the connection to a SQL Server database.
/// </summary>
public override void Disconnect()
{
Console.WriteLine("Disconnecting from SQL Server database...");
// SQL Server-specific disconnection logic
}
}

/// <summary>
/// Represents a connection to an Oracle database.
/// </summary>
public class OracleDatabase : Database
{
/// <summary>
/// Establishes a connection to an Oracle database.
/// </summary>
public override void Connect()
{
Console.WriteLine("Connecting to Oracle database...");
// Oracle-specific connection logic
}

/// <summary>
/// Closes the connection to an Oracle database.
/// </summary>
public override void Disconnect()
{
Console.WriteLine("Disconnecting from Oracle database...");
// Oracle-specific disconnection logic
}
}

/// <summary>
/// Demonstrates how to use different database implementations through abstraction.
/// </summary>
public class DatabaseDemo
{
/// <summary>
/// Executes a query on any database.
/// </summary>
public void RunDatabaseExample()
{
// Create database instances
Database sqlDb = new SqlDatabase();
Database oracleDb = new OracleDatabase();

// Execute queries on different databases
sqlDb.ExecuteQuery("SELECT * FROM Customers");
oracleDb.ExecuteQuery("SELECT * FROM Employees");
}
}

Abstraction Explained

In this example:

  1. Database is an abstract class that defines a common interface for all database types.
  2. It contains abstract methods (Connect and Disconnect) that must be implemented by derived classes.
  3. It also provides a concrete method (ExecuteQuery) that uses the abstract methods.
  4. SqlDatabase and OracleDatabase provide specific implementations of the abstract methods.

Abstraction enables:

  • Focusing on what an object does rather than how it does it
  • Creating simplified models of complex systems
  • Hiding implementation details behind well-defined interfaces
  • Reducing the impact of changes by isolating them to specific implementations

Why OOP in C#?

C# is a modern, object-oriented language that enables developers to build a variety of secure and robust applications. Here's why OOP is particularly effective in C#:

Type Safety

C# is a strongly-typed language, which means that the type of a variable is checked at compile-time. This helps catch errors early in the development process.

// Type safety example
string name = "John";
int age = 30;

// This would cause a compile-time error
// age = name; // Cannot implicitly convert type 'string' to 'int'

// Explicit conversion is required when types are not compatible
// int nameLength = (int)name; // This would cause a runtime error
int nameLength = name.Length; // This is the correct way to get the length of a string

Type Safety Explained

In the example above:

  1. C# enforces type checking at compile time, preventing many common programming errors.
  2. The commented line age = name; would cause a compile-time error because C# doesn't allow implicit conversion between unrelated types.
  3. Attempting to cast a string directly to an integer ((int)name) would fail at runtime.
  4. The proper way to get a numeric value from a string is to use appropriate properties or methods (like name.Length).

Type safety benefits include:

  • Early detection of type-related errors
  • Improved code reliability
  • Better IDE support with IntelliSense
  • Clearer code intent through explicit type declarations

Component-Oriented

C# supports component-oriented programming through properties, methods, and events, making it easier to develop and maintain large applications.

/// <summary>
/// Represents a UI button with text and click functionality.
/// Demonstrates component-oriented programming with properties and events.
/// </summary>
public class Button
{
// Private backing field
private string _text;

/// <summary>
/// Gets or sets the text displayed on the button.
/// Raises the TextChanged event when the value changes.
/// </summary>
public string Text
{
get => _text;
set
{
if (_text != value)
{
_text = value;
// Trigger the event when the property changes
TextChanged?.Invoke(this, EventArgs.Empty);
}
}
}

/// <summary>
/// Occurs when the Text property value changes.
/// </summary>
public event EventHandler TextChanged;

/// <summary>
/// Simulates a button click and raises the Clicked event.
/// </summary>
public void Click()
{
// Trigger the click event
Clicked?.Invoke(this, EventArgs.Empty);
}

/// <summary>
/// Occurs when the button is clicked.
/// </summary>
public event EventHandler Clicked;
}

Using the Button Component

// Creating a button component
Button submitButton = new Button();
submitButton.Text = "Submit";

// Subscribe to events using lambda expressions
submitButton.TextChanged += (sender, e) =>
{
// Cast the sender back to Button to access its properties
Button button = (Button)sender;
Console.WriteLine($"Button text changed to: {button.Text}");
};

submitButton.Clicked += (sender, e) =>
{
Console.WriteLine("Button was clicked!");
};

// Change property and trigger the TextChanged event
submitButton.Text = "Send"; // Output: Button text changed to: Send

// Call method and trigger the Clicked event
submitButton.Click(); // Output: Button was clicked!

Component-Oriented Programming Explained

The Button example demonstrates:

  1. Properties with backing fields that encapsulate data and provide controlled access
  2. Events that allow objects to notify subscribers when something happens
  3. Event handlers using lambda expressions for concise subscription
  4. Separation of concerns between the component's implementation and its usage

This approach enables:

  • Building reusable UI components
  • Creating loosely coupled systems through event-based communication
  • Implementing the Observer pattern through the event mechanism
  • Developing modular applications with clear component boundaries

Rich Framework

The .NET Framework provides a comprehensive library of pre-built classes that implement many common functions, allowing developers to focus on the unique aspects of their applications.

/// <summary>
/// Demonstrates the rich functionality available in the .NET Framework.
/// </summary>
public class FrameworkDemo
{
/// <summary>
/// Shows common file operations using the System.IO namespace.
/// </summary>
public void DemonstrateFileOperations()
{
// Reading from a file
string content = File.ReadAllText("example.txt");
Console.WriteLine($"File content: {content}");

// Writing to a file
File.WriteAllText("output.txt", "Hello, World!");
Console.WriteLine("File written successfully");

// Working with file paths
string directory = Path.GetDirectoryName("C:\\temp\\example.txt");
string fileName = Path.GetFileName("C:\\temp\\example.txt");
string extension = Path.GetExtension("C:\\temp\\example.txt");

Console.WriteLine($"Directory: {directory}");
Console.WriteLine($"File name: {fileName}");
Console.WriteLine($"Extension: {extension}");
}

/// <summary>
/// Shows collection operations using generic collections and LINQ.
/// </summary>
public void DemonstrateCollections()
{
// Creating and modifying a generic List
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
names.Add("David");
names.Remove("Bob");

Console.WriteLine("Names after modifications:");
foreach (string name in names)
{
Console.WriteLine($"- {name}");
}

// Using LINQ for querying collections
var filteredNames = names.Where(n => n.Length > 5)
.OrderBy(n => n)
.ToList();

Console.WriteLine("\nNames with more than 5 characters (sorted):");
foreach (string name in filteredNames)
{
Console.WriteLine($"- {name}");
}

// Using other collection types
Dictionary<string, int> ages = new Dictionary<string, int>
{
["Alice"] = 25,
["Charlie"] = 30,
["David"] = 22
};

Console.WriteLine("\nAges from dictionary:");
foreach (var pair in ages)
{
Console.WriteLine($"{pair.Key}: {pair.Value}");
}
}
}

.NET Framework Benefits

The .NET Framework provides:

  1. Comprehensive class libraries for common tasks like file I/O, networking, and data access
  2. Generic collections for type-safe data structures
  3. LINQ (Language Integrated Query) for querying data from various sources
  4. Consistent design patterns across the framework
  5. Cross-platform support with .NET Core and .NET 5+

This rich ecosystem allows developers to:

  • Focus on business logic rather than reinventing common functionality
  • Write more concise and expressive code
  • Leverage battle-tested implementations for better reliability
  • Benefit from performance optimizations in framework classes

Modern Features

C# continues to evolve with new features that enhance OOP capabilities, such as records, pattern matching, and primary constructors.

/// <summary>
/// Demonstrates modern C# features that enhance object-oriented programming.
/// </summary>
public class ModernCSharpDemo
{
/// <summary>
/// Shows the use of records for creating immutable data types.
/// </summary>
public void DemonstrateRecords()
{
// C# 9+ Record type - immutable reference type with value-based equality
public record Person(string FirstName, string LastName, int Age);

// Creating record instances
var person1 = new Person("John", "Doe", 30);
var person2 = new Person("John", "Doe", 30);
var person3 = new Person("Jane", "Doe", 28);

// Value-based equality comparison
Console.WriteLine($"person1 == person2: {person1 == person2}"); // True
Console.WriteLine($"person1 == person3: {person1 == person3}"); // False

// Records are immutable, but you can create new instances with some properties changed
var olderPerson = person1 with { Age = 31 };
Console.WriteLine($"Modified person: {olderPerson}"); // Person { FirstName = John, LastName = Doe, Age = 31 }
}

/// <summary>
/// Shows the use of primary constructors for classes.
/// </summary>
public void DemonstratePrimaryConstructors()
{
// C# 12+ Primary constructor for a class
public class Product(string name, decimal price)
{
// Properties initialized from constructor parameters
public string Name { get; } = name;
public decimal Price { get; } = price;

// Methods can use constructor parameters directly
public decimal CalculateDiscount(decimal percentage)
{
return Price * percentage / 100;
}

public override string ToString() => $"{Name}: {Price:C}";
}

// Creating an instance using the primary constructor
var laptop = new Product("Laptop", 999.99m);
Console.WriteLine(laptop); // Laptop: $999.99
Console.WriteLine($"20% discount: {laptop.CalculateDiscount(20):C}"); // 20% discount: $199.99
}

/// <summary>
/// Shows the use of pattern matching for type checking and data extraction.
/// </summary>
public void DemonstratePatternMatching()
{
// Create an object of unknown type
object obj = new Circle(5);

// Pattern matching with type patterns
if (obj is Circle circle)
{
// The variable 'circle' is now available with the correct type
Console.WriteLine($"This is a circle with radius {circle.Radius}");
Console.WriteLine($"Area: {circle.CalculateArea()}");
}
else if (obj is Rectangle rectangle)
{
Console.WriteLine($"This is a rectangle with dimensions {rectangle.Width}x{rectangle.Height}");
Console.WriteLine($"Area: {rectangle.CalculateArea()}");
}
else if (obj is Shape shape)
{
Console.WriteLine($"This is a shape with area {shape.CalculateArea()}");
}

// Switch expression with patterns (C# 8+)
string description = obj switch
{
Circle c => $"Circle with radius {c.Radius}",
Rectangle r => $"Rectangle {r.Width}x{r.Height}",
Shape => "Some other shape",
_ => "Not a shape"
};

Console.WriteLine(description);
}
}

Modern C# Features Explained

  1. Records:

    • Concise syntax for creating immutable reference types
    • Built-in value-based equality (instead of reference-based)
    • Non-destructive mutation with the with expression
    • Automatic implementation of ToString(), Equals(), and GetHashCode()
  2. Primary Constructors:

    • Simplified class declaration with parameters directly in the class header
    • Parameters are accessible throughout the class body
    • Reduces boilerplate code for simple classes
  3. Pattern Matching:

    • Type patterns for safe type checking and conversion in one step
    • Property patterns for checking object properties
    • Switch expressions for concise conditional logic
    • Deconstruction patterns for working with tuples and records

These modern features make C# code:

  • More concise and expressive
  • Less prone to errors through immutability
  • More maintainable with reduced boilerplate
  • More powerful with enhanced type checking and pattern recognition

OOP in Practice

Object-oriented programming is not just about using classes and objects; it's about designing systems that are:

  1. Modular: Breaking down complex problems into smaller, manageable parts
  2. Reusable: Creating components that can be used in multiple contexts
  3. Maintainable: Organizing code in a way that makes it easier to understand and modify
  4. Extensible: Designing systems that can be easily extended with new functionality

In the following sections, we'll explore each of the OOP principles in detail and see how they are implemented in C#. We'll start with the fundamental building blocks: classes and objects.

Example: A Simple Library Management System

The following example demonstrates a simple library management system that incorporates multiple OOP principles:

/// <summary>
/// Represents a book in a library.
/// </summary>
public class Book
{
/// <summary>Gets the title of the book.</summary>
public string Title { get; }

/// <summary>Gets the author of the book.</summary>
public string Author { get; }

/// <summary>Gets the ISBN (International Standard Book Number) of the book.</summary>
public string ISBN { get; }

/// <summary>Gets a value indicating whether the book is currently checked out.</summary>
public bool IsCheckedOut { get; private set; }

/// <summary>
/// Initializes a new instance of the Book class with the specified title, author, and ISBN.
/// </summary>
/// <param name="title">The title of the book.</param>
/// <param name="author">The author of the book.</param>
/// <param name="isbn">The ISBN of the book.</param>
/// <exception cref="ArgumentException">
/// Thrown when title, author, or ISBN is null or empty.
/// </exception>
public Book(string title, string author, string isbn)
{
// Validate input parameters
if (string.IsNullOrEmpty(title))
throw new ArgumentException("Title cannot be empty", nameof(title));
if (string.IsNullOrEmpty(author))
throw new ArgumentException("Author cannot be empty", nameof(author));
if (string.IsNullOrEmpty(isbn))
throw new ArgumentException("ISBN cannot be empty", nameof(isbn));

// Initialize properties
Title = title;
Author = author;
ISBN = isbn;
IsCheckedOut = false; // Books start as available
}

/// <summary>
/// Checks out the book if it is available.
/// </summary>
/// <returns>true if the book was successfully checked out; otherwise, false.</returns>
public bool CheckOut()
{
// Can't check out a book that's already checked out
if (IsCheckedOut)
return false;

// Update the state
IsCheckedOut = true;
return true;
}

/// <summary>
/// Returns the book if it is checked out.
/// </summary>
/// <returns>true if the book was successfully returned; otherwise, false.</returns>
public bool Return()
{
// Can't return a book that's not checked out
if (!IsCheckedOut)
return false;

// Update the state
IsCheckedOut = false;
return true;
}

/// <summary>
/// Returns a string that represents the current book.
/// </summary>
/// <returns>A string containing the title, author, and ISBN of the book.</returns>
public override string ToString()
{
return $"{Title} by {Author} (ISBN: {ISBN})";
}
}

/// <summary>
/// Represents a library that manages a collection of books.
/// </summary>
public class Library
{
// Private collection of books - encapsulation
private List<Book> _books = new List<Book>();

/// <summary>
/// Adds a book to the library.
/// </summary>
/// <param name="book">The book to add.</param>
/// <exception cref="ArgumentNullException">Thrown when book is null.</exception>
public void AddBook(Book book)
{
if (book == null)
throw new ArgumentNullException(nameof(book));

_books.Add(book);
}

/// <summary>
/// Finds a book by its ISBN.
/// </summary>
/// <param name="isbn">The ISBN to search for.</param>
/// <returns>The book with the specified ISBN, or null if no such book exists.</returns>
/// <exception cref="ArgumentException">Thrown when ISBN is null or empty.</exception>
public Book FindBookByISBN(string isbn)
{
if (string.IsNullOrEmpty(isbn))
throw new ArgumentException("ISBN cannot be empty", nameof(isbn));

// Use LINQ to find the first book with matching ISBN, or null if none found
return _books.FirstOrDefault(b => b.ISBN == isbn);
}

/// <summary>
/// Finds all books by a specific author.
/// </summary>
/// <param name="author">The author name to search for (case-insensitive, partial match).</param>
/// <returns>A list of books by the specified author.</returns>
/// <exception cref="ArgumentException">Thrown when author is null or empty.</exception>
public List<Book> FindBooksByAuthor(string author)
{
if (string.IsNullOrEmpty(author))
throw new ArgumentException("Author cannot be empty", nameof(author));

// Use LINQ to find all books with matching author (case-insensitive)
return _books.Where(b => b.Author.Contains(author, StringComparison.OrdinalIgnoreCase))
.ToList();
}

/// <summary>
/// Checks out a book by its ISBN.
/// </summary>
/// <param name="isbn">The ISBN of the book to check out.</param>
/// <returns>true if the book was successfully checked out; otherwise, false.</returns>
public bool CheckOutBook(string isbn)
{
var book = FindBookByISBN(isbn);
if (book == null)
return false; // Book not found

return book.CheckOut(); // Delegate to the book's method
}

/// <summary>
/// Returns a book by its ISBN.
/// </summary>
/// <param name="isbn">The ISBN of the book to return.</param>
/// <returns>true if the book was successfully returned; otherwise, false.</returns>
public bool ReturnBook(string isbn)
{
var book = FindBookByISBN(isbn);
if (book == null)
return false; // Book not found

return book.Return(); // Delegate to the book's method
}

/// <summary>
/// Gets all books that are currently available (not checked out).
/// </summary>
/// <returns>A list of available books.</returns>
public List<Book> GetAllAvailableBooks()
{
return _books.Where(b => !b.IsCheckedOut).ToList();
}

/// <summary>
/// Gets all books that are currently checked out.
/// </summary>
/// <returns>A list of checked out books.</returns>
public List<Book> GetAllCheckedOutBooks()
{
return _books.Where(b => b.IsCheckedOut).ToList();
}
}

Using the Library System

// Creating a library management system
Library library = new Library();

// Add books to the library
library.AddBook(new Book("The Great Gatsby", "F. Scott Fitzgerald", "9780743273565"));
library.AddBook(new Book("To Kill a Mockingbird", "Harper Lee", "9780060935467"));
library.AddBook(new Book("1984", "George Orwell", "9780451524935"));

// Find a specific book by ISBN
Book book = library.FindBookByISBN("9780743273565");
Console.WriteLine(book); // Output: The Great Gatsby by F. Scott Fitzgerald (ISBN: 9780743273565)

// Find books by a specific author
List<Book> orwellBooks = library.FindBooksByAuthor("Orwell");
Console.WriteLine("Books by Orwell:");
foreach (var b in orwellBooks)
{
Console.WriteLine($"- {b}"); // Output: - 1984 by George Orwell (ISBN: 9780451524935)
}

// Check out a book
bool success = library.CheckOutBook("9780060935467");
Console.WriteLine(success ? "Book checked out successfully" : "Failed to check out book");

// Get all available books
List<Book> availableBooks = library.GetAllAvailableBooks();
Console.WriteLine("\nAvailable books:");
foreach (var b in availableBooks)
{
Console.WriteLine($"- {b}");
}

// Get all checked out books
List<Book> checkedOutBooks = library.GetAllCheckedOutBooks();
Console.WriteLine("\nChecked out books:");
foreach (var b in checkedOutBooks)
{
Console.WriteLine($"- {b}");
}

// Return a book
success = library.ReturnBook("9780060935467");
Console.WriteLine(success ? "\nBook returned successfully" : "\nFailed to return book");

// Verify the book is now available
availableBooks = library.GetAllAvailableBooks();
Console.WriteLine("\nAvailable books after return:");
foreach (var b in availableBooks)
{
Console.WriteLine($"- {b}");
}

Library System Explained

This example demonstrates several OOP principles:

  1. Encapsulation:

    • The Book class encapsulates its data with private or read-only properties
    • The IsCheckedOut property has a private setter, allowing only the class to modify it
    • The Library class hides its book collection (_books) and provides controlled access through methods
  2. Abstraction:

    • The Library class provides a high-level interface for managing books
    • Users of the library don't need to know how books are stored or managed internally
    • Complex operations like searching are simplified into easy-to-use methods
  3. Single Responsibility Principle:

    • The Book class is responsible only for book-related behavior
    • The Library class is responsible for managing the collection of books
    • Each method has a single, well-defined purpose
  4. Validation and Error Handling:

    • Input parameters are validated to ensure data integrity
    • Methods return appropriate values to indicate success or failure
    • Exceptions are thrown with meaningful messages when necessary

In the following sections, we'll explore each of the OOP principles in detail and see how they are implemented in C#. We'll start with the fundamental building blocks: classes and objects.