Object-Oriented Programming Simplified With C# And .Net 5

Introduction

This article is all about Object Oriented Programming (OOP) using C# as programming language and .Net 5 as framework. In OOP, real world representation of entities is made possible with the help of classes and objects. It also provides a well-structured modular way to create programs where it is possible to hide the implementation details. Here you will get a complete idea of all general concepts of OOP with examples.

Contents Covered in the Article

  1. Object Oriented Programming
  2. General Concepts
  3. Encapsulation
  4. Abstraction
    1. Abstraction Without Abstract Classes and Interfaces
    2. Abstraction using Abstract Classes
    3. Abstraction using Interfaces
  5. Polymorphism
    1. Static or Compile-Time Polymorphism
    2. Dynamic or Run-Time Polymorphism
    3. Method Hiding
  6. Relationship
    1. Association
    2. Aggregation
    3. Composition

Object Oriented Programming

It’s a paradigm which allows to implement the application representing the real world entities where class represents the blueprint of the entity and object represents its instance. Each class can have many instances.

For example, a person is a class with some characteristics like name, height, speed, etc. where he can perform certain tasks like run, draw, exercise, etc. and John with height 6 feet is an object (instance of class) that can run at 15 km per hour.

Following are four major concepts of OOP.

  • Inheritance to make the classes reusable
  • Polymorphism to implement different behaviors for different entities
  • Encapsulation for binding data and code together to restrict direct access to data, and allow only functions to change the state of data
  • Abstraction for hiding of unnecessary details

General Concepts

As C# is an Object Oriented Programming language, where core component is class. Class can have variables, functions, access modifiers etc. to achieve the concepts of OOP. A class can have several objects containing real data, each object occupy space in memory. Some basic terminologies better to understand before going to actual examples:

Class

It’s a blueprint, logical representation of real world entities to define state and behaviors but contains no data within i.e. person is a class.

Object

Instance of class, contains the data related to real world entities in variables (properties) and functions to manage that data. It occupies space in memory. John and Sam represents the objects of class Person.

Data type

Defines the type of data that a property can contain e.g. string for text, int for integer numbers etc.

Variable

Used to store data related to entities.

Constructor

An automatically called function at creation of object in order to initialize the variable of class. It cannot return anything.

Function

A way to store, modify, see or delete the data stored in variable, and represents the behaviors of object e.g. person can run at certain speed, do exercise etc.

Access Modifier

These are some keywords used for accessibility scope or visibility definition

  • Public: Accessible in the implemented and referenced assemblies.
  • Private: Only accessible in implemented class
  • Protected: Accessible in implemented and derived class
  • Internal: Accessible in implemented Assembly
  • InternalProtected: Accessible in implemented Assembly and in derived classes.

Default access modifier for class is internal and its members is private in C#.

Encapsulation

It helps in wrapping data and code together and can prevent unwanted access from code outside the class. To achieve encapsulation, access modifiers are used to define the accessibility scope of properties and functions. Class encapsulates the data and function members in it, and can also expose necessary implementation to be code outside. In this way, the data stored in particular object is protected from modification. E.g. an Employee class has some properties of employee where external code can only access the functions to see the values or update them. Access modifier is used for restricting access.

class Employee {
    //Encapsulation started by restricting properties to be private
    private string _name;
    private double _salary;
    private string _designation;
    public Employee(string name, double salary, string designation) {
        _name = name;
        _salary = salary;
        _designation = designation;
    }
    //Updated the values of private properties using public function
    public bool UpdateEmployeeDetails(string newDesignation, double newSalary) {
        _designation = newDesignation;
        _salary = newSalary;
        return true;
    }
    //Access and show details of employee
    public void EmployeeDetails() {
        Console.WriteLine($ "{_name} is a {_designation} and getting {_salary} salary ");
    }
}
Class Program {
    static void Main(string[] args) {
        Employee john = new("John", 10000, "Software Engineer");
        Employee sam = new("Sam", 12000, "Senior Software Engineer");
        john.EmployeeDetails();
        sam.EmployeeDetails();
        john.UpdateEmployeeDetails("Senior Software Engineer", 12000);
        john.EmployeeDetails();
    }
}

Abstraction

To achieve abstraction, encapsulation must be implemented. To not let the program access data directly is known as data hiding. Abstraction is all about not including the internal implementation and exposing only the necessary details.

Abstract classes and interfaces are also used in achieving the abstraction as both mentioned types only expose the essential information and hide the background details related to actual implementation. Virtual or Abstract methods cannot be private.

For example, if a class has a function to register a new user where email validation, credential validation, store to database and send email are the functions being used but not exposed for direct access, the abstraction is achieved.

Abstraction without Abstract Class or Interfaces

