Assumptions:

  • Understanding of imperative and structured programming

What is Object-Oriented Programming?

Object-Oriented Programming (OOP) is a programming paradigm centered around grouping data, known as attributes, and procedures, known as methods, into data-structures called objects. Objects in OOP can interact with other objects, typically by calling on other object's methods.

OOP also allows code to be constructed in hierarchies using inheritance and polymorphism to allow objects to be related to one another, often sharing similar attributes and methods. This enables object-oriented code to be easily extensible, where a new type of object can be created by "inheriting" from a "parent".

<div style="width:100%;margin:auto;text-align:center"> <img src="https://www.devmaking.com/img/topics/paradigms/ClassHierarchy.png" alt="Class hierarchy" style="width:600px; max-width:95%;"> </div>

Characteristics of OOP

Object-oriented programming is a well-documented, and compared to other paradigms, vast subject that takes time to fully grasp, and longer to master.

If this is your first time being introduced to the world of OOP, I first just want to say I'm honored, and second, I would recommend reading this article over many sittings, and practicing concepts in a code editor to allow yourself time to fully grasp the concepts presented.

Classes and Objects

An object is the union of data and methods. There is another concept within OOP called a class that is often a point of confusion when learning object-oriented programming. Simply put, a class is the actual code that describes the methods and attributes of an object. An object is an instance of a class that exists in memory when the program is running.

We'll take a look at a simple class made in C# called Counter to get a better understanding of what a class looks like:

// This is what a normal class definition looks like:
public class Counter {

 public long count;
 
 // Default Constructor:
 public Counter() {
   count = 0;
 }
 
 // Special Constructor:
 public Counter(long startAtCount) {
   count = startAtCount;
 }
 
 // We'll talk about what "public" means later on!
 public void Tick() {
   count += 1;
 }
 
 public int GetCount() {
   return count;
 }
}

Above is an example of a class. Typically, classes are structured so that the data is put up top, followed by constructors, and finally the class methods. Depending on the programmer and language, some prefer to place the data at the bottom of the class (this is my preferred way in C++, but everywhere else I prefer up top). In our Counter class, we've defined a variable called count. When a variable belongs to a class, we call it a member variable or attribute.

The next few lines of the class contain the constructors, which have a sole purpose of helping the construction of the class; within a constructor, the member variables are initialized to their default values. Sometimes you might want to create an object in a different way, though, so we also have the option to define multiple constructors that take parameters. In the example above we have another "special" constructor that takes a startAtCount parameter, and allows us to initialize the starting value of the object.

Lastly, we've defined two methods within the class. A class method is just a function that belongs to a class! Class methods usually serve to modify or retrieve data, and have access to a special keyword called this, which we'll get into later on.

When we want to use our Counter class, we'll create the object somewhere else in our program using the new keyword, followed by the class constructor.

// Here we create an *instance* of a counter class:
// This is an object!
Counter counterA = new Counter();

// We can call the methods of a class using "."
int currentCount = counterA.GetCount();
System.WriteLine(currentCount) // "0"

counterA.Tick();
counterA.Tick();
counterA.Tick();

currentCount = counterA.GetCount();
System.WriteLine(currentCount) // "3"

// Here we create another counter using a special constructor!
Counter counterB = new Counter(100);

currentCount = counterB.GetCount();
System.WriteLine(currentCount) // "100"

currentCount = counterA.GetCount();
// See here that the values of the two objects are independent:
System.WriteLine(currentCount) // "3"

Here we see the Counter objects being created, and we can interact with the object's methods by placing a dot . between the variable name and the name of the method we want to call. Also take notice that when we create a second instance of the counter, that they both contain their own data independent of each other. There are ways to make them share data, which we'll cover shortly!

Methods

In our counter class example, we saw two examples of methods:

{
 // ...
 public void Tick() {
   count += 1;
 }

 public long GetCount() {
   return count;
 }
}

As mentioned, a method is essentially a function that exists within a class. Methods help express how an object can be interacted with by other objects.

Say that we want to add another method to the Counter class that allows us to add multiple "ticks" at once:

{
 // ...
 public void AddTicks(long count) {
   count += count;
 }
}

Hold on a second, you might be thinking. How does it know if we're referring to the "count" that belongs to the object, or the "count" method parameter?! Spoiler: this is bad code!

If you're new to OOP, you might think the only option we have is to change the name of the parameter to something other than "count". While this is certainly a valid option, we have another tool at our disposal: this.

The this Keyword

