Abstract vs Interface

What is Abstraction?

Abstraction in Java is a fundamental concept in object-oriented programming that allows developers to focus on the essential qualities of an object, rather than its specific details. It simplifies complex reality by modeling classes appropriate to the problem, and by working at the most relevant level of inheritance for a particular aspect of the problem.

Key Points about Abstraction in Java:

  1. Definition: Abstraction is the process of hiding the implementation details and showing only the functionality to the user. It helps in reducing programming complexity and effort.

Advantages of Abstraction:

  • Reduces Complexity: By focusing on what an object does instead of how it does it.

  • Enhances Maintainability: Changes in the implementation do not affect how it is used.

  • Improves Modularity: Helps in dividing the program into independent parts.

Abstract Classes

An abstract class is a class that cannot be instantiated on its own and often contains one or more abstract methods.

Abstract methods are declared without an implementation and must be implemented by subclasses.

Abstract classes are used when you have a base class that should not be instantiated, and you want to provide some shared functionality to multiple subclasses.

Real-Life Example: Vehicles

Let's consider a scenario involving vehicles. We have different types of vehicles like cars and bicycles. Both types of vehicles share some common characteristics (e.g., the ability to start and stop), but they also have unique behaviors (e.g., honking a horn or ringing a bell).

public abstract class Vehicle {
    // Common field
    String brand;
    
    // Abstract method
    public abstract void makeNoise();

    // Concrete method
    public void start() {
        System.out.println("Vehicle is starting...");
    }

    // Concrete method
    public void stop() {
        System.out.println("Vehicle is stopping...");
    }
}

Abstract Method:

An abstract method is a method that is declared in an abstract class but doesn’t have an implementation. Each subclass will provide its own implementation of this method.

In abstract class if different subclass implements a method in their own that doesn't make sense to initiate a method so for this, we create an abstract method. Each subclass initiates this method in their own way

public class Car extends Vehicle {
    @Override
    public void makeNoise() {
        System.out.println("Car goes vroom vroom!");
    }
}

public class Bicycle extends Vehicle {
    @Override
    public void makeNoise() {
        System.out.println("Bicycle goes ring ring!");
    }
}

public class Main {
    public static void main(String[] args) {
        Vehicle myCar = new Car();
        Vehicle myBike = new Bicycle();

        myCar.start();
        myCar.makeNoise();
        myCar.stop();

        myBike.start();
        myBike.makeNoise();
        myBike.stop();
    }
}

Abstract class ensure an organized structure exactly what every subclass of vehicle has to have

Interfaces

An interface is a reference type in Java that is similar to a class and is a collection of abstract methods. A class implements an interface, thereby inheriting the abstract methods of the interface. Interfaces are used to specify what a class must do, but not how it does it. They are used when you want to define a contract that multiple classes can implement.

Real-Life Example: Electronic Devices

Consider electronic devices like a phone and a computer. They can both turn on and turn off, but they might have different ways of charging.

interface Chargeable {
    void charge();
}

public class Phone implements Chargeable {
    @Override
    public void charge() {
        System.out.println("Charging phone with USB cable.");
    }
}

public class Computer implements Chargeable {
    @Override
    public void charge() {
        System.out.println("Charging computer with power adapter.");
    }
}

public class Main {
    public static void main(String[] args) {
        Chargeable myPhone = new Phone();
        Chargeable myComputer = new Computer();

        myPhone.charge();
        myComputer.charge();
    }
}

Interface vs Abstract Class

  1. Use an abstract class when you have many related classes that share common functionality and fields.

  2. Use an interface when you have many unrelated classes that all need to perform a certain action.

  3. You can implement as many interfaces as you wanted. but you can only extend one abstract class only

  4. All fields in an interface are static and final and must be initialized when declared. In an abstract class, you don’t have to initialize fields when declaring them. Each object can have their own age and name

Here’s an example of an interface and an abstract class:

interface AnimalStuff {
    int age = 10;
    String name = "Mridul";
    void poop();
}
abstract class AnimalStuffAbstract {
    int age;
    String name;
    public void poop() {
        System.out.println("Pooping");
    }
    public abstract void makeNoise();
}

  1. Abstract Class Use Case:

    • Use an abstract class when you have multiple related classes that share common behavior and fields.

    • Example: All vehicles (cars, bicycles, trucks) have the ability to start and stop, so you put this shared behavior in an abstract class Vehicle.

  2. Interface Use Case:

    • Use an interface when you have multiple unrelated classes that need to implement a common behavior.

    • Example: Different electronic devices (phones, computers, tablets) need to be chargeable, so you define an interface Chargeable that all these classes implement.

