Professional Java Practices

Dependency Design and Composition

ReadingPreview

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

Lesson Overview

একটি class সাধারণত একা কাজ করে না।

Example:

CourseService

হয়তো depend করে:

CourseRepository
NotificationService
Clock

এই external collaborators-ই হলো:

Dependencies

Strong Java design-এর একটি important goal:

Dependencies should be explicit,
small,
and easy to replace.

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

  • Dependency কী
  • Hard-coded dependency-এর সমস্যা
  • Constructor dependency
  • Dependency injection without frameworks
  • Interface-based boundaries
  • Concrete class dependency কখন acceptable
  • Composition
  • Composition vs inheritance
  • Testability
  • Fake/in-memory collaborators
  • Global state-এর সমস্যা
  • Service locator anti-pattern
  • Dependency direction
  • Object graph
  • Avoiding unnecessary abstraction

Learning Objectives

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

  • Dependency identify করতে
  • Constructor injection ব্যবহার করতে
  • Hard-coded collaborators refactor করতে
  • Useful boundary-তে interface ব্যবহার করতে
  • Composition এবং inheritance distinguish করতে
  • Test-friendly class design করতে
  • Global mutable dependencies avoid করতে
  • Small object graph assemble করতে
  • Unnecessary interface creation avoid করতে

What Is a Dependency?

A dependency হলো এমন object বা service যা একটি class তার কাজ করার জন্য ব্যবহার করে।

Example:

public final class CourseService {

    private final CourseRepository repository;

}

CourseService depends on:

CourseRepository

because repository ছাড়া course load/save করা সম্ভব নয়।


Dependency Example

public final class CourseService {

    private final CourseRepository repository;

    public CourseService(
            CourseRepository repository
    ) {
        this.repository =
                repository;
    }

    public Course find(
            CourseCode code
    ) {
        return repository.findByCode(
                code
        );
    }
}

Dependency:

CourseRepository

Dependent class:

CourseService

Hard-Coded Dependency

Weak design:

public final class CourseService {

    private final FileCourseRepository repository =
            new FileCourseRepository(
                    Path.of(
                            "data",
                            "courses"
                    )
            );
}

Problem:

CourseService decides:
- which repository implementation to use
- where files live
- how repository is constructed

This creates tight coupling।


Why Hard-Coded Dependencies Hurt

Suppose later you want:

PostgresCourseRepository

Now CourseService must change।

Suppose unit test wants:

InMemoryCourseRepository

Again CourseService controls construction, so testing becomes harder।


Constructor Injection

Better:

public final class CourseService {

    private final CourseRepository repository;

    public CourseService(
            CourseRepository repository
    ) {
        if (repository == null) {
            throw new IllegalArgumentException(
                    "Course repository is required."
            );
        }

        this.repository =
                repository;
    }
}

Now caller decides which implementation to provide।


Application Chooses the Dependency

Production:

CourseRepository repository =
        new FileCourseRepository(
                Path.of(
                        "data",
                        "courses"
                )
        );

CourseService service =
        new CourseService(
                repository
        );

Test:

CourseRepository repository =
        new InMemoryCourseRepository();

CourseService service =
        new CourseService(
                repository
        );

CourseService does not change।


What Is Dependency Injection?

Dependency injection means:

A class receives its dependencies from outside
instead of constructing them internally.

Example:

new CourseService(
        repository
);

This is dependency injection।

No framework is required।


Dependency Injection Is Not Spring

Spring can perform dependency injection automatically।

But the underlying design principle is plain Java:

public CourseService(
        CourseRepository repository
) {
    this.repository =
            repository;
}

Understanding this first makes framework-based DI much easier later।


Constructor Injection

Constructor injection is usually a strong default for required dependencies।

Example:

public final class EnrollmentService {

    private final EnrollmentRepository enrollmentRepository;
    private final CourseRepository courseRepository;

    public EnrollmentService(
            EnrollmentRepository enrollmentRepository,
            CourseRepository courseRepository
    ) {
        this.enrollmentRepository =
                enrollmentRepository;

        this.courseRepository =
                courseRepository;
    }
}

Why Constructor Injection Is Strong

It makes dependencies:

  • Explicit
  • Required at construction time
  • Easy to inspect
  • Easy to test
  • Compatible with final fields

An object cannot be created in an incomplete state if all required dependencies must be supplied।


Weak Setter Injection for Required Dependency

