Inheritance, Interfaces, and Polymorphism

Interfaces and Dependency Design

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

Lesson Overview

আগের lesson-এ আমরা interface ব্যবহার করে different implementations একই contract-এর মাধ্যমে process করেছি।

Downloadable downloadable

এবার interface-এর আরও গুরুত্বপূর্ণ ব্যবহার শিখব: dependency design

ধরা যাক learner একটি course-এ enroll করলে LiveKlass notification পাঠাবে।

প্রথম implementation email ব্যবহার করে:

EmailNotificationSender

পরে requirements পরিবর্তিত হতে পারে:

SMS notification
Push notification
Console notification
Test notification

যদি enrollment logic সরাসরি EmailNotificationSender তৈরি করে, তাহলে business logic email implementation-এর সঙ্গে tightly coupled হয়ে যায়।

public class EnrollmentNotifier {

    private final EmailNotificationSender sender =
            new EmailNotificationSender();
}

Problems:

  • Email implementation সহজে replace করা যায় না
  • Testing-এর সময় real email পাঠানোর risk থাকে
  • Configuration class-এর ভেতরে hardcoded হয়
  • Business logic external technology সম্পর্কে বেশি জানে
  • New implementation support করতে class modify করতে হয়

Better design:

public class EnrollmentNotifier {

    private final NotificationSender sender;

    public EnrollmentNotifier(
            NotificationSender sender
    ) {
        this.sender = sender;
    }
}

এখন EnrollmentNotifier concrete email class-এর পরিবর্তে একটি behavior contract-এর ওপর depend করছে।

এই lesson-এ আমরা শিখব:

  • Dependency কী
  • Concrete dependency-এর সমস্যা
  • Interface-based dependency
  • Constructor injection
  • Dependency inversion-এর foundation
  • Replaceable implementations
  • Composition root
  • Fake implementation দিয়ে testing
  • External integration boundary
  • Interface ownership
  • Narrow interface design
  • কখন interface useful
  • কখন interface unnecessary
  • Spring dependency injection-এর foundation

Learning Objectives

এই lesson শেষ করার পর আপনি পারবেন:

  • একটি class-এর dependencies identify করতে
  • Concrete implementation-এর ওপর tight coupling explain করতে
  • Interface দিয়ে stable dependency contract তৈরি করতে
  • Constructor injection implement করতে
  • Different implementations দিয়ে একই service configure করতে
  • Fake implementation ব্যবহার করে isolated test লিখতে
  • Object creation এবং business behavior আলাদা করতে
  • Interface কোথায় define করা উচিত evaluate করতে
  • External integration boundary abstraction করতে
  • Unnecessary interface এবং dependency injection identify করতে

What Is a Dependency?

একটি class নিজের কাজ করতে অন্য object-এর সাহায্য নিলে সেই object তার dependency।

Example:

public class EnrollmentNotifier {

    private final EmailNotificationSender sender;

    public EnrollmentNotifier(
            EmailNotificationSender sender
    ) {
        this.sender = sender;
    }
}

EnrollmentNotifier notification পাঠাতে EmailNotificationSender ব্যবহার করে।

তাই:

EnrollmentNotifier depends on EmailNotificationSender

Dependency মানেই সমস্যা নয়।

সব meaningful applications-এর classes অন্য classes-এর সঙ্গে collaborate করে।

Important question:

Classটি কোন contract-এর ওপর depend করছে—একটি stable behavior, নাকি একটি specific implementation?


Concrete Dependency

public final class EmailNotificationSender {

    public boolean send(
            String recipient,
            String message
    ) {
        System.out.println(
                "Sending email to "
                + recipient
        );

        System.out.println(
                message
        );

        return true;
    }
}

Service:

public final class EnrollmentNotifier {

    private final EmailNotificationSender sender;

    public EnrollmentNotifier(
            EmailNotificationSender sender
    ) {
        this.sender = sender;
    }

    public boolean notifyEnrollment(
            String learnerEmail,
            String courseTitle
    ) {
        String message =
                "You are enrolled in "
                + courseTitle
                + ".";

        return sender.send(
                learnerEmail,
                message
        );
    }
}

এই design constructor injection ব্যবহার করলেও dependency concrete।

EnrollmentNotifier শুধু email sender accept করতে পারে।


Hardcoded Object Creation

আরও tightly coupled version:

public final class EnrollmentNotifier {

    private final EmailNotificationSender sender;

    public EnrollmentNotifier() {
        this.sender =
                new EmailNotificationSender();
    }
}

এখানে class নিজেই dependency create করছে।

Caller implementation select করতে পারে না।

Testing-এর সময়ও real implementation replace করা কঠিন।


Why Internal new Can Be a Problem

সব new usage wrong নয়।

Domain object creation normal:

new CourseCode(
        "JAVA"
);

কিন্তু external collaborator hardcode করা problematic হতে পারে:

new EmailNotificationSender();
new StripePaymentGateway();
new PostgresEnrollmentRepository();
new CloudFileStorage();

