From Concepts to Code: Mastering OOP in C#

Vildana Šuta
7 min readMay 20, 2024

As humans, we tend to put everything into some sort of a box. We need categories and blueprints to understand and organize everything in our heads. Often a lot of confusion arises when we try to manage complex systems without clear structures. This is where, in the software engineering world, object-oriented programming steps in.

In the digital world of programming, OOP provides those essential boxes and blueprints. It allows developers to create ‘objects’ that represent real-world entities or concepts, like a person, a car, or a bank account. Each object has its own set of characteristics (called ‘attributes’) and actions it can perform (called ‘methods’).

By organizing code into these objects, developers can better manage complexity, improve reusability, and enhance maintainability. Need a new type of object? Just create a new class blueprint. Want to change how something works? Update the corresponding class without affecting the rest of the codebase.

In essence, OOP mirrors the way our minds naturally categorize and understand the world, making it an essential paradigm in software development, especially in languages like C#. It’s the tool that helps us tame the digital chaos and build robust, scalable systems with ease.

Classes and objects

Imagine you are making an app to enhance your organization with work obligations. In your workload management app, you’ll need to represent different types of tasks, such as meetings, deadlines, and to-do items. We use classes to define blueprints for these different types of tasks.

For example, our class might look something like this:

public class Task
{
public int TaskId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public int Priority { get; set; }

public void MarkAsCompleted(int taskId)
{
// Mark task as completed
}

public void UpdateDetails(int taskId, string newTitle, string newDescription, int newPriority)
{
// Update task details
}

public void SetReminder(int taskId, DateTime reminderTime)
{
// Set reminder for the task
}
}

It’d have attributes such as Title, Description, and Priority so we can define the details for each task object we create. Also, it’d include some methods such as MarkAsCompleted, UpdateDetails, or SetReminder that can be defined once in the class and used as many times as we want. Essentially, classes represent blueprints for objects we want to create. We can create as many tasks as we want just by using the mentioned class and we can manipulate already created tasks to mark them completed, update their details, or set reminders using the defined methods.

Inheritance

Since the task is a broad concept and it can represent anything from meetings to deadlines, we can use the inheritance principle to build upon our base Task class and create more specialized classes for specific types of tasks. For example, let’s create a Meeting class that inherits from the Task class. This means that a Meeting is a type of Task but with additional attributes and behaviors specific to meetings.

public class Meeting : Task
{
public List<string> Attendees { get; set; }
public string Location { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }

// Additional methods specific to meetings can be added here
}

With this inheritance, the Meeting class automatically inherits all the attributes and methods from the Task class, such as Title, Description, and MarkAsCompleted. We can then add new attributes like Attendees, Location, StartTime, and EndTime, along with any additional methods specific to meetings.

Polymorphism

Polymorphism is like having multiple forms or shapes. In the context of our workload management app, it means that even though we have different types of tasks (Meetings, Deadlines, etc.), we can treat them all as instances of the base Task class. This allows us to write code that can work with any type of task, regardless of its specific implementation details.

For example, let’s say we have a method in our app called “PrintTaskDetails” that prints out the details of a task. Instead of having separate methods for printing Meeting details, Deadline details, and so on, we can have just one method that takes a Task object as input and prints its details.

public void PrintTaskDetails(Task task)
{
Console.WriteLine($"Title: {task.Title}");
Console.WriteLine($"Description: {task.Description}");
// Print other common task details

// Polymorphism in action:
// If 'task' is a Meeting object, it will call the Meeting-specific version of GetMeetingDetails()
// If 'task' is a Deadline object, it will call the Deadline-specific version of GetDeadlineDetails()
task.GetTaskDetails();
}

Now, let’s see how polymorphism works with our Meeting and Deadline classes:

public class Meeting : Task
{
public List<string> Attendees { get; set; }
public string Location { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }

public void GetTaskDetails()
{
// Print meeting-specific details
Console.WriteLine($"Location: {Location}");
Console.WriteLine($"Start Time: {StartTime}");
Console.WriteLine($"End Time: {EndTime}");
// Print other meeting-specific details
}
}

public class Deadline : Task
{
public DateTime DueDate { get; set; }
public bool IsCompleted { get; set; }

public void GetTaskDetails()
{
// Print deadline-specific details
Console.WriteLine($"Due Date: {DueDate}");
Console.WriteLine($"Is Completed: {IsCompleted}");
// Print other deadline-specific details
}
}

