• Labs icon Lab
  • Core Tech
Labs

Guided: Exploring the Pillars of Object-Oriented Programming in C#

This C# code lab is dedicated to the four pillars of Object-Oriented Programming (OOP). These key tenets are encapsulation, abstraction, inheritance, and polymorphism. Through ample exposition, you will learn and apply their uses to complete a functional ticket tracker application in C#. By the end of this lab, you will have gained practical experience with the usage of OOP to architect flexible and extensible solutions for your C# applications.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 45m
Published
Clock icon Nov 27, 2023

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Welcome to the Guided: Exploring the Pillars of Object-Oriented Programming in C# lab.

    In the realm of C# programming, Object-Oriented Programming (OOP) serves as a foundational paradigm that goes beyond mere code execution. OOP is a methodology that organizes code around the concept of objects, combining data and behavior into cohesive units. Embracing key principles like encapsulation, abstraction, inheritance, and polymorphism, C# allows developers to structure their applications in a modular and scalable manner.


    The ticket tracker you will be working with has most of its behind-the-scenes and main application logic already implemented for you. However, the Ticket class and its derived subclasses need to be implemented by you for this application to be fully functional. Until then, running the application will display numerous compilation errors. You can also check the solution directory if you are stuck or want to check your implementations at any time.

    Note: The files in the solution directory puts the solution code in the solution namespace for compilation purposes. Do not copy this namespace solution line or it will cause errors when running the application. Only take into account the code within the class block.


    You're invited to experiment and learn interactively. Make use of the Terminal to the right to execute the ticket tracker application by entering the command dotnet run --no-restore when you have completed the lab.

  2. Challenge

    Encapsulation

    Overview

    Encapsulation is the first of the four pillars of object-oriented programming. It refers to the bundling of related data (attributes) and methods (functions) that operate on this data into a single unit known as a class. It also involves usage of access modifiers such as private, public, protected, etc. to moderate interactions between the class and other objects.

    Let's take a look at the following basic example:

    public class MyClass {
    	private int testVal;
    	
    	public int getInt() {
    		return testVal;
    	}
    	
    	public void setInt(int n) {
    		testVal = n;
    	}
    	
    	public void PerformCalculation() {
    		int result = doMethod(testVal);
    		Console.WriteLine($"{result}");
    	}
    }
    

    This class contains data in the form of an int value called testVal. Because it it marked as private, it cannot be accessed and modified by another class or external action. However, testVal can still be indirectly accessed and modified externally by using the getInt(), setInt(), and PerformCalculation() methods that are marked as public in MyClass.

    In short, encapsulation promotes modularity by grouping similar data members into a class and data integrity by regulating the access and modification of its members.


    Properties

    C# provides a unique syntactic approach for this in the form of properties. Simply put, properties are essentially a public facing wrapper for private fields. To an external accessor, it may seem as though they are using the field directly even though that is not actually the case internally. The following code is equivalent to the snippet above.

    public class MyClass {
    	private int testVal;
    	
    	public int TestVal {
    		get {
    			return testVal;
    		}
    		
    		set {
    			testVal = value;
    		}
    	}
    	
    	public void PerformCalculation() {
    		int result = doMethod(testVal);
    		Console.WriteLine($"{result}");
    	}
    }
    

    In the event that a field and its property have a trivial getter and setter, such as having get { return field } and set { field = value;}, you can utilize an auto-implemented property in the following format:

    public class MyClass {
    	public int TestVal { get; set; }
    	
    	public void PerformCalculation() {
    		int result = doMethod(TestVal);
    		Console.WriteLine($"{result}");
    	}
    }
    

    Notice how much simpler this format is yet it is still equivalent to the two preceding code snippets. When using an auto-implemented property, the compiler is implicity generating a private backing field for the property even though we don't see it explicitly declared.

    With all that being said, properties are still useful in the event you want to perform extra actions in the getters and setters as can be seen in the following example:

    public class MyClass {
    	private int testVal;
    	
    	public int TestVal {
    		get {
    			Console.WriteLine("testVal has been retrieved!");
    			return testVal;
    		}
    		set {
    			if(isValidInput(value)) {
    				testVal = value;
    			}
    			else {
    				Console.WriteLine("Not a valid value.");
    			}
    		}
    	}
    	
    	public void PerformCalculation() {
    		int result = doMethod(testVal);
    		Console.WriteLine($"{result}");
    	}
    }
    

    The Application

    Now in your ticket tracker application, you will need to add some auto-implemented properties in the Ticket, BusTicket, and TrainTicket classes. All the auto-implemented properties should be public.

    In Tickets/Ticket.cs, add an auto-implemented property for the following:

    • CustomerName of type string?
    • TicketNumber of type int
    • DepartDate of type DateTime Note that the ? at the end of string means that the value could be null which is the default value for strings. int and DateTime do not have null as a default value so they do not require the ?.

    In Tickets/BusTicket.cs, add an auto-implemented property for BusNumber of type int.

    In Tickets/TrainTicket.cs, add an auto-implemented property for TrainTerminal of type int.

  3. Challenge

    Abstraction

    Overview

    Abstraction is another pillar of object-oriented programming. At its core, it involves breaking down complex systems into simpler, more manageable parts. This is achieved by removing unnecessary details and modeling classes based on essential behaviors. It's about what the class can do rather than how it does it. In languages such as C#, abstraction is typically achieved through the usage of abstract classes and interfaces. These constructs typically contain methods that define behavior but not necessarily implementation. That's up to the derived subclasses to implement.

    Let's take a look at the following basic example:

    public abstract class MyClass {
    	private string secret;
    	public int SomeNum { get; set; }
    	
    	public MyClass() {
    	//assign secret here
    	}
    	
    	public void DoSomethingConcrete {
    		SomeNum *= 2;
    	}
    	
    	public abstract void ImplementThis();
    }
    

    This abstract class appears very much like a normal class. It can have its own constructor, fields, properties, and concrete methods. However, they can also contain methods and properties marked as abstract, which means they must be implemented by the subclasses derived from the abstract class. Abstract classes are typically used as base classes for derivation. One peculiar thing to note is that even though abstract classes can have a constructor, they cannot be directly instantiated. Their constructors exist to be called if necessary by derived subclasses.


    Interfaces

    Interfaces share quite a few similarities with abstract classes, namely their definition of methods to be implemented by classes that implement the interface. Like abstract classes, they can have default implementations and properties and also cannot be instantiated.

    On the other hand, interfaces cannot define a constructor like abstract classes can. While they can contain properties, they cannot contain data fields and any other forms of "instance state" like abstract classes can. However, derived subclasses can implement multiple interfaces but can only ever implement a single abstract base class.


    The Application

    Now in your ticket tracker application, you will need to do some slight modifications to the Ticket class, which will be the abstract base class for BusTicket and TrainTicket.

    In Tickets/Ticket.cs, add the abstract modifier to the Ticket class. Then define an abstract method called ShowInfo(). This should be a void method marked as public.

  4. Challenge

    Inheritance

    Overview

    As mentioned in the Abstraction section, abstract classes and interfaces define essential behaviors but typically do not provide implementations for them. This is left for their subclasses to handle, which brings us to the next pillar: Inheritance.

    As the name suggest, inheritance allows classes to inherit, or derive, their functionality from other classes and interfaces. It is important to note that when a class inherits from another base class or interface, there should be a clear IS-A relationship wherein the subclass is a logical subtype of the base class. This relationship can also be called a parent-child relationship.

    Let's take a look at the abstract class from the previous example:

    public abstract class MyClass {
    	private string secret;
    	public int SomeNum { get; set; }
    	
    	public MyClass() {
    	//assign secret here
    	}
    	
    	public void DoSomethingConcrete {
    		SomeNum *= 2;
    	}
    	
    	public abstract void ImplementThis();
    }
    
    public class SubClass : MyClass {
    	public int SubClassNum { get; set; }
    	
    	public SubClass() {
    		//Assign values
    	}
    	
    	public override void ImplementThis() {
    		//Implementation
    	}
    }
    

    We can see here that SubClass derives from MyClass using the SubClass : MyClass syntax. Any class that derives from another base class must provide an implementation for any method in the base class marked as abstract. In this example, the ImplementThis() method in the abstract base class MyClass is marked as abstract. This means that any class that derives from MyClass, including SubClass, must provide their own implementation of this method using the override syntax.

    As a child class to MyClass, SubClass also has its own secret field and SomeNum() property without needing to explicitly define it. SubClass can also call DoSomethingConcrete() as that method is already implemented in MyClass. However, SubClass can provide its own implementation for DoSomethingConcrete() using the override syntax if needed. Nonetheless, it is not required for successful compilation as a default implementation has already been defined, unlike the abstract ImplementThis() method.


    The Application

    Now in your ticket tracker application, your Ticket class should be an abstract base class containing an abstract method called ShowInfo().

    The BusTicket and TrainTicket classes are subclasses that will inherit from your Ticket class.

    Instructions 1. For both `BusTicket` and `TrainTicket`, have them implement the `Ticket` class using the `:` syntax. 2. In the constructors for `BusTicket` and `TrainTicket`, set their corresponding properties inherited from `Ticket` using the parameters. For example, `CustomerName = name`. 3. At the end of the constructor for `BusTicket`, set the `BusNumber` property using the `GetRandomVehicle()` method from the `Ticket` class. Do the same for `TrainTerminal` in the `TrainTicket` class. 4. Define the `ShowInfo` method in both subclasses as it is an abstract method in `Ticket`. For now the method body can be left empty.
  5. Challenge

    Polymorphism

    Overview

    The final pillar of Object-Oriented Programming to be addressed in Polymorphism. In the inheritance section, you may have caught a glimpse of this when it was mentioned that subclasses must provide implementations for abstract methods in their parent class or methods defined in the interface they implement.

    Simply put, polymorphism promotes flexibility and code reuse by allowing different types and classes to be treated by their base types that they descend from. This is done through method overriding and overloading. Overloading occurs when you have multiple methods with the same name, but each has different types/numbers of parameters which allows these methods to exist in the same scope despite having the same name. Overriding usually occurs when a subclass provides its own implementation for a specified method from its parent class.

    Here is an example:

    public abstract class Shape {
    	public abstract void CalculateArea();
    }
    
    public class Triangle : MyClass {
    	public int Base { get; set; }
    	public int Height { get; set; }
    
    	public Triangle(int base, int height) {
    		Base = base;
    		Height = height;
    	}
    	
    	public override void CalculateArea() {
    		Console.WriteLine($"Area of the Triangle is {(Base * Height) / 2}");
    	}
    }
    
    public class Circle : MyClass {
    	public int Radius { get; set; }
    
    	public Circle(int radius) {
    		Radius = radius;
    	}
    	
    	public override void CalculateArea() {
    		Console.WriteLine($"Area of the Circle is {Math.PI * Math.Pow(Radius, 2)}");
    	}
    }
    

    The Triangle and Circle classes are subclasses for the Shape class, which defines an abstract CalculateArea() method. Evidently, the implementations for each class will be different but they can be treated similarly as follows:

    	static void Main(string[] args) {
    		Shape firstShape = new Circle(5);
    		Shape secondShape = new Triangle(2, 4);
    		
    		firstShape.CalculateArea();
    		secondShape.CalculateArea();
    	}
    

    Polymorphism allows both of these distinct shapes to be treated as a Shape. Note that these are still instantiated as distinct Circle and Triangle classes because you cannot instantiate an abstract class. The syntax just allows them to be treated like Shape objects because they share it as a common base class. When calling their individual CalculateArea() methods, the correct CalculateArea() method to be called is determined based on the class of the object in question.


    The Application

    Now in your ticket tracker application, you need to provide implementations for ShowInfo() in your BusTicket and TrainTicket classes. The following instructions provide a good starter for information to be printed in ShowInfo(), but you are not required to implement them exactly as specified.

    Instructions 1. The first line in `ShowInfo()` of `BusTicket` should be `Console.WriteLine("Ticket Type: BUS");`. For `TrainTicket`, it should be `Console.WriteLine("Ticket Type: TRAIN");`. Or whatever output you would like to distinguish the two. 2. Next, use string interpolation to print out the properties of each class. For instance, `Console.WriteLine($"Name: {CustomerName}");`. Do this for each property in their respective classes. 3. For the `BusTicket`, prefix the ticket number with a letter, such as `B{TicketNumber}`. Do the same for `TrainTicket`. 4. Print out the departure date and time separately in their own `WriteLine()` statement. For departure date, use `DepartDate:MM/dd/yyyy` for interpolation. For departure time, use `DepartDate:h:mm tt` for interpolation.
  6. Challenge

    Using the Application

    If you've completed the lab, running the application should display a menu in the Terminal. You can add bus or train tickets after providing a name when prompted, remove tickets by giving a valid ticket number, or view all tickets in the system. Entering the words "quit" or "exit" at any time will exit the application.

    From this point onwards, feel free to use this lab as a playground to experiment or use as a point of reference.

George is a Pluralsight Author working on content for Hands-On Experiences. He is experienced in the Python, JavaScript, Java, and most recently Rust domains.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.