কারণ এগুলো often depend করে:

  • Network
  • Database
  • Credentials
  • Configuration
  • External provider
  • File system
  • Runtime environment

Business service যদি এগুলো internally create করে, configuration এবং testing tightly coupled হয়।


The Dependency Graph

Consider:

EnrollmentNotifier
└── EmailNotificationSender
    └── Email provider
        └── Network

EnrollmentNotifier-এর main responsibility:

Enrollment notification message prepare করা

এর email provider connection details জানা উচিত নয়।

Those belong to notification implementation or application configuration।


Introduce a Stable Contract

public interface NotificationSender {

    boolean send(
            String recipient,
            String message
    );
}

Contract:

Given a recipient and message,
attempt to send a notification,
and return whether it succeeded.

এই interface বলে না:

  • Email provider কোনটি
  • HTTP API কী
  • Console ব্যবহার হবে কি না
  • Test memory-তে store করবে কি না

It describes required behavior।


Implementing the Contract

public final class EmailNotificationSender
        implements NotificationSender {

    @Override
    public boolean send(
            String recipient,
            String message
    ) {
        System.out.println(
                "Sending email to "
                + recipient
        );

        System.out.println(
                message
        );

        return true;
    }
}

আরেকটি implementation:

public final class ConsoleNotificationSender
        implements NotificationSender {

    @Override
    public boolean send(
            String recipient,
            String message
    ) {
        System.out.println(
                "[Notification]"
        );

        System.out.println(
                "Recipient: "
                + recipient
        );

        System.out.println(
                "Message: "
                + message
        );

        return true;
    }
}

দুইটি class একই contract fulfil করে।


Depend on the Interface

public final class EnrollmentNotifier {

    private final NotificationSender sender;

    public EnrollmentNotifier(
            NotificationSender sender
    ) {
        if (sender == null) {
            throw new IllegalArgumentException(
                    "Notification sender is required."
            );
        }

        this.sender = sender;
    }

    public boolean notifyEnrollment(
            String learnerEmail,
            String courseTitle
    ) {
        if (
                learnerEmail == null
                || learnerEmail.isBlank()
        ) {
            return false;
        }

        if (
                courseTitle == null
                || courseTitle.isBlank()
        ) {
            return false;
        }

        String message =
                "You are enrolled in "
                + courseTitle.strip()
                + ".";

        return sender.send(
                learnerEmail.strip(),
                message
        );
    }
}

Field type:

NotificationSender

Concrete class নয়।


Constructor Injection

Dependency constructor parameter হিসেবে গ্রহণ করাকে constructor injection বলা হয়।

public EnrollmentNotifier(
        NotificationSender sender
) {
    this.sender = sender;
}

Caller dependency provide করে।

Email configuration:

NotificationSender sender =
        new EmailNotificationSender();

EnrollmentNotifier notifier =
        new EnrollmentNotifier(
                sender
        );

Console configuration:

NotificationSender sender =
        new ConsoleNotificationSender();

EnrollmentNotifier notifier =
        new EnrollmentNotifier(
                sender
        );

EnrollmentNotifier code unchanged।


Why Constructor Injection Is Strong

Required Dependency Is Explicit

Constructor দেখেই বোঝা যায়:

EnrollmentNotifier needs a NotificationSender

Object Is Complete After Construction

Invalid:

new EnrollmentNotifier(
        null
);

Rejected।

Dependency পরে set করার প্রয়োজন নেই।


Field Can Be final

private final NotificationSender sender;

Dependency reference object lifetime-এর মধ্যে replace হয় না।


Testing Is Easier

Real email implementation-এর পরিবর্তে fake implementation pass করা যায়।


Implementation Selection Is External

Business class নিজে decide করে না email, console, না অন্য sender ব্যবহার হবে।


Setter Injection Weakness

Alternative:

public final class EnrollmentNotifier {

    private NotificationSender sender;

    public void setSender(
            NotificationSender sender
    ) {
        this.sender = sender;
    }
}

Problems:

  • Object sender ছাড়া তৈরি হতে পারে
  • Method call-এর আগে dependency set করতে ভুল হতে পারে
  • Dependency runtime-এ unexpectedly change হতে পারে
  • Field final রাখা যায় না
  • Invalid intermediate state possible

Required dependencies-এর জন্য constructor injection সাধারণত stronger।


Method Injection

কখনো dependency একটি single operation-এর জন্য parameter হতে পারে।

public boolean notifyEnrollment(
        NotificationSender sender,
        String learnerEmail,
        String courseTitle
) {
}

Useful যখন dependency:

  • প্রতিটি call-এ different হতে পারে
  • Object-wide collaboration নয়
  • Caller intentionally operation-specific behavior choose করে

কিন্তু repeatedly same dependency pass করা noisy হলে constructor injection better।


Dependency Inversion: Beginner Mental Model

Without abstraction:

EnrollmentNotifier
        ↓
EmailNotificationSender

High-level enrollment workflow specific email technology-এর ওপর depend করছে।

With interface:

EnrollmentNotifier
        ↓
NotificationSender
        ↑