When you have a method parameter that has the same name as a class's member variable, you need a way to distinguish which variable you're referring to so that you (and the compiler) don't mix up variable names, causing catastrophe. Thus, this was invented!

The purpose of the this keyword is to let the computer know that you're referring to this object.

When using the keyword, writing this.count is a signal that you're referring to the class data, not the parameter. You can use the keyword in front of a method too: this.Tick() is valid as it refers to the Tick method attached to this object.

Take a look at how we'd fix the earlier code using this:

{
 // ...
 public void AddTicks(long count) {
   this.count += count;
 }
}

Here's a few guidelines to help you use this:

  1. When there is not a parameter that matches a member variable's name, this is optional: the compiler infers that you're referring to the object's data or method.
  2. When there is a parameter that matches a member variable's name, this always refers to the member variable.
  3. Having a single matching variable and parameter does not mean that you have to use it for everything; only for the variables that collide.

As a note for the third point, I personally find it useful to add in the keyword where applicable to remind myself when I'm modifying data belonging to an object or a parameter. This is especially helpful as a method gets more parameters.

> Python, Swift, and Smalltalk instead use self when referring to the current object.

Encapsulation

A common question when talking about methods and member variables is: "methods are neat, but what's keeping me from just changing count manually from another part of my program?"

So far, yes, you could write a program like this using our Counter class:

Counter myCounter = Counter();
myCounter.count += 1;
myCounter.count = 10000;
//...

While this is currently possible, we don't generally want this to be able to happen.

"Why?"

This a hard-earned lesson for many programmers: the fewer places you modify data, the easier it is to change. If you only modify data in one place, you only have to change it in one place. Using methods is great for making sure data is uniformly accessed, and by restricting how we can access data in a class, we can ensure that if there is ever an issue found, we only have to fix the issue in the class method.

This leaves us with the issue of wanting to "hide" the count variable from the outside world, then; we can use encapsulation to help us dictate what is hidden and shown!

Recall the Counter class once more, our methods began with a public keyword. This type of keyword is known as an access modifier, and can be placed on a class method, member variable, and (depending on the language) the class definition itself!

There are 3 major access modifiers: public, private, and protected. Some languages contain their own set of extra modifiers, but we'll focus on the basics here.

Public

If a member variable or method is preceded with public, that means that it can be accessed by anyone. This means other objects, code around the program, and code in other packages can directly interact with the data or method.

Private

If something is set to private, that means only the object has access to that method or data. In our Counter example above, we can use this knowledge to change our class around so that the outside world can no longer interact with the count member variable:

public class Counter {
 private long count;
 // ...
}

Viola! Now other classes can't interact with our count variable!

> There are certainly times where you might want a member variable to be public, however. After all, they give us the option to make it public or private for a reason! Learning when to use one or the other ultimately comes down to experience and which programming philosophies you fancy.

Protected

When a variable or method is marked as protected, it's private to the outside world, but public to the class and inheriting classes. In english, this means that it acts like a private variable that can optionally be used by child classes!

To understand how to use this modifier, we need to talk about the next characteristic of OOP: inheritance.

Inheritance

Possibly one of the most powerful characteristics of OOP, inheritance allows a super, or parent class to pass on its methods and variables on to a sub, or child class. This property allows functionality to be re-used, which saves time for developers, and makes programs easier to understand.

Let's create a new counter class that inherits from our Counter. The purpose of our new class will be to allow "laps" to be counted like a stopwatch.

public class LapCounter: Counter {
 private long startLapTime;

 public LapCounter() {
   this.startLapTime = 0;
 }

 public void StartLap() {
   this.startLapTime = this.GetCount();  // !?
 }

 public long GetLapTime() {
   return this.GetCount() - this.startLapTime;
 }
}

Let's break down the new class we've created. In C#, we express a class inheriting by using a colon : after the name of our new class, followed by the name of the class we want to inherit from. Note that different languages may have a different syntax for this, but mechanically speaking, it's all the same!

