Matthew Tyson
Contributing writer

Intro to OOP: The everyday programming style

feature
Nov 01, 202312 mins
JavaPythonSoftware Development

Here's what you need to know about object-oriented programming with classes, methods, objects, and interfaces, with examples in Java, Python, and TypeScript.

Objects, geometric shapes, OOP
Credit: Anna Martyanova/Shutterstock

Object-oriented programming (OOP) is sometimes portrayed as difficult and intimidating. The truth is that object-oriented programming uses a very familiar model to help make programs easier to manage. Let’s take another look to see how easy it is to understand this very popular and influential programming style.

Objects are familiar

In everyday life, we interact with objects in the world. Moreover, we recognize individual objects as having defining characteristics. The dog on the couch has attributes such as a color and a breed. In programming, we call these attributes properties. Here’s how we would create an object to represent a dog and its color and breed in JavaScript:


let dog = { 
  color: “cream”,
  breed: “shih tzu”
}

The dog variable is an object with two properties, color and breed. Already we are in the realm of object-oriented programming. We can get at a property in JavaScript using the dot operator: dog.color.

Creating classes of objects

We’ll revisit encapsulation in a stronger form shortly. For now, let’s think about the limitations of our dog object. The biggest problem we face is that any time we want to make a new dog object, we have to write a new dog variable. Often, we need to create many objects of the same kind. In JavaScript and many other programming languages, we can use a class for this purpose. Here’s how to create a Dog class in JavaScript:


class Dog { 
  color; 
  breed; 
}

The class keyword means “a class of objects.” Each class instance is an object. The class defines the generic characteristics that all its instances will have. In JavaScript, we could create an instance from the Dog class and use its properties like this:


let suki = new Dog();
suki.color = "cream"
console.log(suki.color); // outputs “cream”

Classes are the most common way to define object types, and most languages that use objects—including Java, Python, and C++—support classes with a similar syntax. (JavaScript also uses prototypes, which is a different style.)  By convention, the first letter of a class name is capitalized, whereas object instances are lowercased.

Notice the Dog class is called with the new keyword and as a function to get a new object. We call the objects created this way “instances” of the class. The suki object is an instance of the Dog class.

Adding behavior

So far, the Dog class is useful for keeping all our properties together, which is an example of encapsulation. The class is also easy to pass around, and we can use it to make many objects with similar properties (members). But what if we now want our objects to do something? Suppose we want to allow the Dog instances to speak. In this case, we add a function to the class:


