Inheritance and Polymorphism: The Sibling dynamics of Object Oriented Alchemy

Spellbinding Objects

Object-Oriented Programming (OOP) is an enigmatic yet captivating concept. It's odd because it allows computing to interface with the real world in ways one would never have imagined. Picture this: you're trying to communicate with an alien species. First, you have to decode their cryptic language. Then, you have to use it to translate your own thoughts and ideas. That's what OOP feels like.

In this computational cosmos, everything can become an object. Namrata? An object. Bill Gates? Also an object. Elon Musk can either be an object of admiration or disdain, depending on how you view him, but he can also be an object. For those indifferent to tech billionaires, you have got to like my boys Naruto and Sasuke who can also be objects. The air we breathe, or the stomach bacteria responsible for that ghastly infection you had last week, can be objects. Let's not forget the city you reside in—yes, that can be objectified as well.

So why am I so fixated on objects? The first reason is pure fascination. Ever since I was introduced to the concept, it has captivated my imagination. The second reason is pragmatic: OOP simplifies my life. Coding is as unpredictable as life itself. For instance, who could have predicted my sudden infatuation with K-dramas a year after having no interest in them whatsoever?

Consider the example of developing an app initially designed for weather forecasting. Today it tells you if you should carry an umbrella or apply sunscreen, but what if, in a year's time, it must evolve into a comprehensive travel guide? It could provide advice on flights, accommodations, and activities, in addition to weather forecasts. OOP's modular and extensible architecture ensures you don't have to demolish the entire "building" to add new "rooms." Your code can adapt and evolve to accommodate ever-changing business needs—perhaps even suggesting ideal locations for K-drama tourism!

Today, I'll delve into two of the myriad benefits of OOP—Polymorphism and Inheritance. We'll tackle the other intriguing aspects in future discussions. But for now, let's focus on how these two principles are the linchpins that allow your software "car" to run smoothly, ensuring that if the air conditioning breaks, you only need to replace that specific unit rather than the entire vehicle.

The Siblings: Inheritance and Polymorphism

Much like the siblings in the family, inheritance is the older sibling who passes down traits and wisdom (reusable code) to younger siblings (subclasses). Polymorphism on the other hand, is the younger, more adaptable brother who can easily fit into different roles and environments, taking on multiple forms as needed. Jokes aside, both siblings are needed if you want to write cleaner code and implement a lot of functionalities.

Figure 1: Inheritance v/s Polymorphism

Inheritance: The House of Stark’s Code of Conduct

Just like the Stark family in Game of Thrones has a set of values and principles that get passed down from generation to generation—"The man who passes the sentence should swing the sword"—software also has a way to pass down properties and behaviors. This is what we call inheritance in Object-Oriented Programming (OOP). Just in case, if you like to follow Little Finger’s code of conduct, and follow a chaotic style of coding, too bad for you, he doesn’t have any lineage.

Code Reusability: The Stark Words Echo Through Generations

You must be recalling the famous Stark words, “Winter is coming” or “A lone wolf dies, but a pack survives”. These are some of the words, Eddard Stark wanted his children to remember. Think of Ned Stark and his children; they all embody the qualities of honor, bravery, and loyalty. They don't have to redefine these traits for each Stark child born. One of the most important advantages of inheritance is code reusability. When a class inherits from a parent class, it reuses methods and fields defined in the parent class, without the need to redefine them. This reduces redundancy and makes your code more maintainable.

// Parent class Stark
public class Stark {
  public void beHonorable() {
    System.out.println("Being honorable...");
  }
}

// Child class JonSnow
public class JonSnow extends Stark {
  // Inherits beHonorable() from Stark class
}

JonSnow jon = new JonSnow();
jon.beHonorable();  // Output: "Being honorable..."

"Winter is Streaming": The Netflix Saga

Everyone knows Netflix, the media behemoth that brought streaming into our living rooms. At its core, every Netflix region has some fundamental features: user profiles, watch history, a recommendation engine, and playback capabilities. Think of these as the unwavering Stark values of honor and bravery. No matter the region or the specific content it holds, these core features are constant.

Inheritance in Action:

When Netflix expands to a new country, they don't recreate their platform from the ground up. Instead, they build upon their foundational platform (the parent). They might add region-specific content, subtitles in the local language, or even region-specific features (like specific payment gateway integrations or partnerships with local ISPs). Each regional version of Netflix (child) inherits the primary features but has its distinct additions tailored to its audience.

Practical Illustration:

  1. Content Libraries: While the U.S. version of Netflix might have a show like "Stranger Things", the version in India might carry Bollywood hits or regional dramas in addition to the international content. Both versions, however, still allow users to create profiles, remember playback positions, and suggest content based on viewing habits.

  2. Partnerships and Integrations: In Japan, Netflix might partner with local anime studios for exclusive content. In parts of Europe, they might integrate local bank payment gateways that aren't available in other regions. Yet, the foundational playback and recommendation engine remains the same.

  3. Consistency in Experience: Despite these regional variations, when a user logs into Netflix, the look and feel, the method of content discovery, and the playback experience remain consistent. It's as though every user, regardless of location, can feel that underlying essence of Netflix's DNA, much like every Stark carries a piece of Winterfell with them.

In essence, just as the Starks don't redefine their values with each generation, Netflix doesn't reinvent its platform for every region. Instead, they expand and adapt, making the experience tailored yet familiar. This adaptability is what OOP, particularly the principle of inheritance, offers to software developers: a way to build adaptable, scalable, and maintainable systems that can cater to varying needs without losing their core identity.

Avoiding Tight Coupling: Each Stark has their own path

Inheritance also helps you avoid tight coupling between classes. Each Stark has their unique abilities and destinies — Arya becomes an assassin, Sansa a wise ruler and Bran becomes the Three-Eyed Raven. Yet, they share all common Stark characteristics. In the same way, inheritance allows you to modify or extend functionalities in your child classes, making the code more modular and easier to manage.

// Base class representing a Stark
public class Stark {
    public void familyMotto() {
        System.out.println("Winter is Coming");
    }

    public void commonTrait() {
        System.out.println("Honor, bravery, and loyalty");
    }
}

// Arya Stark class inheriting from Stark
public class AryaStark extends Stark {
    public void uniqueTrait() {
        System.out.println("Assassin skills");
    }
}

// Sansa Stark class inheriting from Stark
public class SansaStark extends Stark {
    public void uniqueTrait() {
        System.out.println("Political acumen");
    }
}

// Bran Stark class inheriting from Stark
public class BranStark extends Stark {
    public void uniqueTrait() {
        System.out.println("Three-Eyed Raven");
    }
}

// Main program
public class Main {
    public static void main(String[] args) {
        Stark[] starks = new Stark[3];
        starks[0] = new AryaStark();
        starks[1] = new SansaStark();
        starks[2] = new BranStark();

        for (Stark stark : starks) {
            stark.familyMotto();  // Common trait from base class
            stark.commonTrait();  // Common trait from base class
            if (stark instanceof AryaStark) {
                ((AryaStark) stark).uniqueTrait();  // Unique to Arya
            } else if (stark instanceof SansaStark) {
                ((SansaStark) stark).uniqueTrait();  // Unique to Sansa
            } else if (stark instanceof BranStark) {
                ((BranStark) stark).uniqueTrait();  // Unique to Bran
            }
            System.out.println("--------------------");
        }
    }
}