EmailNotificationSender
ConsoleNotificationSender

Both high-level service এবং concrete implementations একটি contract-এর সঙ্গে connect হয়।

Simple principle:

Business workflow concrete technical detail-এর পরিবর্তে প্রয়োজনীয় behavior contract-এর ওপর depend করবে।

এটি dependency inversion-এর foundation।


“Inversion” Means What?

Normally high-level class directly low-level classকে call করে:

EnrollmentNotifier → Email API implementation

Interface introduce করার পরে:

EnrollmentNotifier → NotificationSender contract
Email implementation → NotificationSender contract

High-level service contract define বা consume করে।

Low-level implementation সেই contract fulfil করে।

Dependency direction specific technology থেকে stable abstraction-এর দিকে যায়।


High-Level and Low-Level Code

High-Level Policy

Learner enroll করলে confirmation পাঠাতে হবে

Represented by:

EnrollmentNotifier

Low-Level Detail

SMTP
Email provider HTTP API
Console output
SMS gateway

Represented by concrete implementations।

High-level workflow low-level details-এর changes থেকে যতটা possible insulated থাকা ভালো।


Composition Root

Application-এর কোথাও objects create এবং connect করতে হয়।

public class Main {

    public static void main(
            String[] args
    ) {
        NotificationSender sender =
                new EmailNotificationSender();

        EnrollmentNotifier notifier =
                new EnrollmentNotifier(
                        sender
                );

        notifier.notifyEnrollment(
                "nur@example.com",
                "Java and OOP Foundation"
        );
    }
}

এই object-wiring locationকে composition root বলা হয়।

Simple console application-এ:

Main

composition root হতে পারে।

Web application-এ framework configuration এই কাজ করতে পারে।


Keep Wiring Outside Business Logic

Business logic:

EnrollmentNotifier

Object wiring:

Main

Separation:

Main decides which implementation
EnrollmentNotifier performs enrollment notification logic
EmailNotificationSender handles email delivery

Responsibilities clear থাকে।


Runtime Implementation Selection

Application configuration অনুযায়ী implementation choose করা যেতে পারে।

public static NotificationSender createSender(
        String mode
) {
    if (
            "email".equalsIgnoreCase(
                    mode
            )
    ) {
        return new EmailNotificationSender();
    }

    return new ConsoleNotificationSender();
}

Then:

NotificationSender sender =
        createSender(
                "email"
        );

EnrollmentNotifier notifier =
        new EnrollmentNotifier(
                sender
        );

Condition composition root-এ আছে।

Business service-এর ভেতরে নয়।


Avoid Choosing Implementation Inside the Service

Weak:

public final class EnrollmentNotifier {

    public boolean notifyEnrollment(
            String mode,
            String recipient,
            String message
    ) {
        if (
                "email".equals(mode)
        ) {
            return new EmailNotificationSender()
                    .send(
                            recipient,
                            message
                    );
        }

        return new ConsoleNotificationSender()
                .send(
                        recipient,
                        message
                );
    }
}

Problems:

  • Service সব implementations জানে
  • New sender add করলে service change
  • Object creation mixed with business behavior
  • Testing harder
  • Mode string invalid হতে পারে

Implementation selection বাইরে রাখুন।


Fake Implementation for Testing

Testing-এর সময় real email পাঠানো উচিত নয়।

একটি fake implementation memory-তে sent data store করতে পারে।

public final class FakeNotificationSender
        implements NotificationSender {

    private String lastRecipient;
    private String lastMessage;
    private int sendCount;

    @Override
    public boolean send(
            String recipient,
            String message
    ) {
        lastRecipient = recipient;
        lastMessage = message;
        sendCount++;

        return true;
    }

    public String getLastRecipient() {
        return lastRecipient;
    }

    public String getLastMessage() {
        return lastMessage;
    }

    public int getSendCount() {
        return sendCount;
    }
}

Usage:

FakeNotificationSender sender =
        new FakeNotificationSender();

EnrollmentNotifier notifier =
        new EnrollmentNotifier(
                sender
        );

boolean sent =
        notifier.notifyEnrollment(
                "nur@example.com",
                "Java and OOP Foundation"
        );

Verify:

System.out.println(sent);
System.out.println(
        sender.getSendCount()
);

System.out.println(
        sender.getLastRecipient()
);

System.out.println(
        sender.getLastMessage()
);

Possible output:

true
1
nur@example.com
You are enrolled in Java and OOP Foundation.

What the Fake Test Proves

Without external network call, আমরা verify করতে পারি:

  • Sender একবার call হয়েছে
  • Correct recipient pass হয়েছে
  • Correct message তৈরি হয়েছে
  • Service sender result return করেছে

Business behavior external provider থেকে isolatedভাবে test করা যায়।


Simulating Failure

public final class FailingNotificationSender
        implements NotificationSender {

    @Override
    public boolean send(
            String recipient,
            String message
    ) {
        return false;
    }
}

Test:

EnrollmentNotifier notifier =
        new EnrollmentNotifier(
                new FailingNotificationSender()
        );

