Professional Java Practices

Refactoring a Small Java Application

ReadingPreview

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 String identifiers
  • 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:

  1. Introduce strong domain types
  2. Encapsulate course state
  3. Move business rules into Course
  4. Introduce repository contract
  5. Move storage out
  6. Simplify service
  7. Introduce clean package structure
  8. Add read-only record output
  9. 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

  1. Refactoring should generally preserve intended behavior.
  2. Working code never needs refactoring.
  3. Raw strings can sometimes hide useful domain concepts.
  4. Domain entities should expose all fields publicly for flexibility.
  5. Repository abstraction can separate persistence mechanics from business logic.
  6. Every class needs an interface before it is clean.
  7. Records are useful for immutable snapshots.
  8. Constructor injection makes dependencies more explicit.
  9. Business rules based entirely on entity state often belong near that entity.
  10. 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
  • CourseCode centralizes 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() and hashCode()
  • Records
  • Clean method design
  • Domain behavior
  • Repository boundaries
  • Constructor injection
  • Composition
  • Code-smell identification
  • Final professional Java design review