- Lab
- Core Tech

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.

Path Info
Table of Contents
-
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 thisnamespace 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. -
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 calledtestVal
. 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 thegetInt()
,setInt()
, andPerformCalculation()
methods that are marked aspublic
inMyClass
.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 }
andset { 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
, andTrainTicket
classes. All the auto-implemented properties should bepublic
.In
Tickets/Ticket.cs
, add an auto-implemented property for the following:CustomerName
of typestring?
TicketNumber
of typeint
DepartDate
of typeDateTime
Note that the?
at the end ofstring
means that the value could benull
which is the default value for strings.int
andDateTime
do not havenull
as a default value so they do not require the?
.
In
Tickets/BusTicket.cs
, add an auto-implemented property forBusNumber
of typeint
.In
Tickets/TrainTicket.cs
, add an auto-implemented property forTrainTerminal
of typeint
. -
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 asabstract
, 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 forBusTicket
andTrainTicket
.In
Tickets/Ticket.cs
, add theabstract
modifier to theTicket
class. Then define anabstract
method calledShowInfo()
. This should be avoid
method marked aspublic
. -
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 fromMyClass
using theSubClass : MyClass
syntax. Any class that derives from another base class must provide an implementation for any method in the base class marked asabstract
. In this example, theImplementThis()
method in the abstract base classMyClass
is marked asabstract
. This means that any class that derives fromMyClass
, includingSubClass
, must provide their own implementation of this method using theoverride
syntax.As a child class to
MyClass
,SubClass
also has its ownsecret
field andSomeNum()
property without needing to explicitly define it.SubClass
can also callDoSomethingConcrete()
as that method is already implemented inMyClass
. However,SubClass
can provide its own implementation forDoSomethingConcrete()
using theoverride
syntax if needed. Nonetheless, it is not required for successful compilation as a default implementation has already been defined, unlike the abstractImplementThis()
method.
The Application
Now in your ticket tracker application, your
Ticket
class should be an abstract base class containing an abstract method calledShowInfo()
.The
BusTicket
andTrainTicket
classes are subclasses that will inherit from yourTicket
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. -
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
andCircle
classes are subclasses for theShape
class, which defines an abstractCalculateArea()
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 distinctCircle
andTriangle
classes because you cannot instantiate an abstract class. The syntax just allows them to be treated likeShape
objects because they share it as a common base class. When calling their individualCalculateArea()
methods, the correctCalculateArea()
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 yourBusTicket
andTrainTicket
classes. The following instructions provide a good starter for information to be printed inShowInfo()
, 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. -
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.
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.