Inheritance, Interfaces, and Polymorphism
Interfaces and Dependency Design
আপনি একটি 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:
EmailNotificationSenderobject তৈরি করেEnrollmentNotifierobject তৈরি করে- Constructor-এ sender inject করে
- 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:
PaymentGatewayCourseCodeFileStorageMathHelperEnrollmentRepositoryMoneyNotificationSender
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
protectedstate - Interface explosion
- Configuration represented as subclasses
- Delegation
- Inheritance থেকে composition-এ refactoring