public final class CourseService {

    private CourseRepository repository;

    public void setRepository(
            CourseRepository repository
    ) {
        this.repository =
                repository;
    }
}

Now this is possible:

CourseService service =
        new CourseService();

service.publish(
        code
);

before repository is set।

Potential result:

NullPointerException

Constructor injection avoids this invalid object state।


Optional Dependencies

Not every collaborator is mandatory।

But before making something optional, ask:

Can this class meaningfully work without it?

If not, constructor dependency is appropriate।


Dependency Should Usually Be final

private final CourseRepository repository;

This communicates:

CourseService always uses the same repository reference after construction.

It also prevents accidental reassignment।


Depend on an Interface at a Useful Boundary

Suppose we have:

public interface CourseRepository {

    Course findByCode(
            CourseCode code
    );

    void save(
            Course course
    );
}

Implementations:

FileCourseRepository
InMemoryCourseRepository
PostgresCourseRepository

CourseService depends on:

CourseRepository

not:

FileCourseRepository

This is a useful abstraction because storage implementation genuinely varies।


Why Repository Is a Good Interface Boundary

It represents a capability:

Store and retrieve courses

rather than a particular implementation।

Different implementations make sense।

That is a strong reason for an interface।


Not Every Class Needs an Interface

Weak overengineering:

CourseService
CourseServiceImpl
CourseValidator
CourseValidatorImpl
CoursePublisher
CoursePublisherImpl

when only one simple implementation exists and no meaningful boundary requires abstraction।

Do not create interfaces mechanically।


Interface Is Useful When

Common signals:

  • Multiple legitimate implementations exist
  • Infrastructure should be hidden behind a contract
  • Caller should depend on capability, not mechanism
  • Testing benefits from a lightweight implementation
  • A clear architectural boundary exists

Concrete Dependency Can Be Fine

Suppose:

CourseTitleNormalizer

is a tiny stateless helper with one implementation।

This may be perfectly fine:

private final CourseTitleNormalizer normalizer;

You do not necessarily need:

CourseTitleNormalizerInterface

Interface Names

Avoid:

ICourseRepository

unless project conventions require it।

Java commonly uses:

CourseRepository

for interface।

Implementation:

FileCourseRepository

or:

InMemoryCourseRepository

Composition

Composition means one object uses other objects to build behavior।

Example:

public final class CourseService {

    private final CourseRepository repository;
    private final CourseNotifier notifier;
}

CourseService is composed with collaborators।


Composition Represents "Has-A"

Example:

CourseService has a CourseRepository
Course has Lessons
Car has an Engine

This differs from inheritance:

Dog is an Animal

Inheritance Represents "Is-A"

Example:

class Dog
        extends Animal {
}

This says:

Dog is an Animal

Inheritance creates a stronger relationship than composition।


Composition Over Unnecessary Inheritance

Suppose we want course publishing notifications।

Weak design:

public class CourseService
        extends EmailSender {

}

Is a CourseService an EmailSender?

No।

It merely needs to use one।

Better:

public final class CourseService {

    private final CourseNotifier notifier;
}

This is composition।


Why Composition Is Often Safer

Composition:

  • Keeps responsibilities separate
  • Avoids inheriting unwanted behavior
  • Makes collaborators replaceable
  • Reduces fragile parent-child coupling
  • Makes testing easier

Inheritance is useful when a genuine subtype relationship exists।


Avoid Inheritance Just for Code Reuse

Weak:

public class FileCourseRepository
        extends FileUtilities {
}

only because it wants helper methods।

This says:

FileCourseRepository is a FileUtilities

which makes little conceptual sense।

Better:

  • Use composition
  • Use focused helper methods
  • Use package-private utilities when appropriate

Example: Notification Boundary

public interface CourseNotifier {

    void coursePublished(
            Course course
    );
}

Email implementation:

public final class EmailCourseNotifier
        implements CourseNotifier {

    @Override
    public void coursePublished(
            Course course
    ) {
        System.out.println(
                "Sending publication email for "
                + course.getTitle()
        );
    }
}

Service Using Composition

public final class CourseService {

    private final CourseRepository repository;
    private final CourseNotifier notifier;

    public CourseService(
            CourseRepository repository,
            CourseNotifier notifier
    ) {
        if (repository == null) {
            throw new IllegalArgumentException(
                    "Repository is required."
            );
        }

        if (notifier == null) {
            throw new IllegalArgumentException(
                    "Notifier is required."
            );
        }

        this.repository =
                repository;

        this.notifier =
                notifier;
    }