Abstract Real-Life Use Case: Payment Processing System

In this example, we'll look at a payment processing system for an e-commerce platform. The system needs to support multiple payment methods such as Credit Card, PayPal, and Bitcoin. We'll use abstraction to define a general payment process and then implement specific payment methods.

Step 1: Define an Abstract Class

We'll start by creating an abstract class PaymentProcessor which defines the general structure of a payment process.

abstract class PaymentProcessor {
    // Template method
    public void processPayment(double amount) {
        validatePaymentDetails();
        initiatePayment(amount);
        sendPaymentReceipt();
    }

    // Abstract methods to be implemented by subclasses
    protected abstract void validatePaymentDetails();
    protected abstract void initiatePayment(double amount);

    // Concrete method
    private void sendPaymentReceipt() {
        System.out.println("Receipt sent to the customer.");
    }
}

Step 2: Implement Concrete Subclasses

Next, we'll create concrete subclasses for different payment methods: CreditCardProcessor, PayPalProcessor, and BitcoinProcessor.

class CreditCardProcessor extends PaymentProcessor {
    @Override
    protected void validatePaymentDetails() {
        System.out.println("Validating credit card details.");
    }

    @Override
    protected void initiatePayment(double amount) {
        System.out.println("Processing credit card payment of $" + amount);
    }
}

class PayPalProcessor extends PaymentProcessor {
    @Override
    protected void validatePaymentDetails() {
        System.out.println("Validating PayPal account details.");
    }

    @Override
    protected void initiatePayment(double amount) {
        System.out.println("Processing PayPal payment of $" + amount);
    }
}

class BitcoinProcessor extends PaymentProcessor {
    @Override
    protected void validatePaymentDetails() {
        System.out.println("Validating Bitcoin wallet.");
    }

    @Override
    protected void initiatePayment(double amount) {
        System.out.println("Processing Bitcoin payment of $" + amount);
    }
}

Step 3: Client Code

Finally, we'll write client code that uses these payment processors.

public class PaymentSystem {
    public static void main(String[] args) {
        PaymentProcessor creditCardProcessor = new CreditCardProcessor();
        creditCardProcessor.processPayment(100.0);

        PaymentProcessor paypalProcessor = new PayPalProcessor();
        paypalProcessor.processPayment(200.0);

        PaymentProcessor bitcoinProcessor = new BitcoinProcessor();
        bitcoinProcessor.processPayment(300.0);
    }
}

Explanation

  1. Abstraction: The PaymentProcessor abstract class defines a template method processPayment which provides a general payment processing algorithm. The details of the specific steps (validatePaymentDetails and initiatePayment) are left to the subclasses to implement.

  2. Concrete Implementations: CreditCardProcessor, PayPalProcessor, and BitcoinProcessor provide specific implementations for the abstract methods defined in PaymentProcessor.

  3. Code Reusability: The common functionality, such as sending a receipt, is implemented in the abstract class, promoting code reuse and reducing duplication.

  4. Extensibility: New payment methods can be added easily by creating new subclasses of PaymentProcessor and providing implementations for the abstract methods.

This example demonstrates how abstraction in Java can be used to create a flexible and maintainable payment processing system, allowing for easy extension and modification of payment methods without changing the core processing logic.

Interface Real-Life Use Case: Payment and Order Notification System

In this example, we'll integrate the payment processing system with a notification system that can notify users via different channels. Additionally, we'll have an order management system that also needs to send notifications. We'll use a common interface to define the notification service.

Step 1: Define the Notifier Interface

We'll start by defining the Notifier interface, which declares the method for sending notifications.

public interface Notifier {
    void sendNotification(String message, String recipient);
}

Step 2: Implement Concrete Notifier Classes

Next, we'll create concrete classes that implement the Notifier interface for different notification methods: EmailNotifier, SMSNotifier, and PushNotifier.

public class EmailNotifier implements Notifier {
    @Override
    public void sendNotification(String message, String recipient) {
        System.out.println("Sending email to " + recipient + " with message: " + message);
        // Logic to send email
    }
}

public class SMSNotifier implements Notifier {
    @Override
    public void sendNotification(String message, String recipient) {
        System.out.println("Sending SMS to " + recipient + " with message: " + message);
        // Logic to send SMS
    }
}