boolean sent =
        notifier.notifyEnrollment(
                "nur@example.com",
                "Java Foundation"
        );

Expected:

false

Real provider fail না করিয়েও failure path test করা যায়।


Fake, Stub, and Mock: Brief Distinction

Testing literature-এ terms পাওয়া যায়:

Fake

Working lightweight implementation।

Example:

In-memory notification sender

Stub

Predefined result return করে।

return false;

Mock

Expected interactions verify করে, often testing library দিয়ে।

এই module-এ manually written fake যথেষ্ট।

Key idea:

Interface external collaborator replace করতে দেয়।


External Integration Boundaries

Interfaces particularly useful যখন dependency application-এর বাইরে যায়।

Examples:

NotificationSender
PaymentGateway
FileStorage
EnrollmentRepository
Clock
IdentityVerifier
CourseSearch

Concrete implementations:

EmailNotificationSender
StripePaymentGateway
DigitalOceanFileStorage
PostgresEnrollmentRepository
SystemClock

External systems change, fail, বা configuration require করতে পারে।

Stable application-facing contract complexity isolate করতে সাহায্য করে।


Example: Payment Boundary

public interface PaymentGateway {

    boolean charge(
            long amountInPaisa
    );
}

Implementations:

StripePaymentGateway
BkashPaymentGateway
FakePaymentGateway

Course enrollment service payment provider-specific HTTP code জানবে না।

তবে interface real domain requirements reflect করতে হবে।

A production payment contract শুধু boolean দিয়ে যথেষ্ট নাও হতে পারে।

It may need:

Transaction ID
Failure reason
Payment status
Idempotency key
Currency

Interface oversimplify করা উচিত নয়।


Good Interface Design Reflects Caller Needs

Weak technical interface:

public interface HttpCaller {

    String post(
            String url,
            String body
    );
}

Enrollment service যদি payment charge করতে চায়, raw HTTP contract too low-level।

Better application-facing contract:

public interface PaymentGateway {

    PaymentResult charge(
            PaymentRequest request
    );
}

Caller business operation express করে।

এই course-এর বর্তমান stage-এ simple contracts ব্যবহার করছি, কিন্তু naming এবং abstraction level গুরুত্বপূর্ণ।


Interface Ownership

A useful design question:

Interfaceটি কার প্রয়োজন represent করে?

EnrollmentNotifier-এর প্রয়োজন:

Send a notification

তাই NotificationSender contract application-এর notification boundary represent করে।

External provider library-এর classes direct expose করা উচিত নয়।

Weak:

public final class EnrollmentNotifier {

    private final ThirdPartyEmailClient client;
}

Better:

private final NotificationSender sender;

Adapter implementation third-party client wrap করতে পারে।


Adapter Implementation

public final class ProviderEmailNotificationSender
        implements NotificationSender {

    private final ProviderEmailClient client;

    public ProviderEmailNotificationSender(
            ProviderEmailClient client
    ) {
        if (client == null) {
            throw new IllegalArgumentException(
                    "Email client is required."
            );
        }

        this.client = client;
    }

    @Override
    public boolean send(
            String recipient,
            String message
    ) {
        return client.sendEmail(
                recipient,
                message
        );
    }
}

Application code provider-specific API থেকে isolated থাকে।


Narrow Interfaces

Broad interface:

public interface NotificationService {

    boolean sendEmail(
            String recipient,
            String message
    );

    boolean sendSms(
            String phone,
            String message
    );

    boolean sendPush(
            String deviceToken,
            String message
    );

    boolean scheduleCampaign();

    boolean cancelCampaign();
}

A simple enrollment notifier-এর এত behavior প্রয়োজন নেই।

Narrower contract:

public interface NotificationSender {

    boolean send(
            String recipient,
            String message
    );
}

Benefits:

  • Caller smaller contract বুঝে
  • Implementations fewer unrelated methods implement করে
  • Testing simpler
  • Unsupported methods avoid হয়
  • Coupling কমে

One Interface or Multiple Capabilities?

Sometimes different channels-এর recipient format আলাদা।

Email:

email address

SMS:

phone number

Push:

device token

A generic String recipient interface oversimplified হতে পারে।

Possible separate contracts:

public interface EmailSender {

    boolean sendEmail(
            String email,
            String subject,
            String message
    );
}
public interface SmsSender {

    boolean sendSms(
            String phoneNumber,
            String message
    );
}

Or a richer notification model।

Correct design domain requirements-এর ওপর depend করে।

Generic interface শুধু implementationsকে force করে একই shape নিতে গেলে weak হতে পারে।


Do Not Abstract Too Early

Suppose:

public final class CourseTitleFormatter {

    public String format(
            String title
    ) {
        return title.strip();
    }
}

Then creating:

public interface CourseTitleFormatterInterface {

    String format(
            String title
    );
}

may add no value if:

  • One trivial implementation
  • No external dependency
  • No replacement need
  • No meaningful strategy variation
  • No independent testing concern

Not every dependency needs an interface।


When a Single Implementation Still Justifies an Interface