All the unnecessary details are hidden and only essential details are exposed to external code for access which means abstraction is achieved. E.g. an employee class has exposed a function to update the details of employee and have an internal function to validate the new details received, here external code can only access the function essential to perform the task and all other details are hidden inside the class.

class Employee {
    //Encapsulation started by restricting properties to be private
    private string _name;
    private double _salary;
    private string _designation;
    public Employee(string name, double salary, string designation) {
        _name = name;
        _salary = salary;
        _designation = designation;
    }
    //Updated the values of private properties using public function
    public bool UpdateEmployeeDetails(string newDesignation, double newSalary) {
        ValidateEmployeeDetails(newDesignation, newSalary);
        _designation = newDesignation;
        _salary = newSalary;
        ShowUpdatedEmployeeDetails();
        return true;
    }
    //internal method to validate details of employee
    private bool ValidateEmployeeDetails(string designation, double salary) => true;
    //internal method to show updated details
    private void ShowUpdatedEmployeeDetails() {
        Console.WriteLine($ "{_name} is {_designation} and new salary: {_salary} ");
    }
    //Access and show details of employee
    public void EmployeeDetails() {
        Console.WriteLine($ "{_name} is a {_designation} and has {_salary} salary ");
    }
}
Class Program {
    static void Main(string[] args) {
        //Abstraction Examples without Abstract class or Interface
        Console.WriteLine("\n\nAbstraction Examples without Abstract class or
            Interface\ n\ n ");
            Employee john = new("John", 10000, "Software Engineer"); john.EmployeeDetails(); john.UpdateEmployeeDetails("Senior Software Engineer", 12000);
        }
    }

Abstraction using Abstract Classes

Abstract class is used to define the prototype of functions required by multiple classes to implement. It provides a way for external code to access the functionality using abstract classes. All abstract methods are mandatory for implementing classes to implement by using override keyword. Abstract classes can have non-abstract methods and properties. Access specifiers can be used in abstract classes. E.g. there are two types of users Admin and Member. Both have some similar functionality and some other required functions also. Both RegisterUser() and LoginUser() functions can be accessed without showing the other internal functions like ValidateUserDetails() and ValidateCredentials(). And Abstract class can contain instances of class extending it.

abstract class ApplicationUser {
    public abstract bool RegisterUser(object userDetails);
    public abstract string LoginUser(string username, string password);
}
class Admin: ApplicationUser {
    public override string LoginUser(string username, string password)
        // validate username and password
        // generate token
        => "token";
    Public override bool RegisterUser(object userDetails)
        //Validate user details
        //Store user details to database
        => ValidateUserDetails() ? true : false;
    private bool ValidateUserDetails()
        // validate username and password
        => true;
    public bool DeleteUserAccount(string username)
        // Delete user account from database
        => true;
}
class Member: ApplicationUser {
    public override string LoginUser(string username, string password)
        // generate token
        => ValidateCredentials() ? "token" : "Login Failed";
    public override bool RegisterUser(object userDetails)
        //Validate user details
        //Store user details to database
        => true;
    private bool ValidateCredentials()
        // validate username and password
        => true;
}
class Program {
    static void Main(string[] args) {
        //Abstraction Using Abstract Classes
        Console.WriteLine("\n\nAbstraction Example using Abstract class\n\n");
        ApplicationUser abstractAdmin = new Admin();
        ApplicationUser abstractMember = new Member();
        abstractAdmin.RegisterUser("Add new user here");
        abstractAdmin.LoginUser("Username", "Password");
        abstractMember.RegisterUser("Add new user here");
        abstractMember.LoginUser("Username", "Password");
    }
}

Abstraction using Interfaces

Interfaces are contracts signed by implementing classes, all members except the default members are mandatory for implementing classes to implement. All the properties and methods defined inside interface have public access modifier. Interface also helps in achieving the abstraction by only showing the function definition without including the background implementation of the functions. E.g. IAplicationUser interface can be used to contain the instance of the classes Admin and Member and make it easy to access the implemented functions in classes without showing the unnecessary details.

interface IApplicationUser {
    bool RegisterUser(object userDetails);
    string LoginUser(string username, string password);
}
class Admin: IApplicationUser {
    public string LoginUser(string username, string password)
        // validate username and password
        // generate token
        => "token";
    public bool RegisterUser(object userDetails)
        //Validate user details
        //Store user details to database
        => ValidateUserDetails() ? true : false;
    private bool ValidateUserDetails()
        // validate username and password
        => true;
    public bool DeleteUserAccount(string username)
        // Delete user account from database
        => true;
}
class Member: IApplicationUser {
    public string LoginUser(string username, string password)
        // generate token
        => ValidateCredentials() ? "token" : "Login Failed";
    public bool RegisterUser(object userDetails)
        //Validate user details
        //Store user details to database
        => true;
    private bool ValidateCredentials()
        // validate username and password
        => true;
}
class Program {
    static void Main(string[] args) {
        //Abstraction Using Abstract Classes
        Console.WriteLine("\n\nAbstraction Example using Abstract class\n\n");
        ApplicationUser abstractAdmin = new Admin();
        ApplicationUser abstractMember = new Member();
        abstractAdmin.RegisterUser("Add new user here");
        abstractAdmin.LoginUser("Username", "Password");
        abstractMember.RegisterUser("Add new user here");
        abstractMember.LoginUser("Username", "Password");
    }
}

Inheritance

It’s a process of sharing properties and methods of one class with other classes. It’s an “Is Kind of” relationship, which allows to reuse the code instead of writing it in all the classes. To achieve inheritance, parent-child relationship is created using base and derived class (class extending base class). Common properties and functions reside in base class, derived classes extend it by adding new properties, member functions and can override the existing functionality also.

Consider an example of Student, Teacher and Accountant where all three entities are persons, so inheriting all class from a base class Person can help in avoiding the writing of duplicate code in all the classes. Student is a kind of person.

Base class represents Generalization where derived classes represents specialization. Multiple inheritance is not supported in .net framework but multi-level inheritance is.

class Person {
    public string Name {
        get;
        private set;
    }
    public int Age {
        get;
        private set;
    }
    public string Address {
        get;
        private set;
    }
    public Person(string name, int age, string address) {
        Name = name;
        Age = age;
        Address = address;
    }
}
class Student: Person {
    private double _fee;
    public Student(string name, int age, string address, double salary): base(name, age, address) {
        _fee = salary;
    }
    public void ShowFee() {
        Console.WriteLine($ "Salary of Teacher is {_fee}");
    }
}
class Teacher: Person {
    private double _salary;
    private List < string > _course;
    public Teacher(string name, int age, string address, double salary, List < string > course): base(name, age, address) {
        _salary = salary;
        _course = course;
    }
    public void ShowSalary() {
        Console.WriteLine($ "Salary of Teacher is {_salary}");
    }
    public void ShowCourses() {
        Console.WriteLine("List of Courses");
        _course.ForEach(x => Console.WriteLine(x));
    }
}
class Accountant: Person {
    private double _salary;
    public Accountant(string name, int age, string address, double salary): base(name, age, address) {
        _salary = salary;
    }
    public void ShowSalary() {
        Console.WriteLine($ "Salary of Teacher is {_salary}");
    }
    public void ManagePayRoll() {
        Console.WriteLine("Payrolls are managed by accountant");
    }
}
class Program {
    static void Main(string[] args) {
        //Inheritance Example
        Console.WriteLine("\n\nExample of Inheritance\n\n");
        Student student = new("Sam", 24, "Rawalpindi", 10000);
        Teacher teacher = new("John", 24, "Rawalpindi", 50000, new List < string > {
            "Computer",
            "DevOps"
        });
        Accountant accountant = new("Eddie", 24, "Rawalpindi", 90000);
        student.ShowFee();
        teacher.ShowCourses();
        accountant.ManagePayRoll();
    }
}

Polymorphism

Many is the meaning of Poly and Form for morph, it defines the ability of a particular function to behave differently in different instances. There are two types of polymorphism: static and dynamic or compile-time and run-time polymorphism. E.g. rectangle, triangle and circle are all shapes.

Static or Compile-Time Polymorphism

When there are multiple functions with same names having different parameters or return type, known as function overloading. Compiler at compile time can justify the call to particular function, this process is called as early binding. It is not compulsory for return type to be different. To achieve static polymorphism function overloading must be implement in a class.

class FunctionOverloading {
    private string _address;
    public string SetAddress(string country) {
        _address = country;
        return "Address added Successfully";
    }
    public bool SetAddress(string state, string country) {
        _address = state + " " + country;
        return true;
    }
    public void SetAddress(string city, string state, string country) {
        _address = city + " " + state + " " + country;
        Console.WriteLine("Address added Successfully");
    }
}
class Program {
    static void Main(string[] args) {
        //Static or Compile-Time Polymorphism
        Console.WriteLine("\n\nExample of Static or Compile-Time Polymorphism \n\n");
        FunctionOverloading
        function = new();
        Console.WriteLine(function.SetAddress("Pakistan"));
        Console.WriteLine(function.SetAddress("Punjab", "Pakistan"));

        function.SetAddress("Rawalpindi", "Punjab", "Pakistan");
    }
}

Dynamic or Run-Time Polymorphism

To achieve dynamic polymorphism, inheritance is a must with exactly similar function prototype in both Base and Derived class. The different implementation of a method from base class in derived class with same function name, parameters and return type is known as function over-riding. Compiler at run-time justifies the call to actual function, this process is called late binding. E.g. Carbon Foot Print or CO2 emission is different for each type of vehicle, so using polymorphism concepts that is function overriding different vehicle classes can have their own implementation for CO2 emission calculation.

class Vehicle {
    private
    const double _co2Factor = 10;
    //Carbon Foot Print Calculation for General Vehicle
    public virtual double CarbonFootPrint(double mileage) => mileage * _co2Factor;
}
class DieselVehicle: Vehicle {
    private
    const double _co2FactorForDiesel = 20;
    //Carbon Foot Print Calculation for Diesel Vehicle
    public override double CarbonFootPrint(double mileage) => mileage * _co2FactorForDiesel;
}
class HybridVehicle: Vehicle {
    private
    const double _co2FactorForHybrid = 15;
    //Carbon Foot Print Calculation for Hybrid Vehicle
    public override double CarbonFootPrint(double mileage) => mileage * _co2FactorForElectric;
}
class ElectricVehicle: Vehicle {
    private
    const double _co2FactorForElectric = 5;
    //Carbon Foot Print Calculation for Electric Vehicle
    public override double CarbonFootPrint(double mileage) => mileage * _co2FactorForHybrid;
}
class Program {
    static void Main(string[] args) {
        //Dynamic or Run-Time Polymorphism
        Console.WriteLine("\n\nExample of Dynamic or Run-Time Polymorphism \n\n");
        Vehicle vehicle = new();
        Vehicle dieselVehicle = new DieselVehicle();
        Vehicle hybridVehicle = new HybridVehicle();
        Vehicle electricVehicle = new ElectricVehicle();
        Console.WriteLine("CarbonFootPrint is: " + vehicle.CarbonFootPrint(1000));
        Console.WriteLine("CarbonFootPrint of Diesel Vehicle is: " + dieselVehicle.CarbonFootPrint(1000));
        Console.WriteLine("CarbonFootPrint Hybrid Vehicle is: " + hybridVehicle.CarbonFootPrint(1000));
        Console.WriteLine("CarbonFootPrint Electric Vehicle is: " + electricVehicle.CarbonFootPrint(1000));
    }
}

Method Hiding

Re-implementation of a function in derived class similar to base class without overriding the function using new keyword hides the implementation of base class method. New keyword is used to avoid the warning of not overriding the base class method.

class Vehicle {
    private
    const double _co2Factor = 10;
    //Virtual method
    public virtual double CarbonFootPrint(double mileage) => mileage * _co2Factor;
}
class Van: Vehicle {
    private
    const double _co2FactorLitreWise = 12;
    //Method hiding using new keyword
    public new void CarbonFootPrint(double fuelComsumedInLitres) {
        Console.WriteLine(fuelComsumedInLitres * _co2FactorLitreWise);
    }
}
class Program {
    static void Main(string[] args) {
        //Method Hiding
        Console.WriteLine("\n\nExample of Method Hiding\n\n");
        Vehicle van = new Van();
        Van.CarbonFootPrint(50);
    }
}

Relationship

As in OOP, there are several classes and objects to solve a particular real-life problem. So, with multiple classes a need of communication between classes will arise, and to solve that need there must be defined relationship between classes through which the objects of each class can interact with other. Following are three ways for an object to interact with another:

Association

The simplest way of communication between two or more objects where each object can exist independently and no one is owning the other. It is also used to show multiplicity when two objects are communicating like:

  • One to One
  • One to Many
  • Many to One
  • Many to Many

For example, every teacher can have one or more students or every student can learn from one or more teachers.

It defines the usage object from another class object. Association can be unidirectional or bidirectional.

Association also have special cases: Aggregation and Composition with a difference of existence of objects.

Aggregation

It defines a relationship with direct association but objects can have their life cycle independent of one another. E.g. a Department can have many employees but not vice versa but both can exist independently. So, if you delete the department, employee can still exist. Weak association is known as Aggregation.

Composition

Strong Aggregation is called Composition. Here one object life cycle is dependent on another, represents the Part-of relationship. If containing object is disposed then contained object is also disposed. E.g. School contains of multiple class room, class room is part of school. But here school can exist without a particular class room but not vice versa. If you delete the school class room no longer exists.

Summary

In this article, I have discussed the Object-oriented Programming paradigm with some practical examples. Some of the core concepts like encapsulation, abstraction, polymorphism and inheritance etc. to give just an idea of how things work. I have also discussed the possible communication between objects of different classes using Association, Aggregation and Composition. Hope you have liked it, thanks for reading so far. If there’s anything I can improve on do let me know in comments section.