Here, the base class Stark has methods familyMotto() and commonTrait() that all Starks share. The individual Stark child classes each have their own uniqueTrait() method, demonstrating their unique abilities. Because each child class inherits from Stark, they can use the familyMotto() and commonTrait() methods without redefining them, thus adhering to the DRY (Don't Repeat Yourself) principle.

So, have you ever thought about how e-commerce platforms handle payments? Take, for instance, a platform that initially only uses a credit card payment gateway. Now, imagine that as time goes by, the platform wants to get with the times and add cool payment methods like PayPal, Bitcoin, or even bank transfers.

If they had gone with a tightly coupled design – think of it like wearing shoes that only fit with one specific outfit – then their whole shopping cart and checkout process would be tailor-made just for credit card payments. So, if they wanted to add a trendy new payment method? Yikes! They'd be looking at a total fashion overhaul, or in this case, rewriting heaps of their system.

But let's say they were savvy from the start and opted for a more versatile, loosely coupled design – kind of like a pair of classic white sneakers that go with anything. Here, the payment part is like an interchangeable accessory. Feel like swapping credit card payments for PayPal? Just switch out the payment module, and you're good to go – no need to change the entire look (or system)!

That's the beauty of a loosely coupled design. It's all about keeping things versatile and ready for whatever comes next, making sure things are sturdy and flexible. Pretty neat, right?

Polymorphism: Arya Stark’s Many Faces

Let's say we have a Stark interface (or an abstract class) that has a method named battleCry(). The implementation of battleCry() would differ for each Stark child. Jon Snow's battleCry() might return "For the North!", while Arya Stark's could return "Valar Morghulis." Here, polymorphism allows a single interface method—battleCry()—to be implemented in multiple ways, depending on the specific object that calls it.

In code, you could have a list of Stark objects and call battleCry() on each, without needing to know which specific child it is. The correct version of battleCry() would be executed based on the object's actual class. This is polymorphism in action—specifically, this is an example of "dynamic polymorphism" or "run-time polymorphism."

public interface Stark {
    void battleCry();
}

public class JonSnow implements Stark {
    public void battleCry() {
        System.out.println("For the North!");
    }
}

public class AryaStark implements Stark {
    public void battleCry() {
        System.out.println("Valar Morghulis!");
    }
}

// In the main program
public static void main(String[] args) {
    Stark[] starks = new Stark[2];
    starks[0] = new JonSnow();
    starks[1] = new AryaStark();

    for (Stark stark : starks) {
        stark.battleCry();  // Output will differ based on the actual object
    }
}

Imagine Netflix as our platform with the Profile interface. This interface has a method named getRecommendation(). Now, if you've used Netflix, you know that every profile has its own set of recommendations based on viewing history. For a profile that binges on romantic comedies, getRecommendation() might suggest "To All the Boys I've Loved Before". But for a documentary lover, the same method might return "Our Planet".

Here's the polymorphism magic: The method getRecommendation() acts like a chameleon. Depending on the profile it's working with, it pulls out a different movie or show. The underlying system doesn't need to know who exactly is watching; it just knows it needs to give a recommendation. So, when the getRecommendation() method is triggered, it dives into the profile's preferences and watching history, and—voilà!—it serves up a top pick, tailored just for that profile.

Imagine you're hosting a movie night. You have a list of your friends' Netflix profiles. You want a movie that's recommended for each of them. Without needing to log into each profile and manually checking, if you could just ask Netflix's system to getRecommendation() for each profile, it would instantly pull out top picks for each friend. Each recommendation is personal, all thanks to the wonder of polymorphism. One action, many outcomes, based on who's asking. That's the magic of Netflix and, by extension, polymorphism.

To be, or not to be: The dilemma about using Inheritance/Polymorphism

Falling into the trap of "More OOP concepts equals better code" is all too easy. While inheritance and polymorphism are powerful tools in the coding arsenal, it's crucial to wield them judiciously. Here are some guidelines to steer your decision-making on when to deploy inheritance and polymorphism in your projects.

To Be (Best Practices):

Clear Hierarchies: Think about a workplace setting. While everyone has a manager, you don't take directions from someone in a different department. The marketing team doesn't report to the head of engineering, and vice versa. Clear hierarchies ensure that you know exactly who to turn to for guidance and whose directives to follow.

Just as you'd tell someone from another department, "You're not the boss of me," ensure your class hierarchies make reporting lines clear and distinct.

Useful Method Names: Imagine having a remote with buttons labeled "Thingamajig" or "Doohickey." Confusing, right? When naming methods, clarity is king.

Name methods like you'd label a remote: clear and to the point.

Loose Coupling: Imagine classes as neighboring towns connected by roads. If one town (class) drastically changes its road layout, it shouldn't disrupt the flow of traffic from neighboring towns. Each town should function independently but can still interact when needed.

Design classes like towns with their own roads; changes in one shouldn’t create traffic jams everywhere else.

Not To Be (Pitfalls to Avoid):

Avoiding Deep Inheritance: Imagine trying to trace back your family tree 20 generations to understand your third cousin's hobbies. Sounds overcomplicated, right? That's what deep inheritance feels like in code.

Keep inheritance chains as short as recounting your immediate family, not the last 10 generations.

Misusing Polymorphism: It's like having a Swiss Army knife where the corkscrew is replaced by another blade. Sure, it's an override, but does it make sense?

Override methods with purpose, like picking the right tool for the task.

Over-reliance on Inheritance: Think of building with LEGO. You don’t need every piece to be a blue 2x4 brick. Mix and match pieces (or classes) for the best results.

Like constructing with LEGO, know when to use the classic pieces and when to try something new.

Navigating OOP is like navigating any new skill or toolset. The "To Be or Not To Be" advice? Simply put, it's about knowing the do's and don'ts. Embrace what works, watch out for pitfalls, and always aim for clarity and practicality in your code. If Hamlet had this guide for decision-making, he might've saved a ton of time! (Okay, okay, last literature reference. I promise!)

Tools and Libraries that have been made using this Alchemy

Embarking on a journey into the world of Object-Oriented Programming (OOP) is akin to delving deep into the ancient arts of potion-making and wizardry. Just as Severus Snape masterfully crafted potions at Hogwarts and the Maesters of the Citadel honed their scholarly crafts, countless OOP maestros have brewed digital concoctions that now infuse our daily lives. Here are some enchanting creations I've encountered in my own professional experiences.

  1. Java Spring Framework:

    • Polymorphism: The Spring framework, commonly used for web applications, heavily relies on polymorphism. The whole principle of Dependency Injection (DI) is based on it. Beans are defined, and then the appropriate implementation is injected at runtime. This way, the code remains decoupled and more maintainable.

    • Inheritance: Spring allows beans to inherit configurations from a parent bean. This can help in reducing redundancy in XML bean definitions or even in annotation-driven configurations.

  2. Django (Python):

    • Polymorphism: Django, a high-level Python web framework, employs polymorphism with its ORM (Object-Relational Mapping). For instance, you can define a QuerySet that behaves differently based on the underlying model, yet the method calls remain consistent.

    • Inheritance: Django supports model inheritance, enabling developers to create a base model with common fields and methods and then derive child models from it. The ORM handles the database structure for this hierarchy seamlessly.

  3. Arduino Libraries:

    • Polymorphism: The Arduino platform, famous for microcontroller programming, has various libraries that support different sensors and actuators. Many of these libraries use polymorphism. For instance, different sensors might have a read() method. Calling this method would fetch data from the sensor, but the underlying implementation varies depending on the sensor type.

    • Inheritance: Many Arduino libraries define base classes for general device types (like a generic sensor or actuator). Specific devices then inherit from these base classes and tailor the behavior as needed. For instance, a base Motor class might have derived classes like StepperMotor and ServoMotor.

  4. Windows Device Driver Kit (WDK):

    • Polymorphism: Writing drivers with WDK might involve using polymorphic methods that behave differently based on the hardware being interacted with but are accessed in a consistent manner.

    • Inheritance: A base driver class can be provided with various functionalities, and specific driver types can inherit and override or extend these functionalities based on the specific hardware.

  5. Qt Framework (C++):

    • Polymorphism: Qt, a free and open-source widget toolkit for creating GUIs, uses polymorphism extensively. For example, event handling in Qt uses polymorphism, allowing different widgets to handle events in their unique ways while providing a consistent interface.

    • Inheritance: In the Qt framework, widgets have a clear inheritance hierarchy. For instance, all button types might derive from a base QAbstractButton, inheriting common properties and methods.

Conclusion

Navigating the realms of Object-Oriented Programming is much like decoding the pages of an ancient spellbook. With inheritance, we have the power to amplify existing enchantments, while polymorphism grants us the versatility to modify our incantations as challenges arise. Remember to keep your spell components—those class hierarchies—neatly organized. And never forget the treasure trove of tools and libraries left behind by the OOP mages of old, waiting for you to explore. I genuinely hope you enjoyed this journey through my article. Keep practicing, keep experimenting, and stay tuned, for I'll be delving deeper into more OOP concepts in the future. Until then, happy coding!

References

  1. Elegant Objects by Yegor Bugayenko

  2. The Object-Oriented Thought Process by Matt Weisfeld

  3. Head First Object-Oriented Analysis and Design by Brett D. McLaughlin, Gary Pollice and David West

  4. Thinking in Java by Bruce Eckel