একটি implementation থাকলেও interface useful হতে পারে যদি boundary inherently external বা volatile।

Examples:

PaymentGateway
EnrollmentRepository
FileStorage
NotificationSender
Clock

Reason:

  • External provider change হতে পারে
  • Tests need replacement
  • Infrastructure detail isolate করতে হয়
  • Application contract provider contract থেকে আলাদা
  • Failure simulation প্রয়োজন

Implementation count alone decision নয়।


When Concrete Dependency Is Fine

Concrete class directly depend করা acceptable হতে পারে যখন:

  • Class stable value object
  • Pure deterministic helper
  • No external side effect
  • Replacement unnecessary
  • Concrete API itself desired contract
  • Abstraction added value দেয় না

Example:

public final class CourseCodeNormalizer {

    public String normalize(
            String value
    ) {
        return value.strip()
                .toUpperCase();
    }
}

Even here behavior CourseCode constructor-এর মধ্যে private method হতে পারে।

Interface mandatory নয়।


Depend on Behavior, Not Class Names

Weak:

private final EmailNotificationSender emailNotificationSender;

Business service specific delivery technology জানে।

Stronger:

private final NotificationSender notificationSender;

Field name behavior describe করে।


Avoid Service Locator

Service locator-style code:

NotificationSender sender =
        ServiceRegistry.get(
                NotificationSender.class
        );

Problems:

  • Dependency constructor-এ visible নয়
  • Hidden global state
  • Testing setup complicated
  • Runtime failure possible
  • Class requirements discover করা কঠিন

Constructor injection dependency explicit রাখে।


Avoid Global Static Dependencies

Weak:

NotificationManager.send(
        recipient,
        message
);

Static global collaborator:

  • Replace করা কঠিন
  • Shared state থাকতে পারে
  • Testing isolation দুর্বল
  • Configuration hidden
  • Dependency declaration absent

Stateless utility static method আলাদা বিষয়।

External collaborator static global করা সাধারণত avoid করা ভালো।


Avoid Passing Too Many Dependencies

Constructor:

public EnrollmentService(
        EnrollmentRepository repository,
        PaymentGateway paymentGateway,
        NotificationSender notificationSender,
        CourseRepository courseRepository,
        LearnerRepository learnerRepository,
        AuditLogger auditLogger,
        CertificateGenerator certificateGenerator,
        AnalyticsTracker analyticsTracker
) {
}

Many dependencies indicate করতে পারে:

  • Class has too many responsibilities
  • Workflow should be decomposed
  • Some dependencies belong to another collaborator
  • Application orchestration too broad

Dependency injection bad নয়।

Too many dependencies class design review করার signal।


Dependency Lifecycle

Injected object shared হতে পারে।

NotificationSender sender =
        new EmailNotificationSender();

EnrollmentNotifier first =
        new EnrollmentNotifier(
                sender
        );

EnrollmentNotifier second =
        new EnrollmentNotifier(
                sender
        );

Both notifiers same sender object reference করে।

If sender mutable বা non-thread-safe হয়, sharing implications বুঝতে হবে।

Interface lifecycle automatically manage করে না।

Frameworks object lifecycle configuration handle করতে পারে।


Spring Dependency Injection Foundation

Spring application-এ concept একই।

Without Spring:

NotificationSender sender =
        new EmailNotificationSender();

EnrollmentNotifier notifier =
        new EnrollmentNotifier(
                sender
        );

Spring container conceptually:

  1. EmailNotificationSender object তৈরি করে
  2. EnrollmentNotifier object তৈরি করে
  3. Constructor-এ sender inject করে
  4. Configured object applicationকে দেয়

Core design Spring-specific নয়।

First design:

public EnrollmentNotifier(
        NotificationSender sender
)

Then framework wiring automate করতে পারে।


Framework Does Not Fix a Bad Contract

Spring interface inject করতে পারে।

কিন্তু interface যদি weak হয়:

public interface CommonService {

    Object process(
            Object input
    );
}

Design still poor।

Dependency injection framework:

  • Objects create করে
  • Dependencies connect করে
  • Lifecycle manage করতে পারে

It does not automatically create meaningful abstractions।


Multiple Implementations and Ambiguity

Suppose:

EmailNotificationSender
ConsoleNotificationSender

দুটিই NotificationSender

Manual wiring-এ caller clearly selects:

new EnrollmentNotifier(
        new EmailNotificationSender()
);

Framework wiring-এ configuration, qualifier, বা primary implementation দরকার হতে পারে।

Lesson-এর key concept:

Multiple implementations থাকলে application composition layer implementation choose করবে।

Business service নয়।


Complete Project Structure

src/main/java/io/liveklass/
├── Main.java
├── enrollment/
│   └── EnrollmentNotifier.java
└── notification/
    ├── NotificationSender.java
    ├── EmailNotificationSender.java
    ├── ConsoleNotificationSender.java
    ├── FakeNotificationSender.java
    └── FailingNotificationSender.java

NotificationSender.java

package io.liveklass.notification;

public interface NotificationSender {