public class LapCounter: Counter { //...

The rest of the class should all make sense, that is, until we hit this line:

this.startLapTime = this.GetCount();

"Wait, we haven't defined a GetCount method in our LapCounter, how can we call this method?"

That's the magic of inheritance at work! Because we inherit from our Counter class, we now have access to all the public and protected methods and variables that the Counter class has. Inheritance not only allows the inheriting class to use the methods of the parent class, it also allows other parts of the program to use those methods as well. Let's take a look at how we would use our new LapCounter:

LapCounter lapCounter = new LapCounter();
long lapTime = lapCounter.GetLapTime();

Console.WriteLine( lapTime ); // "0"

// We can use the `Tick` method from the parent Counter class!
lapCounter.Tick();

lapCounter.StartLap();
lapCounter.Tick();
lapCounter.Tick();
lapTime = lapCounter.GetLapTime();

Console.WriteLine( lapTime ); // "2"
long totalTime = lapCounter.GetCount();
Console.WriteLine( totalTime ); // "3"

Protected Access Modifier Alternative

There's an alternative to the LapCounter's StartLap and GetLapTime implementations: instead of using a method to get the count, we could mark count as protected within our Counter class. This would give our LapCounter direct access to the count variable:

public class Counter {
 protected long count;
 // ...
}

public class LapCounter: Counter {
 public void StartLap() {
   this.startLapTime = this.count;
 }
 // ...
}

Now, before you go around marking all of your variables as protected, ask yourself "why should I do this?" Though we've marked it protected for example's sake, our LapCounter class doesn't have a good reason for directly needing to use the count variable, and can just use the GetCount() method to access the data anyways. Just because you have a hammer, doesn't mean everything is a nail.

Method Overriding

Like a rebellious teenager, child classes in OOP have the ability to override methods defined in the parent class, preferring their way of doing things instead. In C# and other languages, only methods that are specially marked as virtual in the parent class can be overridden by the child class. For instance, consider we're making a "foot" class that enables some video game character to move around at different speeds depending on what they're wearing on their feet:

public class Foot {
 public virtual float GetMoveSpeed(float baseSpeed) {
   return baseSpeed;
 }
}

By making the GetMoveSpeed method virtual, we're signaling that any child class can say "hey, this is brilliant, but I like this better", and define their own way of doing things by redefining the method, except marking it with override:

public class RunningShoe: Foot {
 public override float GetMoveSpeed(float baseSpeed) {
   return baseSpeed * 2.0f;
 }
}

Once more for good measure:

public class RollerBlades: Foot {
 public override float GetMoveSpeed(float baseSpeed) {
   return baseSpeed * 3.5f;
 }
}

> Note, child classes don't have to override virtual methods, they're simply there to optionally be overridden.

Interfaces and Abstract Classes

Using virtual methods, we can define a method in a parent class, and allow it to optionally be overridden in a child class. However, what if instead we wanted a set classes to all share common methods, except the child classes are required to write their own implementation? Most OOP languages give us 2 options for doing so, and the first is to use an interface!

Interfaces

An interface is similar to a class, containing a set of methods that don't have any implementation, and typically can't contain any data themselves. Any class that inherits from an interface requires that inheriting class to implement all of the methods defined in the interface. In a sense, we can view an interface as a contractual agreement that a subclass will implement the methods defined in the interface.

> Keep a mental note of this: it'll be important when we get to polymorphism!

Let's take a look at a simple interface for a sword item in a video game in C#:

// Per C# naming conventions, interfaces start with `I..`
// This is a style choice in other languages, though!
public interface ISword {
 int Damage();
 float SwingSpeed();
 string SwordName();
}

Using our new interface, let's make a few swords for the game:

public class RuneScimitar: ISword {
 public int Damage() {
   return 8;
 }

 public float SwingSpeed() {
   return 5.0f;
 }

 public string SwordName() {
   return "Rune Scimi";
 }
}

public class TheInfinityBlade: ISword {
 public int Damage() {
   return 10;
 }

 public float SwingSpeed() {
   return 3.5f;
 }

 public string SwordName() {
   return "The Infinity Blade";
 }
}

Now we can rest easy knowing that every sword in our game will be implementing the same set of methods!

There's a catch with interfaces, though; since the interface provides no implementations, we can't create an instance of an interface at runtime. The following code creates an error:

ISword mySword = new ISword(); // Error!

A common criticism of interfaces is that they can often lead to "copy-code", where you might have a few classes that all implement a certain method the same way, resulting in code being copied and pasted. This can introduce problems, especially if you ever decide that the implementation needs to change for a given method, resulting in having to change the method in many files (yuck).

Additionally, interfaces usually can't have data, which means classes implementing an interface might also end up copying each other's member variables. If only there was a way to provide some base functionality, while still requiring inheriting classes to implement their versions of methods!

Abstract Classes

An abstract class is a middle ground between an interface and a (real) class; abstract classes can contain data, and even have common "base" methods already implemented so that you only need to define the method in one place. Like interfaces, abstract classes cannot be instantiated at runtime. The real powerhouse of abstract classes, though, are abstract methods. Abstract methods are similar to virtual methods, except a base implementation isn't provided. Let's take a look:

public abstract class ScoreAchievement {
 