class Dog { 
  color; 
  breed;
  speak() { 
    console.log(`Barks!`); 
}

Now the instances of Dog, when created, will have a function that can be accessed with the dot operator:


set suki = new Dog();
suki.speak() // outputs “Suki barks!”

State and behavior

In object-oriented programming, we sometimes describe objects as having state and behavior. These are the object’s members and methods. It’s part of the useful organization that objects give us. We can think about the objects in isolation, as to their internal state and behavior, and then we can think about them in the context of the larger program, while keeping the two separate.

Private and public methods

So far, we’ve been using what are called public members and methods. That just means that code outside the object can directly access them using the dot operator. Object-oriented programming gives us modifiers, which control the visibility of members and methods. 

In some languages, like Java, we have modifiers such as private and public. A private member or method is only visible to the other methods on the object. A public member or method is visible to the outside. (There is also a protected modifier, which is visible to the parts of the same package.) 

For a long time, JavaScript only had public members and methods (although clever coders created workarounds). But the language now has the ability to define private access, using the hashtag symbol:


class Dog { 
  #color; 
  #breed;
  speak() { 
    console.log(`Barks!`); 
  }
}

Now if you try to access the suki.color property directly, it won’t work. This privacy makes encapsulation stronger (that is, it reduces the amount of information available between different parts of the program).

Getters and setters

Since members are usually made private in object-oriented programming, you will often see public methods that get and set variables:


class Dog { 
  #color; 
  #breed; 
  get color() { 
    return this.#color;  
  } 
  set color(newColor) { 
    this.#color = newColor; 
  }
}

Here we have provided a getter and a setter for the color property. So, we can now enter suki.getColor() to access the color. This preserves the privacy of the variable while still allowing access to it. In the long term, this can help keep code structures cleaner. (Note that getters and setters are also called accessors and mutators.)

Constructors

Another common feature of object-oriented programming classes is the constructor. You notice when we create a new object, we call new and then the class like a function: new Dog(). The new keyword creates a new object and the Dog() call is actually calling a special method called the constructor. In this case, we are calling the default constructor, which does nothing. We can provide a constructor like so:


class Dog { 
  constructor(color, breed) { 
    this.#color = color; 
    this.#breed = breed; 
}
let suki = new Dog(“cream”, “Shih Tzu”);

Adding the constructor allows us to create objects with values already set. In TypeScript, the constructor is named constructor. In Java and JavaScript, it’s a function with the same name as the class. In Python, it’s the __init__ function. 

Using private members

Also note that we can use private members inside the class with other methods besides getters and setters:


class Dog { 
  // ... same
  speak() { 
    console.log(`The ${breed} Barks!`); 
  }
}
let suki = new Dog(“cream”, “Shih Tzu”);
suki.speak(); // Outputs “The Shih Tzu Barks!”

OOP in three languages

One of the great things about object-oriented programming is that it translates across languages. Often, the syntax is quite similar. Just to prove it, here’s our Dog example in TypeScript, Java, and Python:


// Typescript
class Dog { 
  private breed: string; 
  constructor(breed: string) { 
    this.breed = breed; 
} 
speak() { console.log(`The ${this.breed} barks!`); } 
} 
let suki = new Dog("Shih Tzu"); 
suki.speak(); // Outputs "The Shih Tzu barks!"

// Java
public class Dog {
    private String breed;

    public Dog(String breed) {
        this.breed = breed;
    }

    public void speak() {
        System.out.println("The " + breed + " barks!");
    }

    public static void main(String[] args) {
        Dog suki = new Dog("cream", "Shih Tzu");
        suki.speak(); // Outputs "The Shih Tzu barks!"
    }
}

// Python
class Dog:
    def __init__(self, breed: str):
        self.breed = breed

    def speak(self):
        print(f"The {self.breed} barks!")

suki = Dog("Shih Tzu")
suki.speak()

The syntax may be unfamiliar, but using objects as a conceptual framework helps make the structure of almost any object-oriented programming language clear.

Supertypes and inheritance

The Dog class lets us make as many object instances as we want. Sometimes, we want to create many instances that are the same in some ways but differ in others. For this, we can use supertypes. In class-based object-oriented programming, a supertype is a class that another class descends from. In OOP-speak, we say the subclass inherits from the superclass. We also say that one class extends another.

JavaScript doesn’t (yet) support class-based inheritance, but TypeScript does, so let’s look at an example in TypeScript.

Let’s say we want to have an Animal superclass with two subclasses defined, Dog and Cat. These classes are similar in having the breed property, but the speak() method is different because the classes have different speak behavior:


// Animal superclass
class Animal {
  private breed: string;

  constructor(breed: string) {
    this.breed = breed;
  }

  // Common method for all animals
  speak() {
    console.log(`The ${this.breed} makes a sound.`);
  }
}

// Dog subclass
class Dog extends Animal {
  constructor(breed: string) {
    super(breed); // Call the superclass constructor
  }

  // Override the speak method for dogs
  speak() {
    console.log(`The ${this.breed} barks!`);
  }
}

// Cat subclass
class Cat extends Animal {
  constructor(breed: string) {
    super(breed); // Call the superclass constructor
  }

