DEV Community

Cover image for Demystifying Object-Oriented Programming: Pt. 2
Felipe Stanzani
Felipe Stanzani

Posted on

Demystifying Object-Oriented Programming: Pt. 2

Welcome back! At the end of our last post, we hit a bit of a roadblock. We had successfully organized our F1, PickupTruck, and SUV classes to inherit from a base Car class. But then, we were faced with a new challenge:

A bicycle, a speedboat, and an electric car. They are all vehicles, but trying to force them into a single Vehicle inheritance hierarchy would be a nightmare. A speedboat doesn't have wheels, and a bicycle doesn't have an engine in the traditional sense. A simple extends Vehicle starts to feel clunky and wrong. How do we model things that share behaviors but are fundamentally different things?

The answer lies in moving beyond the idea that everything must share a common ancestor and instead thinking about what they can DO.

Interfaces: A Contract of Behavior

Let’s ask a different question. Instead of asking what these objects are, let's ask what they have in common from a user's perspective. A person can:

  • Steer them
  • Make them go forward
  • Slow them down

In Object-Oriented Programming, when we want to guarantee that different classes share a common set of behaviors, we use an INTERFACE. Think of an interface not as a blueprint for an object, but as a contract. It’s a list of methods that a class promises to implement. It defines WHAT a class can do, but not HOW it does it.

Let's create a contract for anything that can be driven. We'll call it Drivable.

// Interface
public interface Drivable {
  void turnLeft(double degrees);
  void turnRight(double degrees);
  void accelerate();
  void brake();
}
Enter fullscreen mode Exit fullscreen mode

Notice a few things. We use the interface keyword, not class. And the methods have no body—no {} with code inside. The Drivable interface simply states: "Any class that wants to be considered Drivable MUST provide its own implementation for these four methods."

Now, our classes can sign this contract using the implements keyword.

public class Car implements Drivable {
  // ... all the car attributes

  @Override
  public void accelerate() {
    // Code to make the engine burn fuel and turn the wheels
  }

  // ... other Drivable methods implemented
}

public class Bicycle implements Drivable {
  // ... bicycle attributes like pedals, handlebars

  @Override
  public void accelerate() {
    // Code that represents the rider pedaling faster
  }

  // ... other Drivable methods implemented
}

public class Speedboat implements Drivable {
  // ... speedboat attributes like hull, propeller

  @Override
  public void accelerate() {
    // Code to engage the propeller and push water
  }

  // ... other Drivable methods implemented
}
Enter fullscreen mode Exit fullscreen mode

With interfaces, we've solved our problem! We haven’t forced a bicycle to have an engine, or a car to have a propeller. We simply grouped them by a common capability: being drivable. A class can also implement multiple interfaces. A modern smart car could be Drivable, ConnectableToInternet, and HasGPS, for example.

Polymorphism: One Action, Many Forms

This leads us to one of the most powerful concepts in OOP: POLYMORPHISM. The word literally means "many forms." It’s the ability to treat objects of different classes as if they were the same type, as long as they adhere to the same contract (interface).

Imagine you have a garage. You can put all your drivable things in it.

Drivable mySuv = new SUV();
Drivable myBike = new Bicycle();
Drivable myBoat = new Speedboat();

List<Drivable> garage = new ArrayList<>();
garage.add(mySuv);
garage.add(myBike);
garage.add(myBoat);
Enter fullscreen mode Exit fullscreen mode

Now for the magic. We can loop through our garage and tell everything to accelerate, and each object will know how to perform that action in its own unique way.

for (Drivable vehicle : garage) {
  vehicle.accelerate(); // The SUV roars, the bike pedals, the boat throttles up!
}
Enter fullscreen mode Exit fullscreen mode

This is polymorphism in action. The same method call, vehicle.accelerate(), produces entirely different results depending on the actual object. You, the programmer, don’t need to know or care what type of object it is at that moment. You only need to know that it’s Drivable and can therefore accelerate. This makes your code incredibly flexible and easy to extend.

Abstract Classes: The Best of Both Worlds?

So, are interfaces the solution to everything? Not quite. Sometimes, you have a group of classes that are very closely related—they really do share a common identity and implementation, not just a behavior.

Let's go back to our Car example. We have gasoline cars, electric cars, and hybrid cars. They are all fundamentally cars. They all share a chassis, wheels, and a steering mechanism that works the same way. It would be wasteful to re-write the turnLeft method for every single type of car.

This is where an ABSTRACT CLASS comes in. An abstract class is a hybrid between a regular class and an interface.

  • Like a class, it can have attributes and fully implemented methods.
  • Like an interface, it can have abstract methods that have no implementation.
  • You cannot create an instance of an abstract class directly (you can't build a generic "Car," only a specific type like an ElectricCar).
public abstract class Car {
  // Attributes all cars share
  Chassis chassis;
  Wheel[] wheels;

  // A concrete method that all subclasses will inherit
  public void turnLeft(double degrees) {
  // The logic for turning is the same for all cars
  System.out.println("Turning left.");
  }

  // An abstract method that subclasses MUST implement
  public abstract void refuel();
}
Enter fullscreen mode Exit fullscreen mode

Now, specific car types can extend this abstract class. They inherit the chassis, wheels, and the turnLeft method for free, but they are forced to provide their own logic for refuel.

public class GasolineCar extends Car {
  @Override
  public void refuel() {
    System.out.println("Pumping gasoline.");
  }
}

public class ElectricCar extends Car {
  @Override
  public void refuel() {
    System.out.println("Plugging into charging station.");
  }
}
Enter fullscreen mode Exit fullscreen mode

Interface vs. Abstract Class: When to Use Which?

This is a classic question, but the distinction is clear once you grasp the core idea.

  • Use an Abstract Class for objects that are closely related in an "is-a" relationship. An ElectricCar is a Car. It allows you to share common code and attributes among a family of classes. A class can only extend one abstract class.

  • Use an Interface for objects that share a common capability, often in a "can-do" relationship. A Car can be Drivable. A Drone can be Flyable. A SmartSpeaker can be ConnectableToInternet. A class can implement many interfaces.

Wrapping Up

Today, we've added three more powerful concepts to our OOP toolkit:

  • Interface: A contract defining behaviors (what it CAN DO).
  • Polymorphism: Treating different objects as the same type, allowing one action to have many forms.
  • Abstract Class: A partial blueprint for a family of related classes, sharing common code (what IT IS).

By understanding when to use inheritance, abstract classes, and interfaces, you can build software that is flexible, reusable, and much easier to manage. You can now model complex relationships without tying yourself in knots.

What are your thoughts? Have you struggled with these concepts before? Leave your comments, suggestions, and questions below. See you in the next post!

Originally posted on my blog, Memory Leak

Top comments (0)