September 19, 2024

Intro to OOP: The everyday programming style

8 min read
rb_thumb

rbs-img

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 .

Fancy word: encapsulation You can see in the above code sample how the dog object keeps all the properties together. In object-oriented programming, there is a fancy word for this: encapsulation. Like a vitamin capsule, the object keeps everything together in one container.

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.

Fancy word: member In object-oriented programming, the properties on an object are called members.

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.

Fancy word: method In object-oriented programming, the functions that belong to an object are called methods. So objects have members and methods.

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.

The ‘this’ keyword You’ll notice the this keyword in the previous example. This keyword appears in many object-oriented programming languages. It essentially says: I’m referring to the current object. In some languages like Python, the keyword is self .

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

Source: www.infoworld.com

Leave a Reply

Your email address will not be published. Required fields are marked *