    public void publish(
            CourseCode code
    ) {
        Course course =
                requireCourse(
                        code
                );

        course.publish();

        repository.save(
                course
        );

        notifier.coursePublished(
                course
        );
    }

    private Course requireCourse(
            CourseCode code
    ) {
        Course course =
                repository.findByCode(
                        code
                );

        if (course == null) {
            throw new IllegalStateException(
                    "Course not found."
            );
        }

        return course;
    }
}

Testability Through Dependency Design

Because dependencies are supplied from outside, test can replace them।

Example:

public final class RecordingCourseNotifier
        implements CourseNotifier {

    private int publishCount;

    @Override
    public void coursePublished(
            Course course
    ) {
        publishCount++;
    }

    public int getPublishCount() {
        return publishCount;
    }
}

Test Setup

InMemoryCourseRepository repository =
        new InMemoryCourseRepository();

RecordingCourseNotifier notifier =
        new RecordingCourseNotifier();

CourseService service =
        new CourseService(
                repository,
                notifier
        );

No email is actually sent।

Test receives deterministic local collaborators।


In-Memory Repository

public final class InMemoryCourseRepository
        implements CourseRepository {

    private final Map<CourseCode, Course> courses =
            new HashMap<>();

    @Override
    public Course findByCode(
            CourseCode code
    ) {
        return courses.get(
                code
        );
    }

    @Override
    public void save(
            Course course
    ) {
        courses.put(
                course.getCode(),
                course
        );
    }
}

Useful for:

Tests
Demos
Local experiments

Fake vs Mock

At foundation level, understand the concept:

A fake is a lightweight working implementation।

Example:

InMemoryCourseRepository

A mock typically records or verifies interactions, often using a testing library।

You do not need a mocking framework to design testable code।

Good dependency boundaries come first।


Dependency Design Without a Test Framework

You can manually verify:

service.publish(
        code
);

if (
        notifier.getPublishCount()
        != 1
) {
    throw new AssertionError(
            "Expected one notification."
    );
}

The point is:

Dependencies can be replaced without changing CourseService.

Hard-Coded Time Dependency

Suppose:

public Course createCourse(...) {
    Instant createdAt =
            Instant.now();

    ...
}

Testing time-dependent behavior can be harder because current time changes constantly।

Java provides:

Clock

Injecting Clock

public final class CourseFactory {

    private final Clock clock;

    public CourseFactory(
            Clock clock
    ) {
        this.clock =
                clock;
    }

    public Course create(...) {
        Instant createdAt =
                Instant.now(
                        clock
                );

        ...
    }
}

Production:

new CourseFactory(
        Clock.systemUTC()
);

Test:

Clock fixedClock =
        Clock.fixed(
                Instant.parse(
                        "2026-08-08T12:00:00Z"
                ),
                ZoneOffset.UTC
        );

Now time is deterministic।


External Inputs Are Often Dependencies

Examples:

Current time
Random generator
File system
Database
Network client
Message publisher
Email sender

If important behavior depends on these, explicit dependencies can improve control and testing।


Do Not Inject Every Standard Library Call

Overengineering:

StringTrimmer
IntegerParser
ListFactory
MathCalculator

just so every call is replaceable।

Not every operation needs abstraction।

Focus on meaningful external effects and boundaries।


Global State

Consider:

public final class GlobalRepositories {

    public static CourseRepository courseRepository;
}

Then anywhere:

GlobalRepositories
        .courseRepository
        .save(
                course
        );

This creates hidden dependencies।


Problems with Global Mutable State

  • Dependencies are not visible in constructors
  • Tests can interfere with each other
  • Initialization order matters
  • Any code can replace the value
  • Concurrency becomes harder
  • Reasoning becomes difficult

Static Constants Are Different

This is fine:

public static final int MAX_LESSONS =
        50;

It is immutable configuration-like data।

Problem is primarily:

Mutable global collaborators or state

not static itself।


Static Utility Methods Can Also Be Fine

Example:

Math.max(...)

or a small pure helper:

SlugNormalizer.normalize(...)

can be acceptable।

The issue is not:

static = always bad

The issue is:

Hidden mutable dependencies and uncontrolled global state

Service Locator

