Skip to Content
Course content

17: Inheritance and Interfaces

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I was looking at some old code for a payment processing module last week, and it was a disaster. I had three different classes—CreditCardPayment, PayPalPayment, and BankTransferPayment—and about 70% of the code was identical. They all handled the amount, the currency, and a basic logging mechanism. It's the kind of redundancy that makes you dread a simple change because you have to remember to update it in three places.

Tired of copy-pasting

Let's look at how I started cleaning this up. Initially, I had something like this:

class CreditCardPayment {
    public $amount;
    public function process() {
        echo "Processing credit card payment of {$this->amount}...";
    }
}

class PayPalPayment {
    public $amount;
    public function process() {
        echo "Processing PayPal payment of {$this->amount}...";
    }
}

It works, but it's lazy. If I decide to add a transactionId to every payment, I'm editing every single class. So, I decided to pull the shared logic "upwards." I created a parent class—a blueprint—that handles the basics.

class Payment {
    protected $amount;

    public function __construct($amount) {
        $this->amount = $amount;
    }

    public function getAmount() {
        return $this->amount;
    }
}

class CreditCardPayment extends Payment {
    public function process() {
        echo "Charging credit card for {$this->getAmount()}...";
    }
}

class PayPalPayment extends Payment {
    public function process() {
        echo "Redirecting to PayPal for {$this->getAmount()}...";
    }
}

Now, CreditCardPayment and PayPalPayment "inherit" from Payment. Notice I used protected for $amount. I did that because private would lock it away from the child classes, and public lets any random piece of code change the amount mid-transaction, which is a recipe for a security nightmare. protected is the sweet spot: the children can see it, but the outside world can't.

The "Square Peg, Round Hole" problem

Here is where I hit a wall. I wanted to add a refund() method. Most payments are refundable, but some—like certain types of digital gift cards or one-time credits—absolutely are not.

My first instinct was to put refund() in the Payment base class. But then I realized that would force GiftCardPayment to have a refund() method it can't actually use. I'd end up writing a method that just throws an exception saying "Not supported." That's a huge red flag in software design; it means my class hierarchy is lying about what it can actually do.

I need a way to say: "I don't care what kind of object you are, as long as you are capable of being refunded."

Defining a contract

This is where Interfaces come in. An interface isn't a class; it's a contract. It doesn't contain any actual logic—no code, just a list of methods that a class must implement if it signs the contract.

interface Refundable {
    public function refund($amount);
}

Now, I can apply this selectively. I'll keep my inheritance for the "is-a" relationship (a Credit Card payment is a payment), but use the interface for the "can-do" relationship (this payment can be refunded).

class CreditCardPayment extends Payment implements Refundable {
    public function process() {
        echo "Charging card...";
    }

    public function refund($amount) {
        echo "Refunding {$amount} back to the card...";
    }
}

class GiftCardPayment extends Payment {
    public function process() {
        echo "Deducting from gift card...";
    }
    // No refund method here, because GiftCardPayment doesn't implement Refundable
}

The real magic happens when you write a function to handle refunds. Instead of checking if the object is a CreditCardPayment or a PayPalPayment, I just check if it implements the interface.

function processRefund(Refundable $payment, $amount) {
    $payment->refund($amount);
}

If I try to pass a GiftCardPayment into processRefund(), PHP will stop me immediately with a TypeError. It won't even try to run the code. This is infinitely better than running the code and having it crash halfway through because a method was missing. I've moved the error from "runtime" (where the user sees it) to "type-checking" (where the developer sees it).




📋 Practical Task

Build a Multi-Channel Notification System

You are building a notification system for an app. Some notifications are sent via Email, some via SMS, and some via Push Notifications. Not all of these channels support "Urgent" priority (which requires a special high-priority API call).

Your requirements:

  • Create a base class Notifier that stores the $recipient and has a method getRecipient().
  • Create an interface UrgentCapable with a method sendUrgent($message).
  • Create three classes: EmailNotifier, SmsNotifier, and PushNotifier. All three must extend Notifier.
  • Only SmsNotifier and PushNotifier should implement UrgentCapable.
  • Implement a function called dispatchUrgentNotification(UrgentCapable $notifier, $message) that calls the sendUrgent method.

Test your code by attempting to pass an SmsNotifier into dispatchUrgentNotification (which should work) and then attempting to pass an EmailNotifier (which should trigger a PHP TypeError).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.