Professional Java Practices
Practice and Assessment
আপনি একটি free preview lesson দেখছেন।
Project Overview
এই module-এর final assessment-এ আমরা একটি small course-management application design করব।
এইবার focus file I/O নয়।
Focus হলো:
Professional Java design
আপনাকে এমন code লিখতে হবে যা:
- Easy to understand
- Easy to change
- Difficult to misuse
- Properly encapsulated
- Clearly organized
- Explicit about dependencies
এই assessment cover করবে:
- Package organization
- Strong value objects
- Immutability
- Defensive copying
equals()andhashCode()- Records
- Clean methods
- Domain behavior
- Repository abstraction
- Constructor injection
- Composition
- Code-smell identification
- Refactoring decisions
Scenario
আমরা একটি simplified learning platform তৈরি করছি।
System support করবে:
Course creation
Lesson addition
Course publication
Course lookup
Course listing
A course has:
CourseCode
Title
Price
Status
Lessons
A lesson has:
LessonId
Title
Course lifecycle:
DRAFT
→
PUBLISHED
Publication rules:
Course must contain at least one lesson
Only DRAFT course can be published
Target Project Structure
src/main/java/
└── io/liveklass/
├── Main.java
└── course/
├── Course.java
├── CourseCode.java
├── CourseStatus.java
├── CourseSummary.java
├── Lesson.java
├── LessonId.java
├── CourseRepository.java
├── CourseService.java
├── CourseNotFoundException.java
├── DuplicateCourseException.java
└── storage/
└── InMemoryCourseRepository.java
এই structure deliberately simple।
আমাদের লক্ষ্য:
Clear boundaries without unnecessary architecture
Part 1: CourseCode
CourseCode একটি value object।
Requirements:
- Cannot be null
- Cannot be blank
- Normalize using
strip() - Normalize to uppercase
- Allowed characters:
A-Z
0-9
-
- Value-based equality
- Stable hash code
- Useful
toString()
A record is a strong choice।
CourseCode.java
package io.liveklass.course;
import java.util.Locale;
public record CourseCode(
String value
) {
public CourseCode {
if (
value == null
|| value.isBlank()
) {
throw new IllegalArgumentException(
"Course code is required."
);
}
value =
value.strip()
.toUpperCase(
Locale.ROOT
);
if (
!value.matches(
"[A-Z0-9-]+"
)
) {
throw new IllegalArgumentException(
"Course code contains unsupported characters."
);
}
}
@Override
public String toString() {
return value;
}
}
Why a Record Fits
CourseCode:
Represents a value
Has stable state
Has value equality
Should not mutate
This is exactly the kind of type where a record is useful।
Equality Check
CourseCode first =
new CourseCode(
"java-oop"
);
CourseCode second =
new CourseCode(
" JAVA-OOP "
);
System.out.println(
first.equals(
second
)
);
Expected:
true
because both normalize to:
JAVA-OOP
Part 2: LessonId
A lesson identifier should not be an arbitrary long everywhere।
Create a strong value type।
LessonId.java
package io.liveklass.course;
public record LessonId(
long value
) {
public LessonId {
if (value <= 0) {
throw new IllegalArgumentException(
"Lesson id must be positive."
);
}
}
}
Why Use LessonId?
Compare:
findLesson(
12L
);
with:
findLesson(
new LessonId(
12L
)
);
The second communicates domain meaning more clearly।
It also prevents invalid:
0
negative IDs
from spreading through the application।
Part 3: Immutable Lesson
A lesson in this assessment will be immutable।
Lesson.java
package io.liveklass.course;
public final class Lesson {
private final LessonId id;
private final String title;
public Lesson(
LessonId id,
String title
) {
if (id == null) {
throw new IllegalArgumentException(
"Lesson id is required."
);
}
if (
title == null
|| title.isBlank()
) {
throw new IllegalArgumentException(
"Lesson title is required."
);
}
this.id =
id;
this.title =
title.strip();
}
public LessonId getId() {
return id;
}
public String getTitle() {
return title;
}
}
Does Lesson Need equals()?
That depends on desired semantics।
For duplicate checking in this application, we will compare lessons using:
LessonId
rather than treating the entire lesson as a value object।
Therefore Lesson does not need custom equality for this assessment।
Important lesson:
Do not override equals() just because a class exists.
Equality is a design decision।
Part 4: CourseStatus
CourseStatus.java
package io.liveklass.course;
public enum CourseStatus {
DRAFT,
PUBLISHED
}
This is clearer than:
boolean published
because the state has an explicit domain name।
Part 5: Designing Course
Course is not a record।
Why?
Because it has:
Identity
Mutable lifecycle
Controlled state transitions
Owned collection
Domain behavior
A normal class communicates this better।
Course Invariants
We want these rules:
Code is always present
Title is always present
Price is never negative
Lesson IDs are unique
Lessons cannot be added after publication
Course must contain at least one lesson before publication
Only DRAFT course can publish
Course.java
package io.liveklass.course;
import java.util.ArrayList;
import java.util.List;
public final class Course {
private final CourseCode code;
private final long priceInPaisa;
private final List<Lesson> lessons;
private String title;
private CourseStatus status;
public Course(
CourseCode code,
String title,
long priceInPaisa
) {
if (code == null) {
throw new IllegalArgumentException(
"Course code is required."
);
}
if (
title == null
|| title.isBlank()
) {
throw new IllegalArgumentException(
"Course title is required."
);
}
if (priceInPaisa < 0) {
throw new IllegalArgumentException(
"Course price cannot be negative."
);
}
this.code =
code;
this.title =
title.strip();
this.priceInPaisa =
priceInPaisa;
this.status =
CourseStatus.DRAFT;
this.lessons =
new ArrayList<>();
}
public void changeTitle(
String newTitle
) {
if (
newTitle == null
|| newTitle.isBlank()
) {
throw new IllegalArgumentException(
"Course title is required."
);
}
if (
status
== CourseStatus.PUBLISHED
) {
throw new IllegalStateException(
"Published course title cannot be changed."
);
}
title =
newTitle.strip();
}
public void addLesson(
Lesson lesson
) {
if (lesson == null) {
throw new IllegalArgumentException(
"Lesson is required."
);
}
if (
status
== CourseStatus.PUBLISHED
) {
throw new IllegalStateException(
"Cannot add lessons to a published course."
);
}
boolean duplicate =
lessons.stream()
.anyMatch(
existing ->
existing.getId()
.equals(
lesson.getId()
)
);
if (duplicate) {
throw new IllegalArgumentException(
"Lesson id already exists: "
+ lesson.getId()
+ "."
);
}
lessons.add(
lesson
);
}
public void publish() {
if (
status
!= CourseStatus.DRAFT
) {
throw new IllegalStateException(
"Only draft courses can be published."
);
}
if (lessons.isEmpty()) {
throw new IllegalStateException(
"Course must have at least one lesson before publication."
);
}
status =
CourseStatus.PUBLISHED;
}
public boolean isPublished() {
return status
== CourseStatus.PUBLISHED;
}
public CourseSummary summary() {
return new CourseSummary(
code,
title,
priceInPaisa,
status,
lessons.size()
);
}
public CourseCode getCode() {
return code;
}
public String getTitle() {
return title;
}
public long getPriceInPaisa() {
return priceInPaisa;
}
public CourseStatus getStatus() {
return status;
}
public List<Lesson> getLessons() {
return List.copyOf(
lessons
);
}
}
Defensive Collection Design
Internal collection:
private final List<Lesson> lessons;
is mutable because Course needs to add lessons।
But callers receive:
List.copyOf(
lessons
)
This gives us:
Mutable internally
Controlled through Course methods
Read-only snapshot externally
What Would Be Wrong Here?
public List<Lesson> getLessons() {
return lessons;
}
Caller could execute:
course.getLessons()
.clear();
and bypass every domain rule।
Part 6: CourseSummary
A caller listing courses does not necessarily need mutable domain objects।
Create an immutable snapshot।
CourseSummary.java
package io.liveklass.course;
public record CourseSummary(
CourseCode code,
String title,
long priceInPaisa,
CourseStatus status,
int lessonCount
) {
public CourseSummary {
if (code == null) {
throw new IllegalArgumentException(
"Course code is required."
);
}
if (
title == null
|| title.isBlank()
) {
throw new IllegalArgumentException(
"Course title is required."
);
}
if (priceInPaisa < 0) {
throw new IllegalArgumentException(
"Course price cannot be negative."
);
}
if (status == null) {
throw new IllegalArgumentException(
"Course status is required."
);
}
if (lessonCount < 0) {
throw new IllegalArgumentException(
"Lesson count cannot be negative."
);
}
title =
title.strip();
}
}
Why Return lessonCount Instead of List<Lesson>?
For a summary, caller only needs:
How many lessons?
It does not need entire lesson objects।
A read model should expose only what its consumer needs।
Part 7: Repository Contract
CourseRepository.java
package io.liveklass.course;
import java.util.List;
public interface CourseRepository {
Course findByCode(
CourseCode courseCode
);
void save(
Course course
);
List<Course> findAll();
}
This contract says nothing about:
Files
SQL
HashMap
Network
That is intentional।
Part 8: Custom Exceptions
CourseNotFoundException.java
package io.liveklass.course;
public final class CourseNotFoundException
extends RuntimeException {
public CourseNotFoundException(
CourseCode courseCode
) {
super(
"Course not found: "
+ courseCode
+ "."
);
}
}
DuplicateCourseException.java
package io.liveklass.course;
public final class DuplicateCourseException
extends RuntimeException {
public DuplicateCourseException(
CourseCode courseCode
) {
super(
"Course already exists: "
+ courseCode
+ "."
);
}
}
Why Not Use Generic RuntimeException?
Compare:
throw new RuntimeException(
"exists"
);
with:
throw new DuplicateCourseException(
courseCode
);
The second exposes a meaningful failure category।
Boundary code can later handle it specifically।
Part 9: In-Memory Repository
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.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class InMemoryCourseRepository
implements CourseRepository {
private final Map<CourseCode, Course> courses =
new LinkedHashMap<>();
@Override
public Course findByCode(
CourseCode courseCode
) {
if (courseCode == null) {
throw new IllegalArgumentException(
"Course code is required."
);
}
return courses.get(
courseCode
);
}
@Override
public void save(
Course course
) {
if (course == null) {
throw new IllegalArgumentException(
"Course is required."
);
}
courses.put(
course.getCode(),
course
);
}
@Override
public List<Course> findAll() {
return new ArrayList<>(
courses.values()
);
}
}
Why LinkedHashMap?
We want predictable insertion order for this small example।
A plain:
HashMap
would also be valid if ordering did not matter।
Part 10: Course Service
The service coordinates use cases।
It should not implement rules already owned by Course।
CourseService.java
package io.liveklass.course;
import java.util.List;
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;
}
public void createCourse(
CourseCode code,
String title,
long priceInPaisa
) {
if (
repository.findByCode(
code
)
!= null
) {
throw new DuplicateCourseException(
code
);
}
Course course =
new Course(
code,
title,
priceInPaisa
);
repository.save(
course
);
}
public void addLesson(
CourseCode courseCode,
Lesson lesson
) {
Course course =
requireCourse(
courseCode
);
course.addLesson(
lesson
);
repository.save(
course
);
}
public void publishCourse(
CourseCode courseCode
) {
Course course =
requireCourse(
courseCode
);
course.publish();
repository.save(
course
);
}
public CourseSummary findCourse(
CourseCode courseCode
) {
return requireCourse(
courseCode
)
.summary();
}
public List<CourseSummary> findAllCourses() {
return repository.findAll()
.stream()
.map(
Course::summary
)
.toList();
}
private Course requireCourse(
CourseCode courseCode
) {
if (courseCode == null) {
throw new IllegalArgumentException(
"Course code is required."
);
}
Course course =
repository.findByCode(
courseCode
);
if (course == null) {
throw new CourseNotFoundException(
courseCode
);
}
return course;
}
}
Design Review: Service Responsibility
CourseService handles:
Use-case coordination
Duplicate lookup
Required lookup
Persistence coordination
It does not handle:
How CourseCode is normalized
Whether a lesson ID is valid
Whether course can publish
How repository stores data
Those responsibilities already belong elsewhere।
Part 11: Composition Root
Main.java
package io.liveklass;
import io.liveklass.course.CourseCode;
import io.liveklass.course.CourseRepository;
import io.liveklass.course.CourseService;
import io.liveklass.course.CourseSummary;
import io.liveklass.course.Lesson;
import io.liveklass.course.LessonId;
import io.liveklass.course.storage.InMemoryCourseRepository;
public class Main {
public static void main(
String[] args
) {
CourseRepository repository =
new InMemoryCourseRepository();
CourseService service =
new CourseService(
repository
);
CourseCode javaCourse =
new CourseCode(
"java-oop"
);
service.createCourse(
javaCourse,
"Java and OOP Foundation",
499_000L
);
service.addLesson(
javaCourse,
new Lesson(
new LessonId(
1L
),
"Introduction to Java"
)
);
service.addLesson(
javaCourse,
new Lesson(
new LessonId(
2L
),
"Classes and Objects"
)
);
service.publishCourse(
javaCourse
);
for (
CourseSummary summary
: service.findAllCourses()
) {
System.out.println(
summary
);
}
}
}
Dependency Graph
Conceptually:
Main
│
├── InMemoryCourseRepository
│
└── CourseService
│
└── CourseRepository
Main knows the concrete implementation।
CourseService knows only:
CourseRepository
This is intentional dependency design।
Assessment Part 1: Package Design
Given these classes:
Course
CourseService
CourseCode
FileCourseRepository
Enrollment
EnrollmentService
Main
Which structure is stronger?
Option A
model/
service/
repository/
util/
Option B
course/
enrollment/
with feature-related code grouped together।
Answer
For this small domain-focused application, Option B is generally clearer।
It keeps feature-related code close together।
Assessment Part 2: Record or Class?
Choose the stronger default।
CourseCode
Answer:
Record
It represents an immutable value।
CourseSummary
Answer:
Record
It represents an immutable snapshot।
Course
Answer:
Normal class
It owns mutable lifecycle and behavior।
LessonId
Answer:
Record
It is a validated immutable identifier।
ShoppingCart
Assume it supports:
addItem()
removeItem()
checkout()
Answer:
Normal class
It owns lifecycle and controlled mutation।
Assessment Part 3: Find the Mutation Leak
public final class Course {
private final List<Lesson> lessons;
public Course(
List<Lesson> lessons
) {
this.lessons =
lessons;
}
public List<Lesson> getLessons() {
return lessons;
}
}
There are two obvious mutation leaks।
Problem 1: Constructor Input
Caller retains original list reference।
Fix:
this.lessons =
new ArrayList<>(
lessons
);
Problem 2: Getter Output
Caller receives internal mutable list।
Fix:
public List<Lesson> getLessons() {
return List.copyOf(
lessons
);
}
Strong Pattern
Input
→ defensive copy
Internal state
→ controlled mutation
Output
→ immutable snapshot
Assessment Part 4: Equality
Consider:
CourseCode first =
new CourseCode(
"java"
);
CourseCode second =
new CourseCode(
" JAVA "
);
Expected:
first.equals(
second
)
Result:
true
Why?
Both normalize to:
JAVA
and record equality compares the normalized component।
Assessment Part 5: Hash-Based Collections
Set<CourseCode> codes =
new HashSet<>();
codes.add(
new CourseCode(
"java"
)
);
codes.add(
new CourseCode(
"JAVA"
)
);
Expected size:
1
because:
equals() → true
hashCode() → same
Assessment Part 6: Identify the Equality Bug
public final class EmailAddress {
private final String value;
@Override
public boolean equals(
Object other
) {
if (
!(other
instanceof EmailAddress email)
) {
return false;
}
return value.equals(
email.value
);
}
}
What's missing?
Answer
hashCode()
If equality is value-based, matching hash code semantics are required for hash collections।
Assessment Part 7: Mutable Hash Key
Suppose:
Map<CourseCode, Course>
uses immutable CourseCode keys।
Good।
Now imagine CourseCode had:
setValue(...)
and its hash changed after insertion।
What could happen?
The map may no longer find the key correctly.
Therefore stable immutable keys are preferable।
Assessment Part 8: Clean Methods
Which is stronger?
Version A
public void process(
Course course,
boolean x
) {
}
Version B
public void publishCourse(
CourseCode courseCode
) {
}
Answer
Version B communicates intent much more clearly।
Assessment Part 9: Boolean Blindness
Bad:
exportCourse(
course,
true
);
Better:
exportCourse(
course,
ExportMode.FULL
);
Why?
At the call site:
FULL
communicates meaning।
true does not।
Assessment Part 10: Guard Clauses
Refactor:
if (course != null) {
if (
course.getStatus()
== CourseStatus.DRAFT
) {
course.publish();
}
}
A clearer version:
if (course == null) {
throw new IllegalArgumentException(
"Course is required."
);
}
if (
course.getStatus()
!= CourseStatus.DRAFT
) {
throw new IllegalStateException(
"Course must be draft."
);
}
course.publish();
Though if publish() already validates state, the second check should not be duplicated outside the entity।
Even better:
if (course == null) {
throw new IllegalArgumentException(
"Course is required."
);
}
course.publish();
Assessment Part 11: Move Behavior to the Owner
Given:
if (
course.getStatus()
== CourseStatus.DRAFT
&& !course.getLessons()
.isEmpty()
) {
course.setStatus(
CourseStatus.PUBLISHED
);
}
Better:
course.publish();
Why?
Course owns:
Status
Lessons
Publication invariant
Assessment Part 12: Wrong Responsibility
Should this be inside Course?
Files.writeString(
path,
content
);
Answer
Usually no।
That is persistence/infrastructure behavior।
Should this be inside Course?
public void publish()
Answer
Yes, if publication rules depend on course state and invariants।
Assessment Part 13: Hard-Coded Dependency
Bad:
public final class CourseService {
private final CourseRepository repository =
new FileCourseRepository(
Path.of(
"data"
)
);
}
Better:
public final class CourseService {
private final CourseRepository repository;
public CourseService(
CourseRepository repository
) {
this.repository =
repository;
}
}
Now caller chooses the implementation।
Assessment Part 14: Does This Require Spring?
new CourseService(
repository
);
No।
This is plain Java constructor injection।
Assessment Part 15: Choose the Interface
Which is a stronger interface?
Option A
public interface ApplicationServices {
void saveCourse();
void sendEmail();
void chargeCard();
void uploadFile();
void enrollUser();
}
Option B
public interface CourseRepository {
Course findByCode(
CourseCode code
);
void save(
Course course
);
}
Answer
Option B।
It represents one cohesive capability।
Assessment Part 16: Composition vs Inheritance
Bad:
public class CourseService
extends FileCourseRepository {
}
Why is this weak?
Because:
CourseService is not a FileCourseRepository.
It uses a repository।
Use composition:
public final class CourseService {
private final CourseRepository repository;
}
Assessment Part 17: God Class
Consider:
public class PlatformManager {
void createCourse() {
}
void publishCourse() {
}
void enrollUser() {
}
void processPayment() {
}
void sendEmail() {
}
void uploadVideo() {
}
void generateInvoice() {
}
}
This is a likely god class।
Possible split:
CourseService
EnrollmentService
PaymentService
NotificationService
MediaStorage
InvoiceService
Only introduce those boundaries if the application actually has those responsibilities।
Assessment Part 18: Comments
Which is better?
// Check if course can publish
if (
course.getStatus()
== DRAFT
&& course.getLessons()
.size()
> 0
) {
}
or:
if (
course.canPublish()
) {
}
The second is more expressive।
Comments should usually explain:
Why
not repeat obvious:
What
Assessment Part 19: toString()
A good toString() may help logging and debugging।
But should this appear?
password
accessToken
apiKey
No।
Sensitive values should not be exposed through toString()।
Assessment Part 20: Records and Collections
Consider:
public record CoursePlan(
List<Lesson> lessons
) {
}
Is this automatically deeply immutable?
Answer
No।
The list may be mutable।
The Lesson elements may also be mutable।
Better structural protection:
public CoursePlan {
lessons =
List.copyOf(
lessons
);
}
But that still does not deep-copy mutable elements।
Code Smell Challenge
Identify the problems:
public class CourseManager {
public static FileCourseRepository repo =
new FileCourseRepository(
Path.of(
"data"
)
);
public List<Course> items =
new ArrayList<>();
public void doIt(
String code,
String title,
boolean published
) {
Course course =
new Course();
course.code =
code;
course.title =
title;
course.published =
published;
items.add(
course
);
repo.save(
course
);
}
}
Possible answers:
Global mutable dependency
Public mutable collection
Vague method name
Raw String domain identifier
Boolean state
Boolean parameter
Public mutable entity fields
Invalid object states possible
Hard-coded concrete repository
No constructor injection
Service and collection ownership mixed
No duplicate rule
No meaningful exceptions
Independent Practice Project
Build a small:
Enrollment Management Application
using the same principles।
Domain Requirements
An enrollment has:
EnrollmentId
CourseCode
LearnerId
EnrollmentStatus
Statuses:
ACTIVE
COMPLETED
CANCELLED
Rules:
New enrollment starts ACTIVE
ACTIVE enrollment may complete
ACTIVE enrollment may cancel
COMPLETED cannot cancel
CANCELLED cannot complete
Suggested Types
EnrollmentId
LearnerId
EnrollmentStatus
Enrollment
EnrollmentSummary
EnrollmentRepository
InMemoryEnrollmentRepository
EnrollmentService
EnrollmentNotFoundException
Challenge 1: Strong IDs
Create:
EnrollmentId
and:
LearnerId
as validated records।
For example:
value > 0
Challenge 2: Entity
Create:
Enrollment
as a normal class।
Methods:
complete()
cancel()
Do not expose:
setStatus(...)
publicly।
Challenge 3: Read Model
Create:
EnrollmentSummary
as a record।
Challenge 4: Repository
Create:
EnrollmentRepository
with:
Enrollment findById(
EnrollmentId id
);
void save(
Enrollment enrollment
);
List<Enrollment> findAll();
Challenge 5: In-Memory Implementation
Use:
Map<EnrollmentId, Enrollment>
Why is immutable EnrollmentId useful here?
Because its:
equals()
hashCode()
remain stable।
Challenge 6: Service
Create:
EnrollmentService
with:
completeEnrollment(
EnrollmentId id
);
cancelEnrollment(
EnrollmentId id
);
Service should:
Find entity
Call domain behavior
Save entity
It should not duplicate entity transition rules।
Challenge 7: Composition Root
In:
Main
create:
InMemoryEnrollmentRepository
EnrollmentService
and wire them manually।
No framework required।
Final Design Review Questions
Before considering the exercise complete, answer these questions.
1
Can callers create an invalid identifier?
2
Can callers directly change entity status?
3
Can callers mutate internal collections?
4
Are value objects immutable?
5
Are equals() and hashCode() stable?
6
Are records being used primarily for values/snapshots?
7
Are entities responsible for their own state transitions?
8
Does the service duplicate domain rules?
9
Does the service know persistence mechanics?
10
Are required dependencies visible in constructors?
11
Can the repository implementation be replaced?
12
Are package names meaningful?
13
Are class responsibilities cohesive?
14
Are method names intention-revealing?
15
Are there unnecessary abstractions?
Professional Java Design Assessment
For each item, score:
0 = Missing or seriously incorrect
1 = Partially correct
2 = Strong
| Area | Score |
|---|---|
| Package organization | /2 |
| Strong domain types | /2 |
| Constructor validation | /2 |
| Immutability | /2 |
| Defensive copying | /2 |
| Stable equality | /2 |
Correct hashCode() | /2 |
| Appropriate use of records | /2 |
| Encapsulation | /2 |
| Domain behavior placement | /2 |
| Clean method naming | /2 |
| Guard clauses / readable flow | /2 |
| Repository abstraction | /2 |
| Constructor injection | /2 |
| Composition over unnecessary inheritance | /2 |
| Meaningful exceptions | /2 |
| No global mutable dependencies | /2 |
| Cohesive classes | /2 |
| No unnecessary architecture | /2 |
| Overall readability | /2 |
Maximum:
40
Score Interpretation
35–40
Strong professional Java foundation
29–34
Good foundation with minor design gaps
21–28
Understands the concepts but needs more refactoring practice
Below 21
Review Module 7 and rebuild the exercise
True or False
- Every domain type should be a record.
- Every class should have an interface.
- Immutable value objects are usually safe hash keys.
final List<T>automatically makes the list immutable.- Defensive copying can prevent mutation leaks.
- Records generate value-based equality.
- Mutable entity fields should automatically all participate in
hashCode(). - Domain methods can protect invariants.
- Repository abstraction can hide persistence mechanics.
- Constructor injection requires Spring.
- Composition often works better than inheritance for collaborators.
- Global mutable dependencies make dependencies less explicit.
- Small focused methods are useful even if never reused.
- Boolean arguments can make call sites ambiguous.
- Clean architecture means creating the maximum possible abstractions.
Answers
1. False
2. False
3. True
4. False
5. True
6. True
7. False
8. True
9. True
10. False
11. True
12. True
13. True
14. True
15. False
Knowledge Check
Question 1
Why is CourseCode a strong record candidate?
Question 2
Why is Course better represented as a normal class?
Question 3
What does defensive copying protect against?
Question 4
Why should mutable internal collections not be returned directly?
Question 5
What equality rule must always hold with hashCode()?
Question 6
Why are immutable keys useful in HashMap?
Question 7
What does a record automatically generate?
Question 8
Why doesn't a record guarantee deep immutability?
Question 9
What is a god class?
Question 10
What does constructor injection make explicit?
Question 11
Why depend on CourseRepository instead of FileCourseRepository inside CourseService?
Question 12
What is composition?
Question 13
What is a composition root?
Question 14
What should usually own a rule based entirely on course state?
Question 15
What should drive abstraction decisions?
Knowledge Check Answers
Answer 1
It represents one validated immutable value and naturally benefits from generated value equality and hashing।
Answer 2
Course owns identity, mutable lifecycle, controlled behavior, and invariants।
Answer 3
It prevents external mutable references from changing an object's internal state unexpectedly।
Answer 4
Callers could bypass the object's validation and mutation rules।
Answer 5
If two objects are equal according to equals(), they must return the same hashCode()।
Answer 6
Their equality and hash-related state does not change after insertion।
Answer 7
A canonical constructor, component accessors, equals(), hashCode(), and toString()।
Answer 8
A record component can reference mutable objects such as List, arrays, or mutable classes।
Answer 9
A class that owns too many unrelated responsibilities and becomes excessively coupled।
Answer 10
Which collaborators are required for an object to perform its responsibility।
Answer 11
The service should depend on the persistence capability rather than a specific storage mechanism।
Answer 12
Building behavior by collaborating with contained/dependent objects rather than inheriting from them unnecessarily।
Answer 13
The place where concrete application dependencies are created and wired together।
Answer 14
Usually the Course entity itself because it owns the state and invariant involved।
Answer 15
Real domain meaning, observed complexity, useful boundaries, and maintainability needs—not fashion or rules applied mechanically।
Module Completion Checklist
Before completing Module 7, verify that you can:
- Organize Java code into meaningful packages
- Explain package-private visibility
- Avoid making every type public
- Distinguish mutable and immutable objects
- Explain what
finaldoes and does not guarantee - Defensively copy mutable constructor inputs
- Return safe collection snapshots
- Explain shallow vs deep immutability
- Implement stable logical equality
- Explain the
equals()contract - Explain the
hashCode()contract - Use immutable values safely as
Mapkeys - Choose between a record and normal class
- Validate and normalize record components
- Write intention-revealing method names
- Use guard clauses
- Recognize boolean blindness
- Identify god classes
- Move behavior to the object that owns the relevant state
- Separate infrastructure from domain behavior
- Use constructor injection
- Explain dependency injection without frameworks
- Use interfaces at meaningful boundaries
- Prefer composition over unnecessary inheritance
- Recognize hidden global dependencies
- Assemble a small object graph in
Main - Refactor messy procedural Java into cohesive types
- Avoid unnecessary abstraction
Module Summary
এই module-এ আমরা Java syntax-এর বাইরে professional code design-এর foundation তৈরি করেছি।
আমরা শিখেছি:
- Packages code organization এবং visibility boundaries তৈরি করে
- Feature-oriented packages related code কাছাকাছি রাখতে পারে
- Package-private access unnecessary public API reduce করে
- Immutability unexpected state changes reduce করে
finalreferences referenced objectকে immutable করে না- Defensive copying ownership boundaries protect করে
List.copyOf()immutable snapshots তৈরি করতে useful- Mutable nested objects shallow immutability-এর limitation তৈরি করে
equals()logical equality define করেhashCode()hash-based collections-এর জন্য essential- Equal objects must have equal hash codes
- Mutable equality state
HashMapএবংHashSet-এ dangerous - Records concise immutable data-oriented types provide করে
- Records value objects এবং snapshots-এর strong candidates
- Records every domain entity-এর replacement নয়
- Clean methods clear intentions communicate করে
- Guard clauses nesting reduce করে
- Boolean parameters call-site meaning hide করতে পারে
- Domain behavior should stay close to the state it protects
- God classes reduce cohesion
- Infrastructure should not leak into domain objects
- Constructor injection dependencies explicit করে
- Dependency injection does not require a framework
- Interfaces are useful at real boundaries, not everywhere
- Composition models collaboration better than fake inheritance
- Global mutable dependencies create hidden coupling
Maincan act as a composition root- Refactoring should improve boundaries while preserving behavior
- More classes, interfaces, or layers do not automatically mean better software
The central principle of Module 7 is:
Make important concepts explicit,
keep responsibilities cohesive,
protect object state,
and make dependencies visible.
Module Complete
At this point, a student should no longer think only in terms of:
How do I make this Java code compile?
The better question becomes:
Where should this responsibility live?
Which state should this object protect?
What should be immutable?
What should callers be allowed to change?
Which dependencies should be explicit?
How can another developer understand this code quickly?
That shift is one of the most important transitions from learning Java syntax to writing maintainable Java software.
Next Module
পরবর্তী module হবে:
Final Project
এই project-এ আমরা পুরো course-এর concepts combine করব:
- Java fundamentals
- Classes and objects
- Encapsulation
- Inheritance and composition
- Interfaces
- Generics
- Collections
- Enums
- Exceptions
- File I/O
- Repository abstraction
- Value objects
- Records
- Immutability
- Equality
- Clean methods
- Dependency design
Student একটি complete console-based Java application design এবং implement করবে from scratch.