 public string title;
 protected bool isUnlocked;
 
 // Abstract classes can have constructors!
 public ScoreAchievement(string title) {
   this.title = title;
 }

 private void DisplayUnlockText() {
   Console.WriteLine($"Unlocked: {title}");
 }

 // Here's the important part!
 protected abstract bool TestUnlockCondition(int val);
 
 public void CheckForUnlock(int val) {
   if(this.isUnlocked) return;
   
   // Notice how we can use the abstract method here:
   if(TestUnlockCondition(val)) {
     this.isUnlocked = true;
     this.DisplayUnlockText();
   }
 }
}

Because our abstract class can't be instanced, we can use the TestUnlockCondition method without worry, because any concrete class will already have their own implementation for it! Let's take a look at a concrete achievement:

public class Score1kAchievement: ScoreAchievement {
 
 public Score1kAchievement(): base("Score 1000 Points")
 {
   // ...
 }
 
 protected override bool TestUnlockCondition(int val) {
   return val &gt;= 1000;
 }
}

// Demo of the code:
public static void Main() {

 Score1kAchievement myAchievement = new Score1kAchievement();
 int score = 995;
 
 myAchievement.CheckForUnlock(score);

 score = 1005;
 
 myAchievement.CheckForUnlock(score):
 // "Unlocked: Score 1000 Points"
}

There's another property of OOP that is solely responsible for making abstract classes and interfaces actually useful. Consider this for a moment: if we implement an abstract class or interface, we know for certain that any inheriting class will have the same set of methods. This is also true for regular class inheritance; the child class will contain all of the methods of the parent class.

OOP exploits this knowledge; if every child class is guaranteed to have the methods of a parent class, abstract class, or interface, then we can treat the child class as if it is the parent: Score1kAchievement is a ScoreAchievement, RuneScimitar is an ISword! This idea opens the door to some of the truly awesome features of OOP, and it's called Polymorphism.

> We'll touch on this later, but it's important to know that while a RuneScimitar is an ISword, it is not true the other way around! An ISword is not necessarily a RuneScimitar.

Polymorphism

Polymorphism is the ability for a language to treat a child class as if it's the parent class. Using this allows us to generalize objects, and swap out functionality dynamically in a program.

Let's take our ISword interface and the two swords we created for example: using polymorphism, we can dynamically swap out the type of sword we're using:

ISword aSword;

aSword = new RuneScimitar();
Console.WriteLine(aSword.SwordName()); // "Rune Scimi"

aSword = new TheInfinityBlade();
Console.WriteLine(aSword.SwordName()); // "The Infinity Blade"

Let's go a little further here, and create our very own Hero class that can equip a sword. This is where polymorphism starts to show it's true power, because it allows us to create objects with dynamic properties:

public class Hero {
 private ISword sword;

 public Hero(ISword initialSword) {
   this.sword = initialSword;
 }

 public void EquipSword(ISword newSword) {
   this.sword = newSword;
 }

