Mastering Object-Oriented Programming in C#: Classes, Modifiers, and the Four Pillars
Object-oriented programming (OOP) is a programming paradigm that revolutionized software development by organizing code around the concept of "objects." In C#, a strongly typed, object-oriented language developed by Microsoft, mastering OOP principles is essential for writing clean, maintainable, and scalable code. This article explores key object-oriented concepts in C#, starting with fundamental building blocks like classes and access modifiers before delving into the four pillars of OOP.
Key OOP-Related Keywords in C#
Before we explore the four pillars of OOP, it's important to understand the basic constructs that C# provides for implementing object-oriented programming.
Class vs Object
A class is a blueprint or template that defines properties, methods, and behaviors.
An object is an instance of a class - a concrete entity created based on the class definition.
// Class definition
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public void Introduce()
{
Console.WriteLine($"Hi, I'm {Name} and I'm {Age} years old.");
}
}
// Object creation and use
Person person1 = new Person();
person1.Name = "John";
person1.Age = 30;
person1.Introduce(); // Output: Hi, I'm John and I'm 30 years old.
Partial Class
Partial classes allow you to split the definition of a class into multiple files, which is useful for separating generated code from custom code.
// File: Person.cs
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
// File: PersonMethods.cs
public partial class Person
{
public void Introduce()
{
Console.WriteLine($"Hi, I'm {Name} and I'm {Age} years old.");
}
}
Interface
An interface is a contract that defines a set of methods, properties, and events that implementing classes must provide.
public interface IDriveable
{
void Drive();
}
public class Car : IDriveable
{
public void Drive()
{
Console.WriteLine("Driving the car...");
}
}
public interface IFlyable
{
void Fly();
}
// Multiple interface implementation
public class FlyingCar : IDriveable, IFlyable
{
public void Drive()
{
Console.WriteLine("Driving...");
}
public void Fly()
{
Console.WriteLine("Flying...");
}
}
Interfaces enable a form of multiple inheritance in C# and are fundamental to many design patterns and architectural approaches.
Sealed Class
A sealed class cannot be inherited from, preventing other classes from extending it.
public sealed class Employee
{
public string Department { get; set; }
public decimal Salary { get; set; }
public void CalculateBonus()
{
// Method implementation
}
}
// This would cause a compilation error:
// public class Manager : Employee { } // Error: Cannot inherit from sealed type 'Employee'
Sealed classes are used when inheritance is not appropriate for a class, often for security or design reasons, or to optimize performance.
Access Modifiers in C#
Access modifiers control the visibility and accessibility of classes, methods, properties, and other members. C# offers six access modifiers, each serving a specific purpose:
Public
The most permissive modifier - accessible from anywhere.
public class PaymentService
{
public decimal ProcessPayment(decimal amount)
{
return amount * 1.1m; // With 10% fee
}
}
// Can be used anywhere
var service = new PaymentService();
var total = service.ProcessPayment(100);
This ProcessPayment
method can be called from any other class, regardless of what assembly it's in.
Private
The most restrictive modifier - only accessible within the same class.
public class User
{
private string password;
private bool ValidatePassword(string input)
{
return password == HashPassword(input);
}
public bool Login(string username, string passwordAttempt)
{
return ValidatePassword(passwordAttempt); // Can access private method
}
}
The password
field and ValidatePassword
method are hidden from all other classes, ensuring that sensitive data and validation logic cannot be accessed directly from outside the class.
Protected
Accessible within the same class and by derived classes.
public class Shape
{
protected double area;
protected virtual void Calculate()
{
// Base calculation logic
}
}
public class Circle : Shape
{
private double radius;
public void UpdateRadius(double newRadius)
{
radius = newRadius;
Calculate(); // Can access protected method
Console.WriteLine($"Area: {area}"); // Can access protected field
}
}
The area
field and Calculate
method are accessible within the Shape
class and also in the derived Circle
class, but not in unrelated classes
Internal
Accessible only within the same assembly (project).
internal class Logger
{
internal void LogMessage(string message)
{
// Only accessible within same assembly
Console.WriteLine($"[LOG]: {message}");
}
}
// Usage in same assembly
class Program
{
void Method()
{
var logger = new Logger();
logger.LogMessage("Test"); // Works in same assembly
}
}
This is useful for components that should only be used within the current assembly and not exposed as part of the public API.
The Four Pillars of OOP
The foundation of object-oriented programming rests on four core principles: Encapsulation, Inheritance, Polymorphism, and Abstraction. Understanding these concepts is crucial for effective C# development.
Encapsulation
Encapsulation is the practice of bundling data and methods that operate on that data into a single unit (class) while restricting direct access to some of the object's components. This protects the integrity of the data and prevents uncontrolled access and misuse.
Example:
public class BankAccount
{
// Private fields - Data hiding
private decimal _balance;
private string _accountNumber;
private readonly List<Transaction> _transactions;
// Public properties with controlled access
public string AccountHolder { get; private set; }
public decimal Balance
{
get { return _balance; }
private set
{
if (value < 0)
throw new
InvalidOperationException("Balance cannot be negative");
_balance = value;
}
}
// Constructor
public BankAccount(string accountHolder, string accountNumber)
{
AccountHolder = accountHolder;
_accountNumber = accountNumber;
_balance = 0;
_transactions = new List<Transaction>();
}
// Public methods that control how the data can be accessed/modified
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Deposit amount must be positive");
Balance += amount;
_transactions.Add(new Transaction(amount, "Deposit"));
}
public bool Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Withdrawal amount must be positive");
if (_balance < amount)
return false;
Balance -= amount;
_transactions.Add(new Transaction(-amount, "Withdrawal"));
return true;
}
public IReadOnlyList<Transaction> GetTransactionHistory()
{
return _transactions.AsReadOnly();
}
}
In this example, the BankAccount
class encapsulates the account data. Direct manipulation of the balance is prevented, and it can only be changed through well-defined methods like Deposit
and Withdraw
, which include validation. This ensures that the balance is never negative and that transactions are properly recorded.
Inheritance
Inheritance allows a class to inherit properties, methods, and events from another class, establishing an "is-a" relationship. This promotes code reusability and creates a hierarchy of classes.
Example:
public abstract class Entity
{
public Guid Id { get; private set; }
public DateTime CreatedAt { get; private set; }
public DateTime? UpdatedAt { get; private set; }
protected Entity()
{
Id = Guid.NewGuid();
CreatedAt = DateTime.UtcNow;
}
protected virtual void Update()
{
UpdatedAt = DateTime.UtcNow;
}
public abstract void Validate();
}
public class Product : Entity
{
public string Name { get; private set; }
public decimal Price { get; private set; }
public int Stock { get; private set; }
public Product(string name, decimal price, int stock)
{
Name = name;
Price = price;
Stock = stock;
}
public void UpdatePrice(decimal newPrice)
{
Price = newPrice;
base.Update();
}
public override void Validate()
{
if (string.IsNullOrEmpty(Name))
throw new DomainException("Name cannot be empty");
if (Price < 0)
throw new DomainException("Price cannot be negative");
if (Stock < 0)
throw new DomainException("Stock cannot be negative");
}
}
In this example, the Product
class inherits from the Entity
class, gaining properties like Id
, CreatedAt
, and UpdatedAt
, as well as the functionality to automatically track when entities are updated. This avoids code duplication and ensures that all entities in the system have consistent behavior for these common properties.
Polymorphism
Polymorphism allows different objects to respond to the same method call in different ways. It enables a single interface to represent different underlying forms (data types).
Example:
public abstract class BankAccount
{
protected decimal Balance { get; set; }
public string AccountNumber { get; set; }
public virtual void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Amount must be positive");
Balance += amount;
}
public abstract void CalculateInterest();
}
public class SavingsAccount : BankAccount
{
private const decimal InterestRate = 0.05m;
public override void CalculateInterest()
{
decimal interest = Balance * InterestRate;
Balance += interest;
}
// Additional savings-specific behavior
public void ApplyMinimumBalanceFee()
{
if (Balance < 100)
Balance -= 5;
}
}
public class CheckingAccount : BankAccount
{
private const decimal InterestRate = 0.01m;
public override void CalculateInterest()
{
decimal interest = Balance * InterestRate;
Balance += interest;
}
// Additional checking-specific behavior
public void DeductMaintenanceFee()
{
Balance -= 2.5m;
}
}
The polymorphic behavior is demonstrated in the way different account types calculate interest:
List<BankAccount> accounts = new List<BankAccount>
{
new SavingsAccount { AccountNumber = "SAV-001" },
new CheckingAccount { AccountNumber = "CHK-001" }
};
// Polymorphic behavior
foreach (var account in accounts)
{
account.Deposit(1000);
account.CalculateInterest(); // Same method call, different behavior
}
Polymorphism allows us to work with different account types through a common interface, with each implementing the CalculateInterest
method according to its specific rules.
Abstraction
Abstraction involves hiding complex implementation details and showing only the essential features of an object. It focuses on what an object does rather than how it does it.
Example:
public interface IMusicPlayer
{
void Play();
void Pause();
void Stop();
void SetVolume(int level);
}
public class SpotifyPlayer : IMusicPlayer
{
private readonly SpotifyAPI _spotifyApi;
private bool _isPlaying;
private int _currentVolume;
public SpotifyPlayer()
{
_spotifyApi = new SpotifyAPI();
_isPlaying = false;
_currentVolume = 50;
}
public void Play()
{
_spotifyApi.InitializeStream();
_spotifyApi.BufferAudio();
_spotifyApi.StartPlayback();
_isPlaying = true;
}
public void Pause()
{
_spotifyApi.SuspendPlayback();
_isPlaying = false;
}
public void Stop()
{
_spotifyApi.EndStream();
_spotifyApi.ClearBuffer();
_isPlaying = false;
}
public void SetVolume(int level)
{
if (level < 0 || level > 100)
throw new ArgumentException("Volume must be between 0 and 100");
_currentVolume = level;
_spotifyApi.AdjustVolume(level);
}
}
Here, the IMusicPlayer
interface abstracts the concept of a music player to its essential operations without exposing the complex details of how audio streaming works. Users of this interface can simply call Play()
without needing to understand the underlying streaming protocols, audio buffering, or codec handling that might be involved.
Conclusion
Understanding the four pillars of OOP—encapsulation, inheritance, polymorphism, and abstraction—along with access modifiers and key OOP-related keywords in C# is fundamental for developing robust, maintainable software. These concepts not only help in writing clean code but also in designing systems that can adapt and evolve over time.
By mastering these principles, C# developers can create code that is more modular, reusable, and easier to maintain. Whether you're building small applications or large enterprise systems, these object-oriented concepts provide the foundation for effective software development in C#.