public class PushNotifier implements Notifier {
    @Override
    public void sendNotification(String message, String recipient) {
        System.out.println("Sending push notification to " + recipient + " with message: " + message);
        // Logic to send push notification
    }
}

Step 3: Integrate Notification into Payment Processing

We'll modify the PaymentProcessor class to use the Notifier interface for sending notifications.

abstract class PaymentProcessor {
    protected Notifier notifier;

    public PaymentProcessor(Notifier notifier) {
        this.notifier = notifier;
    }

    public void processPayment(double amount, String recipient) {
        validatePaymentDetails();
        initiatePayment(amount);
        sendPaymentReceipt(recipient);
    }

    protected abstract void validatePaymentDetails();
    protected abstract void initiatePayment(double amount);

    private void sendPaymentReceipt(String recipient) {
        notifier.sendNotification("Payment of $" + amount + " processed successfully.", recipient);
    }
}

Now, concrete payment processors will use the notifier.

public class CreditCardProcessor extends PaymentProcessor {
    public CreditCardProcessor(Notifier notifier) {
        super(notifier);
    }

    @Override
    protected void validatePaymentDetails() {
        System.out.println("Validating credit card details.");
    }

    @Override
    protected void initiatePayment(double amount) {
        System.out.println("Processing credit card payment of $" + amount);
    }
}

public class PayPalProcessor extends PaymentProcessor {
    public PayPalProcessor(Notifier notifier) {
        super(notifier);
    }

    @Override
    protected void validatePaymentDetails() {
        System.out.println("Validating PayPal account details.");
    }

    @Override
    protected void initiatePayment(double amount) {
        System.out.println("Processing PayPal payment of $" + amount);
    }
}

public class BitcoinProcessor extends PaymentProcessor {
    public BitcoinProcessor(Notifier notifier) {
        super(notifier);
    }

    @Override
    protected void validatePaymentDetails() {
        System.out.println("Validating Bitcoin wallet.");
    }

    @Override
    protected void initiatePayment(double amount) {
        System.out.println("Processing Bitcoin payment of $" + amount);
    }
}

Step 4: Create Order Management System

We'll create an OrderManager class that also uses the Notifier interface to send notifications.

public class OrderManager {
    private Notifier notifier;

    public OrderManager(Notifier notifier) {
        this.notifier = notifier;
    }

    public void placeOrder(String orderDetails, String recipient) {
        System.out.println("Order placed: " + orderDetails);
        notifier.sendNotification("Your order has been placed: " + orderDetails, recipient);
    }
}

Step 5: Client Code

Finally, we'll write client code that demonstrates both the payment processing system and the order management system using the common notification service.

public class Main {
    public static void main(String[] args) {
        Notifier emailNotifier = new EmailNotifier();
        Notifier smsNotifier = new SMSNotifier();

        // Payment processing with email notification
        PaymentProcessor creditCardProcessor = new CreditCardProcessor(emailNotifier);
        creditCardProcessor.processPayment(150.0, "user@example.com");

        // Payment processing with SMS notification
        PaymentProcessor paypalProcessor = new PayPalProcessor(smsNotifier);
        paypalProcessor.processPayment(250.0, "123-456-7890");

        // Order management with push notification
        Notifier pushNotifier = new PushNotifier();
        OrderManager orderManager = new OrderManager(pushNotifier);
        orderManager.placeOrder("Order #12345", "userDeviceToken");
    }
}

Explanation

  1. Interface Definition: The Notifier interface defines a contract for sending notifications. Any class that implements this interface must provide an implementation for the sendNotification method.

  2. Concrete Implementations: EmailNotifier, SMSNotifier, and PushNotifier classes implement the Notifier interface, providing specific behavior for sending notifications through different channels.

  3. Integration with Payment Processing: The PaymentProcessor class is modified to use the Notifier interface to send payment receipts, demonstrating dependency injection and adherence to the Open/Closed Principle.

  4. Order Management System: The OrderManager class also uses the Notifier interface to send notifications when an order is placed, showing the reusability and flexibility of the interface.

  5. Client Code: The client code demonstrates how both the payment processing system and the order management system can use different notifier implementations without changing their core logic.

This example shows how interfaces in Java can be used to create a flexible and reusable notification system that can be integrated into various parts of an application, such as payment processing and order management.

Last updated