Advanced C# OOP Examples: Encapsulation, Inheritance, Polymorphism, Abstraction
Encapsulation
Encapsulation means hiding the internal state and implementation details of an object, and requiring all interaction to occur through a public interfacelearn.microsoft.com. In practice, this allows a class library to expose a simple API while keeping complex logic and data hidden internallylinkedin.com. The example below shows an AppSettings class that encapsulates configuration data. It uses a private data store and only exposes a controlled method to retrieve settings, preventing external code from manipulating internal data directly.
using System;
using System.Collections.Generic;
// Encapsulation example: internal config data hidden behind a public API
public class AppSettings
{
// Private field that stores settings (hidden from outside access)
private readonly Lazy<Dictionary<string, string>> _settings;
public AppSettings()
{
// Lazy initialization ensures thread-safe one-time loading of config data
_settings = new Lazy<Dictionary<string, string>>(LoadSettings);
}
// Loads settings from a configuration source (e.g., file, env variables)
private Dictionary<string, string> LoadSettings()
{
// In a real library, read from file or environment. Here we simulate:
return new Dictionary<string, string>
{
{ "ConnectionString", "Server=SQL01;Database=AppDB;Trusted_Connection=True;" },
{ "ApiKey", "12345-ABCDE" }
};
}
// Public method provides controlled access to configuration values
public string Get(string key)
{
if (_settings.Value.TryGetValue(key, out string value))
{
return value; // Return the config value if present
}
throw new KeyNotFoundException($"Key '{key}' not found in configuration.");
}
}
// Usage example (client code):
// AppSettings config = new AppSettings();
// string apiKey = config.Get("ApiKey"); // retrieve config value safely
In the above code, the AppSettings class encapsulates its configuration dictionary in a private field. External code can only access settings via the Get method, which enforces read-only, validated access. This hides the complexity of loading and storing configuration data behind a simple API, exemplifying encapsulation.
Inheritance
Inheritance is the ability to create a new class (derived class) based on an existing class (base class), thereby reusing and extending its functionalitymedium.com. This promotes code reuse and establishes a hierarchy of classes. In the following example, an abstract Logger base class defines common logging behavior, and two subclasses FileLogger and ConsoleLogger inherit from it. They reuse the base functionality (like message formatting) and provide specialized implementations for the abstract Log method.
using System;
using System.IO;
// Base class defining common logging behavior (message format, severity levels)
public abstract class Logger
{
public string ComponentName { get; } // e.g., which component/module is logging
protected Logger(string componentName = "General")
{
ComponentName = componentName;
}
public void LogInfo(string message)
{
// Common functionality: format message with severity and component
Log($"INFO [{ComponentName}]: {message}");
}
public void LogError(string message)
{
Log($"ERROR [{ComponentName}]: {message}");
}
// Abstract method to be implemented by subclasses for the actual logging output
protected abstract void Log(string formattedMessage);
}
// Derived class that inherits Logger and implements logging to a file
public class FileLogger : Logger
{
private readonly string _filePath;
public FileLogger(string filePath, string componentName = "General")
: base(componentName)
{
_filePath = filePath;
}
protected override void Log(string formattedMessage)
{
// Write the formatted message to a log file (append to file)
File.AppendAllText(_filePath, formattedMessage + Environment.NewLine);
}
}
// Derived class that inherits Logger and implements logging to the console
public class ConsoleLogger : Logger
{
public ConsoleLogger(string componentName = "General") : base(componentName) { }
protected override void Log(string formattedMessage)
{
// Write the formatted message to the console
Console.WriteLine(formattedMessage);
}
}
// Usage example:
Logger logger = new FileLogger("logs/app.log", componentName: "DataAccess");
logger.LogInfo("Initialized data connection."); // Uses FileLogger's implementation
logger = new ConsoleLogger("UI");
logger.LogError("Null reference encountered."); // Uses ConsoleLogger's implementation
In this example, FileLogger and ConsoleLogger derive from the base Logger class. They inherit the common methods LogInfo/LogError (which format messages) and override the abstract Log method to output to different targets. This demonstrates inheritance by reusing base class logic and extending it in subclasses. Adding a new logger (e.g., a DatabaseLogger) would be easy and would reuse the existing logging framework, showing the power of inheritance for code reuse and extension.
Polymorphism
Polymorphism allows objects of different classes to be treated through the same interface, with each object responding in its own waycodeguru.com. In other words, a single code routine can work with any subclass or interface implementation, and the appropriate overridden method executes based on the runtime type. The example below implements a strategy pattern for compression: an ICompressionStrategy interface defines a Compress method, and concrete strategies (GZipCompression, BZip2Compression) implement it differently. An Archiver class can use any strategy interchangeably via the interface.
using System.IO;
using System.IO.Compression;
// Polymorphism example: different compression strategies share the same interface
public interface ICompressionStrategy
{
byte[] Compress(byte[] data);
}
// One implementation of the strategy using GZip compression (from System.IO.Compression)
public class GZipCompression : ICompressionStrategy
{
public byte[] Compress(byte[] data)
{
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, CompressionLevel.Optimal))
{
gzip.Write(data, 0, data.Length);
}
// Return compressed bytes (GZip format)
return output.ToArray();
}
}
// Another implementation, e.g., BZip2 compression (using a hypothetical library or custom algorithm)
public class BZip2Compression : ICompressionStrategy
{
public byte[] Compress(byte[] data)
{
// In a real scenario, integrate BZip2 compression. Here we'll simulate:
return SimulateBZip2Compression(data);
}
private byte[] SimulateBZip2Compression(byte[] data)
{
// Placeholder for actual BZip2 compression logic
// For demo, just return the original data or some transformation
return data;
}
}
// The Archiver uses a compression strategy, but is agnostic to which one.
public class Archiver
{
private ICompressionStrategy _compression;
public Archiver(ICompressionStrategy compression)
{
_compression = compression;
}
public void SetCompressionStrategy(ICompressionStrategy compression)
{
_compression = compression;
}
public byte[] CreateArchive(byte[] content)
{
// Delegate to the currently set compression strategy.
// Actual Compress() method called depends on the runtime type of _compression.
return _compression.Compress(content);
}
}
// Usage example:
byte[] data = File.ReadAllBytes("docs/report.pdf");
Archiver archiver = new Archiver(new GZipCompression());
byte[] archive1 = archiver.CreateArchive(data); // compresses using GZip
archiver.SetCompressionStrategy(new BZip2Compression());
byte[] archive2 = archiver.CreateArchive(data); // now compresses using BZip2 (different behavior)
Here, the Archiver class is written against the interface ICompressionStrategy, not a specific implementation. At runtime, you can supply any compression strategy (GZip, BZip2, etc.) to Archiver. Calling CreateArchive will invoke the Compress method of the concrete strategy in use, demonstrating polymorphism – the Archiver code doesn’t need to change to accommodate new compression algorithms. The same interface (Compress) is implemented in multiple forms, and the selection of which form to execute is determined by the object’s actual type at runtime.
Abstraction
Abstraction involves modeling a complex system with simplified, high-level constructs while hiding unnecessary detailslearn.microsoft.com. In practice, abstraction lets you define an abstract interface or base class to represent a general concept, so that developers can use it without needing to understand the underlying complexitylinkedin.com. The following example defines an abstract base class NotificationService that provides a template for sending notifications. It encapsulates the general steps (formatting a message and sending it), while concrete subclasses (EmailNotificationService, SmsNotificationService) implement the details for specific channels.
using System;
using System.Net.Mail; // for SmtpClient in email sending example
// Abstract base class defining a generic notification service
public abstract class NotificationService
{
// Public method providing a high-level operation for sending a notification
public void Notify(User recipient, string message)
{
string formatted = FormatMessage(recipient, message);
// Delegate the actual send to the subclass implementation
Send(formatted, recipient);
}
// A hook for formatting the message; default behavior can be overridden by subclasses if needed
protected virtual string FormatMessage(User user, string message)
{
return $"Dear {user.Name}, {message}";
}
// Abstract method to send the message, implementation is deferred to subclasses
protected abstract void Send(string message, User recipient);
}
// A simple User class to represent notification recipients
public class User
{
public string Name { get; }
public string Email { get; }
public string Phone { get; }
public User(string name, string email, string phone)
{
Name = name;
Email = email;
Phone = phone;
}
}
// Concrete implementation of NotificationService for email notifications
public class EmailNotificationService : NotificationService
{
private readonly SmtpClient _smtpClient;
public EmailNotificationService(SmtpClient smtpClient)
{
_smtpClient = smtpClient; // dependency injection of an email client
}
protected override void Send(string message, User recipient)
{
// Use SMTP to send an email (simplified for example)
_smtpClient.Send("no-reply@myapp.com", recipient.Email, "Notification", message);
}
}
// Concrete implementation of NotificationService for SMS notifications
public class SmsNotificationService : NotificationService
{
// Override format to simplify message for SMS (e.g., no greeting, shorter text)
protected override string FormatMessage(User user, string message)
{
return message;
}
protected override void Send(string message, User recipient)
{
// Use an SMS gateway API to send the text (pseudo-code for demo)
SmsGateway.Send(recipient.Phone, message);
}
}
// Usage example:
User user = new User("Alice", "alice@example.com", "+1234567890");
NotificationService notifier = new EmailNotificationService(new SmtpClient("smtp.myapp.com"));
notifier.Notify(user, "Your order has been shipped."); // Email sent
notifier = new SmsNotificationService();
notifier.Notify(user, "Your OTP code is 123456"); // SMS sent
In this code, NotificationService defines an abstraction for sending notifications. The caller simply uses Notify without worrying about how the message is delivered. The details are abstracted away in the Send implementations of EmailNotificationService and SmsNotificationService. This design allows developers to focus on the high-level action (notifying a user) rather than the complex details of SMTP or SMS gateway integration. By providing a common abstract base, the system is easily extensible (new notification methods can be added by subclassing) and the complexity is kept hidden behind a clean, high-level interface, which is the essence of abstraction in OOP.
Sources: Encapsulationlearn.microsoft.comlinkedin.com, Inheritancemedium.com, Polymorphismcodeguru.com, Abstractionlearn.microsoft.comlinkedin.com.
Citations
Inheritance in C#. One of the core features of… | by Joshua Eze | Medium
https://medium.com/@ezejoshuac/inheritance-in-c-67fd567bc43b
All Sources