    boolean send(
            String recipient,
            String message
    );
}

EmailNotificationSender.java

package io.liveklass.notification;

public final class EmailNotificationSender
        implements NotificationSender {

    @Override
    public boolean send(
            String recipient,
            String message
    ) {
        if (
                recipient == null
                || recipient.isBlank()
                || !recipient.contains("@")
        ) {
            return false;
        }

        if (
                message == null
                || message.isBlank()
        ) {
            return false;
        }

        System.out.println(
                "Sending email..."
        );

        System.out.println(
                "Recipient: "
                + recipient
        );

        System.out.println(
                "Message: "
                + message
        );

        return true;
    }
}

ConsoleNotificationSender.java

package io.liveklass.notification;

public final class ConsoleNotificationSender
        implements NotificationSender {

    @Override
    public boolean send(
            String recipient,
            String message
    ) {
        if (
                recipient == null
                || recipient.isBlank()
        ) {
            return false;
        }

        if (
                message == null
                || message.isBlank()
        ) {
            return false;
        }

        System.out.println(
                "[Console Notification]"
        );

        System.out.println(
                recipient
                + " → "
                + message
        );

        return true;
    }
}

FakeNotificationSender.java

package io.liveklass.notification;

public final class FakeNotificationSender
        implements NotificationSender {

    private String lastRecipient;
    private String lastMessage;
    private int sendCount;

    @Override
    public boolean send(
            String recipient,
            String message
    ) {
        lastRecipient = recipient;
        lastMessage = message;
        sendCount++;

        return true;
    }

    public String getLastRecipient() {
        return lastRecipient;
    }

    public String getLastMessage() {
        return lastMessage;
    }

    public int getSendCount() {
        return sendCount;
    }
}

FailingNotificationSender.java

package io.liveklass.notification;

public final class FailingNotificationSender
        implements NotificationSender {

    @Override
    public boolean send(
            String recipient,
            String message
    ) {
        return false;
    }
}

EnrollmentNotifier.java

package io.liveklass.enrollment;

import io.liveklass.notification.NotificationSender;

public final class EnrollmentNotifier {

    private final NotificationSender sender;

    public EnrollmentNotifier(
            NotificationSender sender
    ) {
        if (sender == null) {
            throw new IllegalArgumentException(
                    "Notification sender is required."
            );
        }

        this.sender = sender;
    }

    public boolean notifyEnrollment(
            String learnerEmail,
            String learnerName,
            String courseTitle
    ) {
        if (
                learnerEmail == null
                || learnerEmail.isBlank()
        ) {
            return false;
        }

        if (
                learnerName == null
                || learnerName.isBlank()
        ) {
            return false;
        }

        if (
                courseTitle == null
                || courseTitle.isBlank()
        ) {
            return false;
        }

        String message =
                "Hello "
                + learnerName.strip()
                + ", you are now enrolled in "
                + courseTitle.strip()
                + ".";

        return sender.send(
                learnerEmail.strip(),
                message
        );
    }
}

Main.java

package io.liveklass;

import io.liveklass.enrollment.EnrollmentNotifier;
import io.liveklass.notification.ConsoleNotificationSender;
import io.liveklass.notification.EmailNotificationSender;
import io.liveklass.notification.FakeNotificationSender;
import io.liveklass.notification.NotificationSender;

public class Main {

    public static void main(
            String[] args
    ) {
        sendEmailNotification();
        sendConsoleNotification();
        verifyWithFake();
    }

    private static void sendEmailNotification() {
        NotificationSender sender =
                new EmailNotificationSender();

        EnrollmentNotifier notifier =
                new EnrollmentNotifier(
                        sender
                );

        boolean sent =
                notifier.notifyEnrollment(
                        "nur@example.com",
                        "Nur",
                        "Java and OOP Foundation"
                );

        System.out.println(
                "Email sent: "
                + sent
        );

        System.out.println();
    }

    private static void sendConsoleNotification() {
        NotificationSender sender =
                new ConsoleNotificationSender();

        EnrollmentNotifier notifier =
                new EnrollmentNotifier(
                        sender
                );

        boolean sent =
                notifier.notifyEnrollment(
                        "subu@example.com",
                        "Subu",
                        "Backend Development"
                );

        System.out.println(
                "Console notification sent: "
                + sent
        );

        System.out.println();
    }

    private static void verifyWithFake() {
        FakeNotificationSender sender =
                new FakeNotificationSender();

        EnrollmentNotifier notifier =
                new EnrollmentNotifier(
                        sender
                );

        boolean sent =
                notifier.notifyEnrollment(
                        "sumu@example.com",
                        "Sumu",
                        "System Design"
                );

        System.out.println(
                "Fake result: "
                + sent
        );

        System.out.println(
                "Send count: "
                + sender.getSendCount()
        );

        System.out.println(
                "Captured recipient: "
                + sender.getLastRecipient()
        );

        System.out.println(
                "Captured message: "
                + sender.getLastMessage()
        );
    }
}

Possible Output