A service locator lets classes ask a global container for dependencies।

Example:

CourseRepository repository =
        ServiceLocator.get(
                CourseRepository.class
        );

Then constructor looks dependency-free:

new CourseService()

but dependency still exists—it is hidden।


Why Hidden Dependency Is Worse

Compare:

new CourseService(
        repository
);

You immediately know what is required।

Versus:

new CourseService();

Then somewhere inside:

ServiceLocator.get(...)

You have to inspect implementation to know dependencies।

Explicit is easier to reason about।


Object Graph

An object graph is the network of objects and dependencies that make up the running application।

Example:

Main
│
├── FileCourseRepository
├── EmailCourseNotifier
│
└── CourseService
     ├── CourseRepository
     └── CourseNotifier

Main can assemble this graph।


Composition Root

The place where application dependencies are constructed and connected is often called a:

composition root

For a small console app:

Main

can be the composition root।


Example Composition Root

public class Main {

    public static void main(
            String[] args
    ) {
        CourseRepository repository =
                new FileCourseRepository(
                        Path.of(
                                "data",
                                "courses"
                        )
                );

        CourseNotifier notifier =
                new EmailCourseNotifier();

        CourseService service =
                new CourseService(
                        repository,
                        notifier
                );

        runApplication(
                service
        );
    }

    private static void runApplication(
            CourseService service
    ) {
        // application flow
    }
}

Construction is centralized।

Business classes do not create infrastructure dependencies themselves।


Why a Composition Root Helps

It answers:

Which implementation are we using?
How are components connected?
Where is configuration applied?

in one predictable place।


Dependency Direction

Suppose:

CourseService

depends on:

CourseRepository

Interface is defined around the needs of course behavior।

Implementation:

FileCourseRepository

depends on that contract।

Conceptually:

Business logic
      ↓
Repository contract
      ↑
Infrastructure implementation

Business logic does not need to know file details।


High-Level vs Low-Level

High-level policy:

Publish a course

Low-level mechanism:

Write course data to disk

Strong design tries to keep high-level policy independent from a specific low-level mechanism।


Example of Wrong Dependency Direction

public final class CourseService {

    private final FileCourseRepository repository;

}

Now high-level course behavior knows:

Storage is specifically file-based

Better:

private final CourseRepository repository;

Interfaces Should Reflect Consumer Needs

Weak giant interface:

public interface Storage {

    void saveCourse();
    void saveUser();
    void savePayment();
    void uploadVideo();
    void deleteImage();
    void backupEverything();
}

A CourseService depending on this gets many operations it does not need।


Smaller Capability Interface

public interface CourseRepository {

    Course findByCode(
            CourseCode code
    );

    void save(
            Course course
    );
}

This is cohesive and focused।


Interface Segregation in Practice

A caller should not depend on a huge interface containing unrelated methods।

If one implementation supports many capabilities, expose focused contracts where useful।

But again:

Do not split every method into a separate interface.

Balance cohesion with simplicity।


Constructor with Too Many Dependencies

Example:

public CourseService(
        CourseRepository repository,
        EnrollmentRepository enrollmentRepository,
        PaymentGateway paymentGateway,
        EmailSender emailSender,
        AnalyticsClient analyticsClient,
        MediaStorage mediaStorage,
        SearchIndexer searchIndexer,
        Clock clock
)

This is a design signal।

Ask:

Does CourseService own too many use cases?

Maybe responsibilities need to be split।


Do Not Hide Too Many Dependencies in a Context Object

Weak attempt to "fix" constructor size:

public CourseService(
        AppContext context
)

where AppContext contains:

Everything in the application

Now dependencies are hidden again।

A long constructor may expose a real cohesion problem.

Do not hide the signal।


Group Dependencies Only When They Form a Real Concept

Example:

NotificationChannels

might legitimately group:

EmailNotifier
SmsNotifier
PushNotifier

if the group itself is meaningful।

But generic:

Dependencies

or:

AppContext

usually just hides coupling।


Composition and Decorators

Composition can also add behavior around another implementation।

Suppose:

CourseRepository

has a file implementation।

You want logging:

public final class LoggingCourseRepository
        implements CourseRepository {

    private final CourseRepository delegate;

    public LoggingCourseRepository(
            CourseRepository delegate
    ) {
        this.delegate =
                delegate;
    }

    @Override
    public Course findByCode(
            CourseCode code
    ) {
        System.out.println(
                "Loading "
                + code
        );

        return delegate.findByCode(
                code
        );
    }

    @Override
    public void save(
            Course course
    ) {
        System.out.println(
                "Saving "
                + course.getCode()
        );

        delegate.save(
                course
        );
    }
}

This is composition।


Why Decorator Composition Is Useful

You can add:

Logging
Metrics
Caching
Tracing

without subclassing the concrete repository।

Example:

CourseRepository repository =
        new LoggingCourseRepository(
                new FileCourseRepository(
                        path
                )
        );

Avoid Deep Wrapper Chains Without Need

This can become hard to reason about:

Retrying
→ Logging
→ Caching
→ Metrics
→ Tracing
→ Secure
→ FileRepository

Composition is powerful, but excessive layering also creates complexity।

Use it where behavior is real and justified।


Inheritance Still Has Valid Uses

Examples:

IOException
        extends Exception

or framework extension points।

Inheritance is useful when:

Subtype really satisfies the parent contract

and substituting child for parent makes semantic sense।


Liskov Intuition

Without going deep into formal principles:

If:

Dog extends Animal

then code expecting Animal should reasonably work with Dog

If subclass must constantly say:

UnsupportedOperationException

for inherited methods, inheritance is probably wrong।


Bad Inheritance Example

public class ReadOnlyRepository
        extends FileCourseRepository {

    @Override
    public void save(
            Course course
    ) {
        throw new UnsupportedOperationException();
    }
}

If parent contract promises saving, this subtype breaks expectations।

Better design may use a smaller read-only interface।


Read-Only Capability

public interface CourseReader {

    Course findByCode(
            CourseCode code
    );
}

Then read-only implementation can implement only what it supports।

Again, create this only if the distinction genuinely exists।


Dependency Lifecycle

Some dependencies are short-lived:

InputStream

Some are long-lived:

Repository
HTTP client
Service object

The composition root often owns long-lived dependency lifecycle।

Short-lived resources are usually opened and closed near use।


Do Not Construct Expensive Dependencies Per Method Call

Weak:

public void publish(
        CourseCode code
) {
    DatabaseConnection connection =
            createConnection();

    ...
}

if the application architecture already has a reusable repository or connection pool।

Let infrastructure lifecycle be owned at the appropriate level।


Complete Example

We will create:

CourseRepository
CourseNotifier
CourseService
InMemoryCourseRepository
ConsoleCourseNotifier
Main

CourseRepository.java

package io.liveklass.course;

public interface CourseRepository {

    Course findByCode(
            CourseCode code
    );

    void save(
            Course course
    );
}

CourseNotifier.java

package io.liveklass.course;

public interface CourseNotifier {

    void published(
            Course course
    );
}

CourseService.java

package io.liveklass.course;

public final class CourseService {

    private final CourseRepository repository;
    private final CourseNotifier notifier;

    public CourseService(
            CourseRepository repository,
            CourseNotifier notifier
    ) {
        if (repository == null) {
            throw new IllegalArgumentException(
                    "Course repository is required."
            );
        }

        if (notifier == null) {
            throw new IllegalArgumentException(
                    "Course notifier is required."
            );
        }

        this.repository =
                repository;

        this.notifier =
                notifier;
    }

    public void publish(
            CourseCode courseCode
    ) {
        if (courseCode == null) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        Course course =
                requireCourse(
                        courseCode
                );

        course.publish();

        repository.save(
                course
        );

        notifier.published(
                course
        );
    }

    private Course requireCourse(
            CourseCode courseCode
    ) {
        Course course =
                repository.findByCode(
                        courseCode
                );

        if (course == null) {
            throw new IllegalStateException(
                    "Course not found: "
                    + courseCode
                    + "."
            );
        }

        return course;
    }
}

InMemoryCourseRepository.java

package io.liveklass.course.storage;

import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import io.liveklass.course.CourseRepository;

import java.util.HashMap;
import java.util.Map;

public final class InMemoryCourseRepository
        implements CourseRepository {

    private final Map<CourseCode, Course> courses =
            new HashMap<>();

    @Override
    public Course findByCode(
            CourseCode code
    ) {
        return courses.get(
                code
        );
    }

    @Override
    public void save(
            Course course
    ) {
        courses.put(
                course.getCode(),
                course
        );
    }
}

ConsoleCourseNotifier.java