Now, when we call the “PrintTaskDetails” method and pass in a Meeting object, it will automatically call the Meeting-specific version of “GetTaskDetails” method to print its details. Similarly, if we pass in a Deadline object, it will call the Deadline-specific version of “GetTaskDetails”. This is polymorphism in action: the ability of different types of objects to be treated as instances of the same base class and to exhibit different behaviors based on their specific implementations.

In essence, polymorphism allows us to write flexible and reusable code that can work with different types of tasks without needing to know their specific types at compile time.

Encapsulation

Encapsulation is like putting a protective bubble around your data and methods. It’s all about bundling data (attributes) and methods (behaviors) together within a class and controlling access to them from outside the class. This helps in keeping the internal state of an object safe from unwanted interference and manipulation.

In our workload management app, encapsulation ensures that the details of a task, such as its title, description, and due date, are only accessible and modifiable through controlled means. This prevents accidental or unauthorized changes to the task’s data, maintaining the integrity of our task objects.

public class Task
{
// Private attributes, accessible only within the Task class
private string title;
private string description;
private DateTime dueDate;
private int priority;

// Public properties to provide controlled access to the private attributes
public string Title
{
get { return title; }
set { title = value; }
}

public string Description
{
get { return description; }
set { description = value; }
}

public DateTime DueDate
{
get { return dueDate; }
set { dueDate = value; }
}

public int Priority
{
get { return priority; }
set { priority = value; }
}

// Constructor to initialize task attributes
public Task(string title, string description, DateTime dueDate, int priority)
{
Title = title;
Description = description;
DueDate = dueDate;
Priority = priority;
}

// Other methods of the Task class...
}

In this example, the attributes (title, description, dueDate, priority) of the Task class are marked as private, meaning they can only be accessed within the Task class itself. However, we provide public properties (Title, Description, DueDate, Priority) to allow controlled access to these attributes from outside the class. This way, the internal data of a task is encapsulated within the class, and any modifications to it must go through the designated properties, ensuring data integrity and security.

Abstraction

Abstraction is like looking at something from a high level without worrying about the nitty-gritty details. In the context of our task management app, abstraction allows us to focus on the essential aspects of tasks, such as their title, description, and due date, without getting bogged down in the specific implementation details of how tasks are stored or managed internally.

// Abstract class representing a generic task
public abstract class Task
{
public string Title { get; set; }
public string Description { get; set; }
public DateTime DueDate { get; set; }

// Constructor to initialize task attributes
public Task(string title, string description, DateTime dueDate)
{
Title = title;
Description = description;
DueDate = dueDate;
}

// Abstract method to be implemented by derived classes
public abstract void DisplayDetails();
}

// Concrete class representing a specific type of task: Meeting
public class Meeting : Task
{
public Meeting(string title, string description, DateTime dueDate) : base(title, description, dueDate)
{
}

public override void DisplayDetails()
{
Console.WriteLine($"Meeting - Title: {Title}, Description: {Description}, Due Date: {DueDate}");
}
}

// Concrete class representing another specific type of task: Deadline
public class Deadline : Task
{
public Deadline(string title, string description, DateTime dueDate) : base(title, description, dueDate)
{
}

public override void DisplayDetails()
{
Console.WriteLine($"Deadline - Title: {Title}, Description: {Description}, Due Date: {DueDate}");
}
}

class Program
{
static void Main(string[] args)
{
// Create a list of tasks
List<Task> tasks = new List<Task>();
tasks.Add(new Meeting("Team Meeting", "Discuss project progress", new DateTime(2024, 5, 30)));
tasks.Add(new Deadline("Submit Report", "Submit quarterly report to management", new DateTime(2024, 6, 15)));

// Display details of each task using abstraction
foreach (Task task in tasks)
{
task.DisplayDetails();
}
}
}

In the Main method, we create a list of tasks containing both meetings and deadlines, treating them uniformly as instances of the Task class. We use abstraction to interact with tasks at a high level, without worrying about their specific implementations.

Embracing object-oriented programming has truly molded me as a developer. It’s not just about coding; it’s a mindset that influences how I approach problems and craft solutions. The concepts of OOP have been transformative. They’ve instilled in me a knack for cleaner, more concise code and a focus on efficiency. For anyone starting out in software development, OOP is not just a stepping stone — it’s the foundation upon which everything else is built.

--

--

Vildana Šuta

👩‍💻 Hi, I'm Vildana! Software engineer working with .NET (C#), exploring full-stack with Angular, Flutter, React, and Vue. Coding adventures await! 🚀