Sending email...
Recipient: nur@example.com
Message: Hello Nur, you are now enrolled in Java and OOP Foundation.
Email sent: true

[Console Notification]
subu@example.com → Hello Subu, you are now enrolled in Backend Development.
Console notification sent: true

Fake result: true
Send count: 1
Captured recipient: sumu@example.com
Captured message: Hello Sumu, you are now enrolled in System Design.

Design Review

Why Does EnrollmentNotifier Not Create the Sender?

Because implementation selection is configuration responsibility।

Main

chooses:

Email
Console
Fake

Why Is the Field an Interface Type?

private final NotificationSender sender;

Service required behavior-এর ওপর depend করে।

Concrete delivery technology-এর ওপর নয়।


Why Is the Dependency Final?

Notifier creation-এর পরে collaborator unexpectedly replace হবে না।

Object configuration stable থাকে।


Why Does the Fake Expose Captured Values?

Tests verify করতে পারে service correct interaction করেছে।

Production sender-এর behavior duplicate করার প্রয়োজন নেই।


Why Is NotificationSender in the Notification Package?

Contract notification capability represent করে।

এটি email provider-specific package-এর অংশ নয়।

Larger architecture-এ consumer-owned boundary বা application port package-এও রাখা যেতে পারে।

Important হলো interface provider-specific detail থেকে independent।


Common Mistakes

Creating the Dependency Inside the Service

this.sender =
        new EmailNotificationSender();

Implementation replace করা কঠিন হয়।


Depending on a Third-Party Client Directly

Application provider API-এর সঙ্গে tightly coupled হয়।


Using Setter Injection for Required Dependencies

Object incomplete state-এ থাকতে পারে।


Making the Dependency Static Global State

Testing এবং configuration hidden হয়।


Creating an Interface for Every Class

Real boundary বা substitutability না থাকলে abstraction noise তৈরি করে।


Designing an Interface Around an Implementation

Weak:

interface SmtpClientWrapper

Better application need:

interface NotificationSender

Making the Interface Too Broad

Implementations unsupported methods নিয়ে burdened হয়।


Selecting Implementation Inside Business Logic

New implementation add করতে service modify করতে হয়।


Assuming Interface Automatically Makes Code Testable

If service:

  • Uses global state
  • Creates other dependencies internally
  • Mixes many responsibilities
  • Has time-dependent behavior

then interface alone যথেষ্ট নয়।


Over-Simplifying External Contracts

Payment বা persistence boundaryকে শুধু boolean দিয়ে represent করা production requirements hide করতে পারে।


Practice Exercises

Exercise 1: Add SmsNotificationSender

Implement:

NotificationSender

For the exercise, print:

Sending SMS to ...

Use it without modifying EnrollmentNotifier


Exercise 2: Create a Failure Test

Use:

FailingNotificationSender

Verify:

notifyEnrollment(...)

returns false


Exercise 3: Remove Internal Construction

Refactor:

public class CoursePublisher {

    private final NotificationSender sender =
            new EmailNotificationSender();
}

Use constructor injection।


Exercise 4: Choose Interface or Concrete Type

Decide whether an interface is likely useful:

  1. PaymentGateway
  2. CourseCode
  3. FileStorage
  4. MathHelper
  5. EnrollmentRepository
  6. Money
  7. NotificationSender

Explain each decision।


Exercise 5: Narrow a Broad Interface

Refactor:

public interface CommunicationService {

    void sendEmail();

    void sendSms();

    void makePhoneCall();

    void scheduleCampaign();

    void deleteCampaign();
}

Create focused contracts for a service that only needs to send one enrollment notification।


Exercise 6: Write a Fake

Create:

FakePaymentGateway

It should:

  • Implement PaymentGateway
  • Record charged amount
  • Allow success or failure configuration
  • Expose charge count

Exercise 7: Find the Hidden Dependency

Review:

public boolean enroll(
        Learner learner,
        Course course
) {
    SystemClock clock =
            new SystemClock();

    EmailNotificationSender sender =
            new EmailNotificationSender();

    // Enrollment workflow
}

Identify hidden dependencies and propose constructor-injected contracts।


Predict the Result

Question 1

NotificationSender sender =
        new ConsoleNotificationSender();

EnrollmentNotifier notifier =
        new EnrollmentNotifier(
                sender
        );

Does EnrollmentNotifier need to know the sender's concrete class?


Question 2

EnrollmentNotifier notifier =
        new EnrollmentNotifier(
                new FakeNotificationSender()
        );

Will the notifier code need modification?


Question 3

new EnrollmentNotifier(
        null
);

What happens in the provided implementation?


Question 4

If EmailNotificationSender and SmsNotificationSender both implement NotificationSender, can either be injected through the same constructor?


Question 5

Does using an interface mean every implementation must use the same external technology?


Predict the Result Answers

Answer 1

না।

It only depends on the NotificationSender contract।

Answer 2

না।

Fake fulfils the same interface।

Answer 3

IllegalArgumentException

Required dependency missing।

Answer 4

হ্যাঁ।

Both are substitutable implementations of the contract।