package io.liveklass.course.notification;

import io.liveklass.course.Course;
import io.liveklass.course.CourseNotifier;

public final class ConsoleCourseNotifier
        implements CourseNotifier {

    @Override
    public void published(
            Course course
    ) {
        System.out.println(
                "Course published: "
                + course.getTitle()
        );
    }
}

Main.java

package io.liveklass;

import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import io.liveklass.course.CourseNotifier;
import io.liveklass.course.CourseRepository;
import io.liveklass.course.CourseService;
import io.liveklass.course.notification.ConsoleCourseNotifier;
import io.liveklass.course.storage.InMemoryCourseRepository;

public class Main {

    public static void main(
            String[] args
    ) {
        CourseRepository repository =
                new InMemoryCourseRepository();

        CourseNotifier notifier =
                new ConsoleCourseNotifier();

        CourseService service =
                new CourseService(
                        repository,
                        notifier
                );

        Course course =
                new Course(
                        new CourseCode(
                                "JAVA-OOP"
                        ),
                        "Java and OOP Foundation"
                );

        course.addLesson(
                new Lesson(
                        1L,
                        "Introduction",
                        "Content"
                )
        );

        repository.save(
                course
        );

        service.publish(
                new CourseCode(
                        "JAVA-OOP"
                )
        );
    }
}

What Main Owns

Main decides:

Which repository?
Which notifier?
How are they connected?

CourseService only knows the contracts।

This is clean composition।


What Happens When Storage Changes?

Replace:

new InMemoryCourseRepository()

with:

new FileCourseRepository(
        path
)

CourseService remains unchanged।


What Happens When Notification Changes?

Replace:

new ConsoleCourseNotifier()

with:

new EmailCourseNotifier(...)

Again:

CourseService does not change

This is the practical value of dependency design।


Common Mistakes

Constructing Dependencies Inside Business Classes

Creates tight coupling।


Using Setter Injection for Mandatory Collaborators

Allows partially initialized objects।


Creating Interfaces for Every Class

Adds unnecessary abstraction।


Depending on Concrete Infrastructure When a Real Boundary Exists

Leaks implementation details into high-level code।


Using Inheritance Only for Code Reuse

Creates incorrect subtype relationships।


Using Global Mutable Service References

Hides dependencies and complicates testing।


Replacing Constructors with a Giant AppContext

Hides excessive coupling instead of solving it।


Injecting Tiny Pure Functions for No Reason

Not every operation needs replaceability।


Creating Too Many Decorator Layers

Can make simple behavior difficult to trace।


Assuming DI Requires a Framework

Constructor injection is plain Java।


Practice Exercises

Exercise 1: Remove Hard-Coded Dependency

Refactor:

public final class EnrollmentService {

    private final FileEnrollmentRepository repository =
            new FileEnrollmentRepository(
                    Path.of(
                            "data"
                    )
            );
}

so the repository is supplied from outside।


Exercise 2: Choose an Interface Boundary

Which of these are strong candidates for an interface?

CourseRepository
EmailAddress
PaymentGateway
CourseCode
Clock

Explain why।


Exercise 3: Composition vs Inheritance

Refactor:

public class CourseService
        extends EmailSender {
}

into composition।


Exercise 4: Test Dependency

Create:

RecordingCourseNotifier

that counts how many times:

published(...)

is called।

Use it to verify a service interaction।


Exercise 5: Remove Global State

Refactor:

GlobalServices.courseRepository

into constructor dependencies।


Exercise 6: Composition Root

Write a Main method that constructs:

FileCourseRepository
ConsoleCourseNotifier
CourseService

and wires them together।


Predict the Better Design

Question 1

Which is better for a required repository?

public CourseService() {
}

then:

service.setRepository(
        repository
);

or:

public CourseService(
        CourseRepository repository
) {
}

Answer

Constructor injection is usually better because the object cannot exist without its required repository।


Question 2

Should CourseService generally extend FileCourseRepository to reuse repository methods?

Answer

No।

A course service is not a file repository।

Use composition।


Question 3

Does dependency injection require Spring?

Answer

No।

Passing dependencies through constructors is already dependency injection।


Question 4

Should every concrete helper receive a matching interface?

Answer

No।

Interfaces are useful at meaningful boundaries, not as a mandatory pair for every class।


Question 5

Why is:

CourseRepository

a strong abstraction?

Answer