 public int Attack() {
   return this.sword.Damage();
 }
}

// ...

public static void Main() {

 ISword runeScimi = new RuneScimitar();
 ISword infinityBlade = new TheInfinityBlade();

 Hero theHero = new Hero(runeScimi);

 int damage = theHero.Attack();
 Console.WriteLine($"The Hero dealt {damage} damage!"); 
 // "The hero dealt 8 damage!"

 theHero.EquipSword(infinityBlade);

 damage = theHero.Attack();
 Console.WriteLine($"The Hero dealt {damage} damage!"); 
 // "The hero dealt 10 damage!"
}

As a general rule of thumb, it's often a good idea to use the generalized type (interface, abstract class) as the variable type, and specifying the concrete type on the right side when creating the variables. We'll get to the why of this when we talk about OOP idioms soon!

What We Shouldn't do with Polymorphism, Usually

OOP allows you to take one type of data and reinterpret it as a different type of data. This is known as casting. This is something you've probably seen before when dealing with integers and floats:

float myFloat = 5.5f;

int myInt = (int)myFloat;

Console.WriteLine(myInt); // "5"

In OOP, you can do the same with objects. When casting between objects in the same hierarchy (i.e, ISword and RuneScimitar), there is a golden rule to remember: do not cast down the hierarchy (unless you know what you're doing).

For example, we can do this safely:

RuneScimitar scimi = new RuneScimitar();
ISword aSword = (ISword)scimi; // Good!

However, we cannot do this safely:

ISword aSword = new TheInfinityBlade();
TheInfinityBlade infBlade = (TheInfinityBlade)aSword; // Bad!

The first example shows upcasting, where you cast to the parent type, and the second example shows downcasting, where you cast to the child type. In the second example, we know the concrete type of aSword, but we won't always know it in reality. Consider this instead:

ISword aSword = new TheInfinityBlade();
// ...
RuneScimitar runeScimi = (RuneScimitar)aSword; // Error!

While the two concrete swords implement the same interface, they aren't the same swords. RuneScimtar and TheInfinityBlade are each ISword, but RuneScimitar is not TheInfinityBlade!

There are specific instances where you can safely downcast, but they require more advanced techniques, and often also require knowing ahead of time the concrete type of the object being casted into. To reiterate, You can always safely upcast, but you can't always safely downcast.

Why Downcasting is Unsafe

To give a better explanation as to why downcasting is unsafe, we'll need to add a method to our RuneScimitar class:

public class RuneScimitar: ISword {
 // ...

 public int Damage() {
   return (int)this.SpeedDamage();
 }

 public float SpeedDamage() {
   return this.Damage() * this.SwingSpeed();
 }
}

Something to note about inheritance is that child classes are free to add more methods beyond what the interface requires. The only requirement is that child classes implement at least the interface methods.

Using the above as an example, if we were to cast a RuneScimitar object to an ISword, we would lose the ability to call our SpeedDamage method from outside. That is because ISword is not guaranteed to have this method. However, as the example also shows, the object can still use these methods internally. This is good for if you need to define private helper methods to break down a problem.

Why this is unsafe: if we have an ISword variable that is referencing a TheInfinityBlade object, and try to downcast it to a RuneScimitar, we'll run into an error because TheInfinityBlade doesn't have a SpeedDamage method, nor is it ever guaranteed to. This means that the two types are not compatible to be casted between. Even if we were to define the same method in our TheInfinityBlade class, we still won't be able to cast between the two safely!

The static Keyword

So far we've learned about classes that don't share any methods or data, however, there's a special keyword called static that exists in some OOP languages that allows data and methods to belong to the class and not a particular object. To help understand what this means, let's show an example:

public class StaticCounter {
 private static int totalCount;
 
 private int localCount;

 public void Tick() {
   this.localCount++;
   StaticCounter.totalCount++;
 }

 public int GetLocalCount() {
   return this.localCount;
 }

 public static int GetTotalCount() {
   return StaticCounter.totalCount;
 }

 public static void ResetTotal() {
   StaticCounter.count = 0;
 }
}

When dealing with static methods and variables, it's important to know that static methods do not have access to non-static methods or data. This is because static methods cannot be called through an instance of a class. When calling on a static method, you need to reference it like so: ClassName.StaticMethod(..).

StaticCounter counterA = new StaticCounter();
StaticCounter counterB = new StaticCounter();

counterA.Tick();
counterA.Tick();

counterB.Tick();

Console.WriteLine(counterA.GetLocalCount()); // "2"
Console.WriteLine(counterB.GetLocalCount()); // "1"

Console.WriteLine(StaticCounter.GetTotalCount()); // "3"

> Be careful when using static data, as it can create hard-to-debug code. Typically, static data is used to store read-only values and constants that don't need to exist within a class instance. This doesn't mean there aren't good use-cases for static data, though!

Static classes

In some languages, it's possible to declare an entire class static. Typically you'll see this with "utility classes" like math libraries, where the class (i.e., Math) serves mainly as a domain to group methods i.e., Math.Cos(angle), and Math.Add(a, b). When a class is declared static, all data and methods must also be marked static:

public static class Math {
 public static float Cos(float angle) { /*..*/ }
 public static float Add(float a, float b) { /*..*/ }
 // ...
}

When a class is declared static, you cannot create an instance of it, however you can call the methods.

OOP Idioms

Closing things out, object-oriented-programming is likely the most widely used paradigm in the development industry (at least as of 2021 it is). Hence the length of this article. It also means that it's had a lot of eyes and hours poured into it, which has resulted in the emergence of OOP idioms ("best practices") to help keep projects from flying off the rails, with the most popular OOP idiom being <a href="https://www.devmaking.com/learn/design-patterns/solid-principles/" target="_blank" style="color:inherit">SOLID</a> principles.

Additionally, developers often found themselves encountering similar software design problems that could be solved by generalized "patterns" in the way they have objects interact with each other. In 1994, a group known today as the Gang of Four released a book on <a href="https://www.devmaking.com/learn/design-patterns/introduction/" target="_blank" style="color:inherit;">design patterns</a> that is regarded as standard reading in many universities and among professional software developers.