Answer 5

না।

Implementations can use email, console, SMS, memory, or another mechanism as long as they fulfil the contract।


Knowledge Check

Question 1

Dependency কী?

Question 2

Concrete dependency-এর সমস্যা কী হতে পারে?

Question 3

Constructor injection কী?

Question 4

Required dependency constructor দিয়ে নেওয়া useful কেন?

Question 5

Programming to an interface বলতে কী বোঝায়?

Question 6

Dependency inversion-এর beginner-friendly meaning কী?

Question 7

Composition root কী?

Question 8

Fake implementation testing-এ useful কেন?

Question 9

External integration-এর সামনে interface useful কেন?

Question 10

Every class-এর interface প্রয়োজন কি?

Question 11

একটি implementation থাকলেও interface কখন justified হতে পারে?

Question 12

Service locator-এর সমস্যা কী?

Question 13

Broad interface problematic কেন?

Question 14

Implementation selection কোথায় থাকা উচিত?

Question 15

Spring dependency injection-এর foundation কী?


Knowledge Check Answers

Answer 1

একটি class নিজের কাজ করতে যে collaborator object-এর ওপর নির্ভর করে।

Answer 2

Implementation replacement, testing, configuration এবং independent evolution কঠিন হতে পারে।

Answer 3

Dependency constructor parameter হিসেবে গ্রহণ করা।

Answer 4

Object complete state-এ তৈরি হয়, dependency explicit থাকে এবং field final রাখা যায়।

Answer 5

Caller specific concrete class-এর পরিবর্তে প্রয়োজনীয় behavior contract-এর ওপর depend করে।

Answer 6

High-level workflow specific low-level technology নয়, stable abstraction-এর ওপর depend করে।

Answer 7

Application-এর যে জায়গায় objects create এবং dependencies connect করা হয়।

Answer 8

Real external side effect ছাড়াই interaction এবং failure paths verify করা যায়।

Answer 9

Provider changes, network behavior, credentials এবং infrastructure details application logic থেকে isolate করা যায়।

Answer 10

না।

Real boundary, replaceable behavior, বা testing need না থাকলে interface unnecessary হতে পারে।

Answer 11

Boundary external, volatile, side-effecting, বা testing-এর জন্য replaceable হলে।

Answer 12

Dependencies hidden হয়, global state বাড়ে এবং tests/configuration কঠিন হয়।

Answer 13

Implementations unrelated methods implement করতে বাধ্য হয় এবং caller unnecessary behavior-এর সঙ্গে coupled হয়।

Answer 14

Composition root, configuration layer, বা dependency injection container-এ।

Answer 15

Classes required dependencies constructor দিয়ে declare করে; container objects create করে এবং matching implementations inject করে।


Lesson Summary

এই lesson-এ আমরা শিখেছি:

  • একটি class অন্য collaborator ব্যবহার করলে সেটি dependency
  • Concrete dependencies implementation coupling বাড়াতে পারে
  • External collaborator internally create করলে configuration hidden হয়
  • Interface required behavior-এর stable contract তৈরি করতে পারে
  • Class concrete technology-এর পরিবর্তে interface-এর ওপর depend করতে পারে
  • Constructor injection required dependency explicit করে
  • Constructor injection objectকে complete state-এ তৈরি করে
  • Injected dependency final রাখা যায়
  • Different implementations same service configure করতে পারে
  • Business service implementation choose করা উচিত নয়
  • Composition root objects create এবং connect করে
  • Dependency inversion high-level workflowকে concrete technical detail থেকে আলাদা করে
  • Fake implementation isolated testing support করে
  • Failure implementation error path test করতে সাহায্য করে
  • External integrations interfaces-এর strong candidates
  • Interface provider API-এর পরিবর্তে application need represent করা উচিত
  • Narrow contract broad service interface-এর চেয়ে stronger
  • Interface ownership consumer requirement-এর সঙ্গে align করা ভালো
  • Every class-এর interface প্রয়োজন নেই
  • One implementation থাকলেও external boundary interface justify করতে পারে
  • Pure stable classes concrete dependency হিসেবে acceptable হতে পারে
  • Service locator এবং static global collaborators hidden dependencies তৈরি করে
  • Too many constructor dependencies class responsibility review করার signal
  • Dependency lifecycle এবং object sharing still consider করতে হয়
  • Spring constructor injection একই core design automate করে
  • Framework meaningful interface design-এর replacement নয়
  • Programming to interfaces মানে abstraction যেখানে real flexibility বা boundary দেয় সেখানে ব্যবহার করা

Next Lesson

পরবর্তী lesson:

Inheritance Design Traps and Composition-Based Alternatives

আমরা শিখব:

  • Inheritance শুধু code reuse-এর জন্য ব্যবহার করার সমস্যা
  • is-a বনাম has-a
  • Substitutability failures
  • Fragile base class
  • Deep hierarchy
  • Empty subclasses
  • Excessive protected state
  • Interface explosion
  • Configuration represented as subclasses
  • Delegation
  • Inheritance থেকে composition-এ refactoring