It represents a persistence capability with multiple meaningful possible implementations and keeps storage mechanics away from business logic।


True or False

  1. A dependency is another object a class relies on.
  2. Constructor injection requires a framework.
  3. Required dependencies are good candidates for constructor injection.
  4. Every Java class should have an interface.
  5. Composition models "has-a" relationships.
  6. Inheritance should be used whenever two classes share code.
  7. Global mutable collaborators create hidden dependencies.
  8. Main can act as a composition root.
  9. A long constructor may indicate low cohesion.
  10. Hiding all dependencies inside AppContext automatically fixes coupling.

Answers

1. True
2. False
3. True
4. False
5. True
6. False
7. True
8. True
9. True
10. False

Knowledge Check

Question 1

What is a dependency?

Question 2

What is dependency injection?

Question 3

Why is constructor injection useful?

Question 4

Why should required dependencies often be final?

Question 5

When is an interface a useful dependency boundary?

Question 6

Why should interfaces not be created mechanically?

Question 7

What is composition?

Question 8

What is the conceptual difference between composition and inheritance?

Question 9

Why is composition often safer than inheritance for collaborators?

Question 10

How does dependency design improve testing?

Question 11

What is global mutable state?

Question 12

Why is a service locator considered a hidden dependency?

Question 13

What is an object graph?

Question 14

What is a composition root?

Question 15

What can a constructor with many unrelated dependencies indicate?


Knowledge Check Answers

Answer 1

An object or service another class needs to perform its responsibility।

Answer 2

Supplying a class's dependencies from outside instead of constructing them internally।

Answer 3

It makes required collaborators explicit and prevents creating partially initialized objects।

Answer 4

Because required collaborator references usually should remain stable for the lifetime of the object।

Answer 5

When it represents a meaningful capability that can have different implementations or separates high-level code from infrastructure।

Answer 6

Unnecessary interfaces increase complexity without adding useful flexibility or separation।

Answer 7

Building object behavior by holding and collaborating with other objects।

Answer 8

Composition represents "has-a" relationships; inheritance represents genuine "is-a" subtype relationships।

Answer 9

It avoids inheriting unrelated behavior, lowers coupling, and allows collaborators to be replaced independently।

Answer 10

Tests can provide lightweight deterministic implementations instead of real file systems, networks, or external services।

Answer 11

State or collaborators accessible globally and changeable from many places in the application।

Answer 12

The class still depends on services but does not declare those dependencies in its constructor or API।

Answer 13

The connected set of objects and dependencies that make up a running application।

Answer 14

The place where application objects are created, configured, and wired together।

Answer 15

The class may own too many responsibilities or coordinate too many unrelated concerns।


Lesson Summary

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

  • Dependencies are collaborators required by a class
  • Hard-coded dependencies create tight coupling
  • Constructor injection makes dependencies explicit
  • Dependency injection is plain Java and does not require Spring
  • Required dependencies are strong candidates for constructor injection
  • final dependency fields keep collaborator references stable
  • Interfaces are valuable at meaningful boundaries such as repositories and gateways
  • Not every class needs an interface
  • Concrete dependencies are acceptable when abstraction adds no real value
  • Composition represents "has-a" collaboration
  • Inheritance represents a stronger "is-a" relationship
  • Composition is usually preferable to inheritance used only for code reuse
  • Replaceable dependencies make testing easier
  • In-memory fakes can replace external infrastructure during tests
  • External factors such as time can sometimes be explicit dependencies
  • Global mutable collaborators hide dependencies
  • Service locator patterns also hide dependencies
  • Main can act as a simple composition root
  • High-level business code should avoid depending directly on low-level infrastructure mechanisms
  • Interfaces should remain cohesive and focused
  • Very large constructors can reveal class responsibility problems
  • Dependency context objects should not be used merely to hide coupling
  • Strong dependency design aims for:
Explicit collaborators
Clear ownership
Small boundaries
Replaceable infrastructure
Simple composition

Next Lesson

পরবর্তী lesson:

Refactoring a Small Java Application

আমরা একটি intentionally poorly designed Java application নেব এবং step-by-step refactor করব:

  • Package organization
  • Strong value objects
  • Encapsulation
  • Immutable data
  • Equality
  • Records
  • Clean methods
  • Repository abstraction
  • Constructor dependencies
  • Composition
  • Exception boundaries
  • Removing god classes
  • Building a clean final application structure