Professional Java Practices
Refactoring a Small Java Application
You are viewing a free preview lesson.
Lesson Overview
এই lesson-এ আমরা নতুন concept খুব বেশি introduce করব না।
বরং এখন পর্যন্ত শেখা professional Java practices একসঙ্গে apply করব।
আমরা শুরু করব একটি intentionally poorly designed application দিয়ে।
Applicationটি course create এবং publish করতে পারে।
কিন্তু code-এর মধ্যে থাকবে:
- Everything in one class
- Raw
Stringidentifiers - Public mutable state
- Direct file handling
- Hard-coded dependencies
- Long methods
- Boolean flags
- Weak exception handling
- Mutable collection exposure
- No repository abstraction
- No clear package boundaries
তারপর আমরা step-by-step refactor করব।
Final design-এ থাকবে:
Packages
CourseCode value object
Course entity
CourseStatus enum
CourseSummary record
Repository abstraction
In-memory repository
Constructor injection
Clean methods
Explicit exceptions
Composition root
Goal হলো শুধু final code দেখা নয়।
Goal হলো বুঝতে শেখা:
কোন design problem দেখলে
কী ধরনের refactoring চিন্তা করতে হবে?
Learning Objectives
এই lesson শেষে আপনি পারবেন:
- Poor Java design smells identify করতে
- Large procedural class split করতে
- Raw primitives/strings থেকে strong types introduce করতে
- Public mutable state encapsulate করতে
- Business rules domain object-এর কাছে move করতে
- Infrastructureকে repository boundary-এর পেছনে নিতে
- Constructor injection introduce করতে
- Recordsকে read-only snapshots হিসেবে ব্যবহার করতে
- Package structure improve করতে
- Refactoring-এর সময় behavior preserve করতে
The Starting Application
ধরুন আমাদের application:
- Course create করে
- Course publish করে
- Course list করে
- Disk-এ simple text file save করে
সব code এক class-এ।
Bad Version
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class CourseManager {
public List<CourseData> courses =
new ArrayList<>();
public void course(
String code,
String title,
long price,
boolean published
) {
if (
code == null
|| code.trim()
.length()
== 0
) {
throw new RuntimeException(
"bad code"
);
}
if (
title == null
|| title.trim()
.length()
== 0
) {
throw new RuntimeException(
"bad title"
);
}
if (price < 0) {
throw new RuntimeException(
"bad price"
);
}
for (
CourseData c
: courses
) {
if (
c.code.equalsIgnoreCase(
code
)
) {
throw new RuntimeException(
"exists"
);
}
}
CourseData c =
new CourseData();
c.code =
code.trim()
.toUpperCase();
c.title =
title.trim();
c.price =
price;
c.published =
published;
courses.add(
c
);
try {
Files.createDirectories(
Path.of(
"data"
)
);
Files.writeString(
Path.of(
"data",
c.code
+ ".txt"
),
c.code
+ "|"
+ c.title
+ "|"
+ c.price
+ "|"
+ c.published
);
} catch (
IOException e
) {
throw new RuntimeException(
e
);
}
}
public void publish(
String code
) {
CourseData found =
null;
for (
CourseData c
: courses
) {
if (
c.code.equalsIgnoreCase(
code
)
) {
found =
c;
}
}
if (found == null) {
throw new RuntimeException(
"not found"
);
}
if (found.published) {
throw new RuntimeException(
"already published"
);
}
if (
found.title == null
|| found.title.length()
< 3
) {
throw new RuntimeException(
"invalid title"
);
}
found.published =
true;
try {
Files.writeString(
Path.of(
"data",
found.code
+ ".txt"
),
found.code
+ "|"
+ found.title
+ "|"
+ found.price
+ "|"
+ found.published
);
} catch (
IOException e
) {
throw new RuntimeException(
e
);
}
System.out.println(
"published"
);
}
public List<CourseData> getCourses() {
return courses;
}
public static class CourseData {
public String code;
public String title;
public long price;
public boolean published;
}
}
Does the Bad Version Work?
Possibly।
That is important।
Poor design does not always mean:
Code does not run
Often it means:
Code becomes increasingly expensive and risky to change
Refactoring is about improving internal design while preserving intended behavior।
Identify the Problems
Before changing anything, inspect the code।
We can identify several design smells।
Problem 1: CourseManager Does Everything
It handles:
Validation
Course creation
Duplicate checking
Course state
Persistence
Path creation
Serialization
Console output
Course lookup
Collection ownership
This is low cohesion।
Problem 2: Raw String Course Code
Every method receives:
String code
Therefore every location may need to remember:
Trim it
Uppercase it
Validate it
Compare case-insensitively
The course-code concept is duplicated across the application।
Problem 3: Public Mutable Fields
public String code;
public String title;
public long price;
public boolean published;
Any caller can write:
course.price =
-500;
course.code =
null;
The object cannot protect itself।
Problem 4: Boolean State
boolean published
works only while lifecycle is:
Published
Not published
But what if later we need:
DRAFT
REVIEW
PUBLISHED
ARCHIVED
An enum models domain state better।
Problem 5: Boolean Parameter
course(
code,
title,
price,
true
);
What does true mean at the call site?
It also allows creation directly as published, bypassing a publication lifecycle।
Problem 6: Duplicate Business Logic
Course file writing exists in both:
course(...)
publish(...)
Changes to persistence require editing multiple methods।
Problem 7: Infrastructure Mixed with Business Logic
This:
Files.writeString(...)
is inside the same class deciding:
Whether a course may publish
Storage mechanism and domain policy are tightly coupled।
Problem 8: Weak Exceptions
throw new RuntimeException(
"exists"
);
Caller cannot easily distinguish:
Duplicate course
Course not found
Storage failure
Invalid state
Problem 9: Mutable Collection Exposure
public List<CourseData> getCourses() {
return courses;
}
Caller:
manager.getCourses()
.clear();
can destroy internal state।
Problem 10: Search Continues After Match
for (...) {
if (...) {
found =
c;
}
}
Once a course is found, continuing the loop is unnecessary।
Problem 11: Naming
Method:
course(...)
does not clearly say what it does।
Better:
createCourse(...)
Problem 12: Console Output Inside Business Logic
System.out.println(
"published"
);
Publishing should not require a console।
This makes reuse harder।
Refactoring Strategy
Do not rewrite everything randomly।
A safer sequence:
- Introduce strong domain types
- Encapsulate course state
- Move business rules into
Course - Introduce repository contract
- Move storage out
- Simplify service
- Introduce clean package structure
- Add read-only record output
- Assemble dependencies in
Main
Step 1: Introduce CourseCode
Instead of repeating:
trim()
toUpperCase()
equalsIgnoreCase()
create one type।
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;
}
}
Now:
new CourseCode(
" java-oop "
)
always becomes:
JAVA-OOP
What Did This Refactoring Remove?
We no longer need repeated:
equalsIgnoreCase(...)
trim()
toUpperCase()
blank checks
everywhere।
CourseCode guarantees:
If an instance exists, its value is valid and normalized.
Step 2: Replace Boolean with Enum
package io.liveklass.course;
public enum CourseStatus {
DRAFT,
REVIEW,
PUBLISHED,
ARCHIVED
}
Now state is explicit।
Instead of:
published =
true;
we can model meaningful transitions।
Step 3: Encapsulate Course
Instead of:
CourseData
with public fields, create a real domain class।
package io.liveklass.course;
public final class Course {
private final CourseCode code;
private final long priceInPaisa;
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;
}
public CourseCode getCode() {
return code;
}
public String getTitle() {
return title;
}
public long getPriceInPaisa() {
return priceInPaisa;
}
public CourseStatus getStatus() {
return status;
}
}
Now no caller can directly write:
course.priceInPaisa =
-100;
Step 4: Move Publication Rule into Course
Bad version decides publication outside the course:
if (
found.published
) {
...
}
if (
found.title.length()
< 3
) {
...
}
found.published =
true;
This is course behavior।
Move it।
Add publish()
public void publish() {
if (
status
== CourseStatus.PUBLISHED
) {
throw new IllegalStateException(
"Course is already published."
);
}
if (
status
!= CourseStatus.DRAFT
&& status
!= CourseStatus.REVIEW
) {
throw new IllegalStateException(
"Course cannot be published from status "
+ status
+ "."
);
}
if (
title.length()
< 3
) {
throw new IllegalStateException(
"Course title is too short for publication."
);
}
status =
CourseStatus.PUBLISHED;
}
Now caller simply says:
course.publish();
Why Is This Better?
The service does not need to know:
Which statuses can publish?
How long must title be?
How status changes?
The Course owns the data needed to enforce these rules।
Step 5: Introduce Meaningful Exceptions
Create:
CourseNotFoundException
package io.liveklass.course;
public final class CourseNotFoundException
extends RuntimeException {
public CourseNotFoundException(
CourseCode courseCode
) {
super(
"Course not found: "
+ courseCode
+ "."
);
}
}
And:
DuplicateCourseException
package io.liveklass.course;
public final class DuplicateCourseException
extends RuntimeException {
public DuplicateCourseException(
CourseCode courseCode
) {
super(
"Course already exists: "
+ courseCode
+ "."
);
}
}
Step 6: Introduce Repository Contract
Business code should not know:
Files
Paths
Serialization
Define capability:
package io.liveklass.course;
import java.util.List;
public interface CourseRepository {
Course findByCode(
CourseCode courseCode
);
void save(
Course course
);
List<Course> findAll();
}
Why Put the Interface Near the Feature?
CourseRepository describes what course application logic needs।
Infrastructure implementations depend on this contract।
This keeps the feature API cohesive।
Step 7: Create In-Memory Repository
For now we remove file complexity from the refactoring exercise।
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
) {
return courses.get(
courseCode
);
}
@Override
public void save(
Course course
) {
courses.put(
course.getCode(),
course
);
}
@Override
public List<Course> findAll() {
return new ArrayList<>(
courses.values()
);
}
}
What Happened to File Storage?
It is not deleted conceptually।
We changed:
Business contract
from:
Must save using Files.writeString()
to:
Must save through CourseRepository
Later we can plug in:
FileCourseRepository
without changing CourseService।
Step 8: Create a Focused Service
Now application workflow becomes much simpler।
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 publishCourse(
CourseCode code
) {
Course course =
requireCourse(
code
);
course.publish();
repository.save(
course
);
}
public List<Course> findAll() {
return repository.findAll();
}
private Course requireCourse(
CourseCode code
) {
Course course =
repository.findByCode(
code
);
if (course == null) {
throw new CourseNotFoundException(
code
);
}
return course;
}
}
Compare Old and New Publishing Flow
Old:
Find manually
Check null
Check published boolean
Check title
Set boolean
Write file
Print result
New:
Course course =
requireCourse(
code
);
course.publish();
repository.save(
course
);
This is much easier to read।
Step 9: Do Not Return Mutable Entities If Not Needed
Current:
public List<Course> findAll()
allows callers to receive actual mutable domain objects।
Sometimes that is okay internally।
But for a read-only presentation API, a snapshot is cleaner।
Introduce:
CourseSummary
CourseSummary Record
package io.liveklass.course;
public record CourseSummary(
CourseCode code,
String title,
long priceInPaisa,
CourseStatus status
) {
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."
);
}
title =
title.strip();
}
}
Add Snapshot Method to Course
public CourseSummary summary() {
return new CourseSummary(
code,
title,
priceInPaisa,
status
);
}
Now course can expose immutable read data।
Change Service Query
public List<CourseSummary> findAll() {
return repository.findAll()
.stream()
.map(
Course::summary
)
.toList();
}
Callers cannot use a summary to mutate the stored course।
Step 10: Improve Package Structure
Final structure:
src/main/java/
└── io/liveklass/
├── Main.java
└── course/
├── Course.java
├── CourseCode.java
├── CourseStatus.java
├── CourseSummary.java
├── CourseRepository.java
├── CourseService.java
├── CourseNotFoundException.java
├── DuplicateCourseException.java
└── storage/
└── InMemoryCourseRepository.java
This is simple but meaningful।
Step 11: Composition Root
Main decides concrete dependencies।
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.storage.InMemoryCourseRepository;
public class Main {
public static void main(
String[] args
) {
CourseRepository repository =
new InMemoryCourseRepository();
CourseService service =
new CourseService(
repository
);
service.createCourse(
new CourseCode(
"JAVA-OOP"
),
"Java and OOP Foundation",
499_000L
);
service.createCourse(
new CourseCode(
"BACKEND"
),
"Backend Development",
799_000L
);
service.publishCourse(
new CourseCode(
"JAVA-OOP"
)
);
for (
CourseSummary course
: service.findAll()
) {
System.out.println(
course
);
}
}
}
Final Course.java
Complete version:
package io.liveklass.course;
public final class Course {
private final CourseCode code;
private final long priceInPaisa;
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;
}
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 publish() {
if (
status
== CourseStatus.PUBLISHED
) {
throw new IllegalStateException(
"Course is already published."
);
}
if (
status
!= CourseStatus.DRAFT
&& status
!= CourseStatus.REVIEW
) {
throw new IllegalStateException(
"Course cannot be published from status "
+ status
+ "."
);
}
if (
title.length()
< 3
) {
throw new IllegalStateException(
"Course title is too short for publication."
);
}
status =
CourseStatus.PUBLISHED;
}
public CourseSummary summary() {
return new CourseSummary(
code,
title,
priceInPaisa,
status
);
}
public CourseCode getCode() {
return code;
}
public String getTitle() {
return title;
}
public long getPriceInPaisa() {
return priceInPaisa;
}
public CourseStatus getStatus() {
return status;
}
}
What Improved?
Let's compare responsibilities।
Before
CourseManager owned:
Validation
State
Storage
Serialization
Search
Console
Collection
Business rules
After
CourseCode owns:
Course-code validation and normalization
Course owns:
Course state and behavior
CourseRepository owns:
Persistence contract
InMemoryCourseRepository owns:
In-memory persistence implementation
CourseService owns:
Application workflow
CourseSummary owns:
Read-only course snapshot
Main owns:
Dependency assembly
This is higher cohesion।
Refactoring Did Not Mean "Create More Classes"
The final version has more types।
But each type is smaller and has clearer responsibility।
The goal is not:
More classes
The goal is:
Better boundaries
If five extra classes do not create meaningful boundaries, they are not automatically an improvement।
File Repository Can Return Later
Because service depends on:
CourseRepository
we can later create:
FileCourseRepository
from Module 6।
Then Main changes:
CourseRepository repository =
new FileCourseRepository(
Path.of(
"data",
"courses"
)
);
CourseService remains untouched।
That is a major design improvement over the original class।
Behavior-Preserving Refactoring
Real refactoring should ideally preserve behavior while structure changes।
A safe process:
Small change
Compile
Test
Small change
Compile
Test
Do not perform a huge rewrite blindly if the application already matters।
Useful Refactoring Sequence
A practical sequence is:
1. Characterize existing behavior
2. Add tests where possible
3. Rename unclear concepts
4. Extract strong values
5. Encapsulate state
6. Move behavior
7. Extract boundaries
8. Simplify orchestration
9. Reorganize packages
10. Remove dead code
Refactor vs Rewrite
Refactor:
Change internal structure
Preserve behavior
Rewrite:
Replace implementation substantially
Potentially rebuild behavior
A rewrite may sometimes be justified, but it carries more risk।
Do not call every rewrite a refactoring।
Smell: Primitive Obsession
Original:
String code
everywhere।
Refactored:
CourseCode
This is an example of addressing:
primitive obsession
where domain concepts are represented only by generic primitives or strings despite having meaningful rules।
Do Not Wrap Every Primitive
This does not mean:
Every String needs a class
Every int needs a record
Use strong types where the domain meaning or validation provides real value।
Smell: Anemic Data Bag
Original:
CourseData
had public fields and no behavior।
All decisions happened elsewhere।
Moving:
publish()
changeTitle()
into Course gives the domain object responsibility for its invariants।
Smell: Feature Envy
If CourseService constantly asks:
course.getStatus()
course.getTitle()
course.getLessons()
and makes decisions based entirely on those values, the service may be doing work that belongs to Course।
This smell is sometimes called:
feature envy
The code is more interested in another object's data than its own responsibility।
Smell: Shotgun Surgery
Original file writing appeared in multiple methods।
If storage format changes, many methods need edits।
This is a form of:
shotgun surgery
where one conceptual change requires touching many unrelated locations।
Repository extraction centralizes persistence behavior।
Smell: Long Parameter List
Original:
course(
code,
title,
price,
published
);
This can become worse as requirements grow।
Instead of continuously adding parameters:
createCourse(
code,
title,
price,
language,
duration,
published,
featured,
...
);
a meaningful command record may eventually help।
Optional Future Improvement: Command Record
public record CreateCourseCommand(
CourseCode code,
String title,
long priceInPaisa
) {
public CreateCourseCommand {
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."
);
}
title =
title.strip();
}
}
Service:
public void createCourse(
CreateCourseCommand command
) {
...
}
But with only three parameters, this is optional।
Do not introduce it unless it improves the surrounding API।
Smell: Generic Exceptions
Original:
throw new RuntimeException(
"exists"
);
Refactored:
throw new DuplicateCourseException(
code
);
Now the type communicates failure meaning।
This makes boundary handling easier later।
Smell: Hidden Dependencies
Original class directly called:
Files.writeString(...)
The file system was a hidden dependency of business behavior।
Refactored service receives:
CourseRepository
explicitly।
Smell: Mutable Collection Exposure
Original:
return courses;
Repository now returns:
new ArrayList<>(
courses.values()
);
The caller gets a copy of the collection structure।
Service can further transform entities into immutable:
CourseSummary
records।
A Note About Repository Returning Mutable Entities
InMemoryCourseRepository stores actual Course object references।
Therefore:
Course loaded =
repository.findByCode(
code
);
returns the same mutable object stored inside the map।
For this foundation example, that is acceptable।
A more sophisticated persistence boundary may return reconstructed copies or use database transactions।
Understand the semantics rather than assuming every repository behaves identically।
Refactoring for Testability
Final service is easy to test because repository is replaceable।
Example setup:
CourseRepository repository =
new InMemoryCourseRepository();
CourseService service =
new CourseService(
repository
);
No disk is needed।
Simple Behavioral Test
CourseCode code =
new CourseCode(
"JAVA-OOP"
);
service.createCourse(
code,
"Java and OOP Foundation",
499_000L
);
service.publishCourse(
code
);
Course course =
repository.findByCode(
code
);
if (
course.getStatus()
!= CourseStatus.PUBLISHED
) {
throw new AssertionError(
"Course should be published."
);
}
Later JUnit can formalize this testing style।
Refactoring Checklist
When reviewing messy code, ask:
State
- Are fields publicly mutable?
- Can invalid state exist?
- Who owns collection mutation?
Types
- Are important domain concepts raw strings/primitives?
- Would a value object simplify repeated validation?
Methods
- Are methods too broad?
- Do names communicate intent?
- Is nesting excessive?
Classes
- Does one class own unrelated concerns?
- Is behavior located near the data it uses?
Dependencies
- Are infrastructure objects constructed inside business classes?
- Could a meaningful interface boundary help?
Errors
- Are failures distinguishable?
- Are generic exceptions hiding intent?
Packages
- Are related classes grouped together?
- Are implementation details separated where useful?
Do Not Refactor for Fashion
Bad reasoning:
I heard records are modern,
so everything becomes a record.
Or:
I heard interfaces are clean,
so every class gets an interface.
Or:
I heard microservices are scalable,
so every package becomes a service.
Refactoring should solve observed design problems।
Keep Architecture Proportional
Our final application does not need:
20 layers
Factories for every object
Event buses
Abstract service factories
Plugin systems
A small application can remain:
Domain
Service
Repository
Infrastructure implementation
Main
Simple is a feature।
Practice Exercise 1: Identify Smells
Given:
public class UserManager {
public List<User> users =
new ArrayList<>();
public void doUser(
String email,
String name,
boolean active
) {
// validate
// save file
// send email
// print console
}
}
Identify at least five design problems।
Suggested Answer
Possible issues:
Vague class/method naming
Public mutable collection
Raw email String
Boolean mode/state
Too many responsibilities
Direct persistence
Direct notification
Direct presentation
Weak encapsulation
Practice Exercise 2: Move Behavior
Given:
if (
enrollment.getStatus()
== EnrollmentStatus.ACTIVE
) {
enrollment.setStatus(
EnrollmentStatus.CANCELLED
);
}
Move this logic to:
enrollment.cancel();
and make the entity reject invalid cancellation states।
Practice Exercise 3: Extract Boundary
Given:
Files.writeString(
path,
courseData
);
inside CourseService, introduce:
CourseRepository
and move storage outside the service।
Practice Exercise 4: Strong Type
Replace repeated:
String email
validation with:
EmailAddress
value object or record।
Requirements:
- Non-null
- Non-blank
- Normalize according to a deliberate policy
- Value equality
Practice Exercise 5: Read Model
Given mutable:
Enrollment
create immutable:
EnrollmentSummary
record for displaying:
learnerId
courseCode
status
Predict the Better Refactoring
Question 1
Where should this logic usually live?
if (
course.getStatus()
== DRAFT
) {
course.setStatus(
PUBLISHED
);
}
Answer
Inside a meaningful:
course.publish();
method, where course state transitions can be enforced consistently।
Question 2
Where should:
Files.writeString(...)
usually live?
Answer
Inside an infrastructure/storage component such as a repository, not directly in domain behavior।
Question 3
Should CourseService instantiate:
new FileCourseRepository(...)
itself?
Answer
Usually no।
Receive a CourseRepository dependency from the composition root।
Question 4
Should a read-only UI need a mutable Course if only code/title/status are required?
Answer
Not necessarily।
An immutable CourseSummary record can provide a safer read model।
Question 5
Does more classes automatically mean better design?
Answer
No।
Each type should create a meaningful responsibility or boundary।
True or False
- Refactoring should generally preserve intended behavior.
- Working code never needs refactoring.
- Raw strings can sometimes hide useful domain concepts.
- Domain entities should expose all fields publicly for flexibility.
- Repository abstraction can separate persistence mechanics from business logic.
- Every class needs an interface before it is clean.
- Records are useful for immutable snapshots.
- Constructor injection makes dependencies more explicit.
- Business rules based entirely on entity state often belong near that entity.
- A total rewrite and a refactoring are always the same thing.
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 refactoring?
Question 2
Why can working code still need refactoring?
Question 3
What problem did CourseCode solve?
Question 4
Why replace a publication boolean with an enum?
Question 5
Why move publish() into Course?
Question 6
What does repository abstraction remove from CourseService?
Question 7
Why is constructor injection useful in the refactored application?
Question 8
What role does CourseSummary play?
Question 9
What is primitive obsession?
Question 10
What is a god class?
Question 11
What is feature envy?
Question 12
What is shotgun surgery?
Question 13
Why are generic RuntimeExceptions often weak application design?
Question 14
Why should refactoring happen incrementally in real systems?
Question 15
What should drive creation of new abstractions?
Knowledge Check Answers
Answer 1
Improving the internal structure of code while preserving its intended external behavior।
Answer 2
Because maintainability, readability, testability, and change safety can be poor even when current output is correct।
Answer 3
It centralized course-code validation, normalization, equality, and domain meaning।
Answer 4
An enum represents multiple explicit lifecycle states and communicates domain meaning better than a simple true/false flag।
Answer 5
Course owns the state and information required to protect publication invariants।
Answer 6
Direct knowledge of file APIs, storage location, and persistence mechanics।
Answer 7
The service receives its required storage capability explicitly and can work with different repository implementations।
Answer 8
It provides an immutable read-only snapshot of course state।
Answer 9
Overusing generic primitive types such as String, long, or boolean for concepts that have meaningful domain rules of their own।
Answer 10
A class that owns many unrelated responsibilities and becomes heavily coupled to the rest of the application।
Answer 11
When code outside an object repeatedly uses that object's data to perform behavior the object itself could reasonably own।
Answer 12
A design problem where one conceptual change requires modifying many scattered pieces of code।
Answer 13
They fail to communicate meaningful failure categories and make boundary handling harder।
Answer 14
Small behavior-preserving steps reduce risk and make regressions easier to detect।
Answer 15
Observed complexity, meaningful domain concepts, dependency boundaries, or repeated design problems—not fashion or abstract rules alone।
Lesson Summary
এই lesson-এ আমরা একটি poorly designed Java application step-by-step refactor করেছি।
আমরা দেখেছি:
- Working code can still have serious design problems
- Refactoring changes structure while aiming to preserve behavior
- Large god classes should be split by coherent responsibility
- Raw domain strings can become strong value objects
CourseCodecentralizes validation and normalization- Boolean lifecycle flags often benefit from enums
- Public mutable fields break encapsulation
- Domain entities should protect their own invariants
course.publish()is clearer than external state manipulation- Meaningful custom exceptions communicate failure intent
- Repository interfaces separate application logic from storage mechanics
- Constructor injection makes dependencies explicit
- In-memory implementations improve testability
- Records can provide immutable read models
- Package organization should reflect meaningful feature boundaries
- Composition root owns concrete dependency assembly
- Primitive obsession, god classes, feature envy, and shotgun surgery are useful design smells to recognize
- Refactoring should be incremental and proportional
- More classes or interfaces do not automatically mean better architecture
The most important refactoring question is not:
How can I make this code look more advanced?
It is:
Which responsibility belongs where,
and how can I make that relationship clearer?
Next Lesson
পরবর্তী lesson:
Module Practice and Assessment
Module 7-এর final assessment-এ আপনি একটি messy Java applicationকে independently redesign করবেন।
Assessment cover করবে:
- Package organization
- Immutability
- Defensive copying
equals()andhashCode()- Records
- Clean method design
- Domain behavior
- Repository boundaries
- Constructor injection
- Composition
- Code-smell identification
- Final professional Java design review