  // Override the speak method for cats
  speak() {
    console.log(`The ${this.breed} meows!`);
  }
}

// Create instances of Dog and Cat
const suki = new Dog("Shih Tzu");
const whiskers = new Cat("Siamese");

// Call the speak method for each instance
suki.speak(); // Outputs "The Shih Tzu barks!"
whiskers.speak(); // Outputs "The Siamese meows!"

Simple! Inheritance just means that a type has all the properties of the one it extends from, except where I define something differently. 

In object-oriented programming, we sometimes say that when type A extends type B, that type A is-a type B. (More about this in a moment.)

Inheritance concepts: Overriding, overloading, and polymorphism

In this example, we’ve defined two new speak() methods. This is called overriding a method. You override a superclass’s property with a subclass property of the same name. (In some languages, you can also overload methods, by having the same name with different arguments. Method overriding and overloading are different, but they are sometimes confused because the names are similar.)

This example also demonstrates polymorphism, which is one of the more complex concepts in object-oriented programming. Essentially, polymorphism means that a subtype can have different behavior, but still be treated the same insofar as it conforms to its supertype. 

Say we have a function that uses an Animal reference, then we can pass a subtype (like Cat or Dog) to the function. This opens up possibilities for making more generic code. 


function talkToPet(pet: Animal) { 
  pet.speak(); // This will work because speak() is defined in the Animal class 
}

Polymorphism literally means “many forms.” 

Abstract types

We can take the idea of supertypes further by using abstract types. Here, abstract just means that a type doesn’t implement all of its methods, it defines their signature but leaves the actual work to the subclasses. Abstract types are contrasted with concrete types. All the types we’ve seen so far were concrete classes.

Here’s an abstract version of the Animal class (TypeScript):


abstract class Animal {
  private breed: string;
  abstract speak(): void;
}

Besides the abstract keyword, you’ll notice that the abstract speak() method is not implemented. It defines what arguments it takes (none) and its return value (void). For this reason, you can’t instantiate abstract classes. You can create references to them or extend them—that’s it. 

Also note that our abstract Animal class doesn’t implement speak(), but it does define the breed property. Therefore, the subclasses of Animal can access the breed property with the super keyword, which works like the this keyword, but for the parent class.

Interfaces

In general, an abstract class lets you mix concrete and abstract properties. We can take that abstractness even further by defining an interface. An interface has no concrete implementation at all, only definitions. Here’s an example in TypeScript:


interface Animal {
  breed: string;
  speak(): void;
}

Notice that the property and method on this interface don’t declare the abstract keyword—we know they are abstract because they are part of an interface.

Abstract types and overengineering

The ideal of abstract types is to push as much as you can into the supertypes, which supports code reuse. Ideally, we could define hierarchies that naturally contain the most general parts of a model in the higher types, and only gradually define specifics in the lower. (You can get a sense of this in Java and JavaScript’s Object class, from which all others descend and which defines a generic toString() method.)

In practice, however, there is a tendency to drift toward overengineering with deep and extravagant type hierarchies. The very good rule is to prefer shallow hierarchies over deep ones. In practice, developers have discovered that inheritance creates strong coupling between members, which makes them not amenable to change over time. Also, deep hierarchies tend towards inherent complexity that can overwhelm the purpose of the code.

Inheritance vs. composition

You remember when we described how a class is-a instance of its superclass? Oftentimes, we can solve a reuse problem by using has-a instead. That means, we create another object and put it as a reference. For example, in our Animal example, we might have originally been tempted to make Breed a subclass of Animal, since both Cat and Dog objects could use it. In that case, we could add behaviors or other characteristics to the Breed superclass:


class Breed extends Animal { 
  private originCountry: string;
}
Class Dog extends Breed { … }

In this case, we get the originCountry property on all our Dog and Cat instances, which is good for reuse. But ultimately, we’d likely be better served by setting up a has-a relationship. We do this by giving Dog and Cat a reference through Animal:


class Breed { 
  private originCountry: string;
}

class Animal {
  private breed: Breed;
}

The general rule here is: to prefer composition to inheritance. That is, compose objects rather than setting up inheritance hierarchies.

Conclusion

Object-oriented programming is one of the most significant and useful innovations in software development. In this article, you’ve learned the essential elements of this programming style and seen it applied in a few popular languages.