Professional Java Practices

Clean Methods and Class Design

ReadingPreview

You are viewing a free preview lesson.

Lesson Overview

Java syntax জানা আর maintainable Java code লেখা এক জিনিস নয়।

একটি program compile করতে পারে, output-ও ঠিক দিতে পারে, কিন্তু codebase তবুও difficult হতে পারে যদি:

  • Methods অনেক বড় হয়
  • এক method অনেক unrelated কাজ করে
  • Class অনেক responsibility নেয়
  • Names vague হয়
  • Boolean parameters meaning hide করে
  • Business rules scattered থাকে
  • Comments দিয়ে confusing code explain করতে হয়
  • Data এক object-এ থাকে, behavior অন্য জায়গায় ছড়িয়ে থাকে

Professional Java code-এর একটি major goal হলো:

Make the code easy to understand,
easy to change,
and difficult to misuse.

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

  • Single responsibility
  • Cohesion
  • Intention-revealing names
  • Small focused methods
  • Guard clauses
  • Method parameters
  • Boolean blindness
  • Command-query separation
  • Avoiding hidden side effects
  • Avoiding god classes
  • Moving behavior to the right object
  • Comments vs expressive code
  • Extract method refactoring
  • Designing readable application flows

Learning Objectives

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

  • Method responsibility identify করতে
  • Long methodsকে smaller cohesive methods-এ refactor করতে
  • Better method এবং class names choose করতে
  • Guard clauses ব্যবহার করতে
  • Excessive parameters identify করতে
  • Boolean parameter problems explain করতে
  • Query এবং command operations distinguish করতে
  • God class detect করতে
  • Behaviorকে better owner-এর কাছে move করতে
  • Comments-এর বদলে expressive code ব্যবহার করতে
  • A procedural methodকে cleaner object-oriented design-এ refactor করতে

Clean Code Does Not Mean Short Code

Clean code মানে শুধু:

কম line

না।

এই code short:

if (c != null && c.getS() == 1 && c.getL().size() > 0) {
    r.save(c);
}

কিন্তু readable নয়।

Longer version:

if (
        course != null
        && course.isReadyToPublish()
) {
    repository.save(
            course
    );
}

এখানে code-এর intent অনেক clearer।

Goal:

Minimum confusion

not:

Minimum characters

A Method Should Have a Clear Purpose

Consider:

public void processCourse(
        Course course
) {
    // Validate title
    // Save course
    // Send email
    // Generate report
    // Update analytics
}

Method name:

processCourse()

কিছুই clearly বলে না।

Methodটি অনেক responsibilityও নিচ্ছে।


Better: Separate Intentions

validateCourse(
        course
);

repository.save(
        course
);

notificationService.notifyCourseCreated(
        course
);

Now each operation communicates a clear purpose।


Single Responsibility

Single responsibility মানে:

A method or class should have one coherent reason to change.

এটা literal rule নয় যে:

Every method must do one line
Every class must have one method

Rather:

Related behavior should stay together.
Unrelated concerns should not be mixed.

Example: Mixed Responsibilities

public void createCourse(
        String code,
        String title,
        long price
) throws IOException {
    if (
            code == null
            || code.isBlank()
    ) {
        throw new IllegalArgumentException();
    }

    Course course =
            new Course(
                    code,
                    title,
                    price
            );

    Files.writeString(
            Path.of(
                    "courses",
                    code + ".txt"
            ),
            title
    );

    System.out.println(
            "Course created"
    );
}

This method handles:

Input validation
Domain creation
File persistence
Console output

Too many concerns।


Better Separation

public void createCourse(
        CourseCode code,
        String title,
        long price
) {
    Course course =
            new Course(
                    code,
                    title,
                    price
            );

    repository.save(
            course
    );
}

Elsewhere:

CourseRepository

handles persistence।

Console/UI layer handles presentation।


Cohesion

A cohesive class has methods and fields that belong to one concept।

High cohesion:

Course

contains:

title
status
lessons

addLesson()
publish()
changeTitle()

These all belong to course behavior।


Low Cohesion

public class CourseManager {

    public void publishCourse() {
    }

    public void calculateTax() {
    }

    public void sendEmail() {
    }

    public void resizeImage() {
    }

    public void parseCsv() {
    }
}

These responsibilities have little relation।

The class is likely becoming a dumping ground।


Naming Is Part of Design

Weak names:

doWork()
process()
handle()
manage()
executeStuff()
data()
thing()

They hide intent।

Better:

publishCourse()
calculatePrice()
findCourseByCode()
validateEnrollment()
archiveCourse()

A good name tells the reader:

What this code means

Method Names Should Describe Behavior

Weak:

course.update();

Update what?

Better:

course.changeTitle(
        newTitle
);

or:

course.publish();

Specific verbs make APIs easier to use correctly।


Boolean Method Names

For boolean-returning methods, names should read like questions।

Good:

isPublished()
hasLessons()
canPublish()
containsCourse()
isEmpty()

Weak:

published()
checkCourse()
validate()

because return meaning may be unclear।


Avoid Misleading Names

Bad:

public boolean validateCourse(
        Course course
) {
    repository.save(
            course
    );

    return true;
}

A method named:

validate

should not unexpectedly persist data।

Names should match behavior।


Method Size

There is no magical correct line count।

A method is probably too large when:

  • You need comments to divide it into sections
  • It performs multiple levels of abstraction
  • You cannot describe it with one clear sentence
  • Variables from the beginning are still manipulated far later
  • Testing individual rules is difficult

Long Method Example

public void publishCourse(
        Course course
) {
    if (course == null) {
        throw new IllegalArgumentException(
                "Course is required."
        );
    }

    if (
            course.getTitle() == null
            || course.getTitle().isBlank()
    ) {
        throw new IllegalStateException(
                "Course title is required."
        );
    }

    if (
            course.getLessons()
                    .isEmpty()
    ) {
        throw new IllegalStateException(
                "Course must have lessons."
        );
    }

    for (
            Lesson lesson
            : course.getLessons()
    ) {
        if (
                lesson.getContent() == null
                || lesson.getContent()
                        .isBlank()
        ) {
            throw new IllegalStateException(
                    "Every lesson must have content."
            );
        }
    }

    course.setStatus(
            CourseStatus.PUBLISHED
    );

    repository.save(
            course
    );

    System.out.println(
            "Course published."
    );
}

Several concerns are mixed।


Move Rules to the Object That Owns Them

The Course knows:

Whether it can be published

Better:

public void publish() {
    if (lessons.isEmpty()) {
        throw new IllegalStateException(
                "Course must have at least one lesson."
        );
    }

    boolean incompleteLesson =
            lessons.stream()
                    .anyMatch(
                            lesson ->
                                    !lesson.hasContent()
                    );

    if (incompleteLesson) {
        throw new IllegalStateException(
                "Every lesson must have content."
        );
    }

    status =
            CourseStatus.PUBLISHED;
}

Service:

public void publishCourse(
        CourseCode courseCode
) {
    Course course =
            requireCourse(
                    courseCode
            );

    course.publish();

    repository.save(
            course
    );
}

Much clearer।


Keep the Same Level of Abstraction

Consider:

public void publishCourse(
        CourseCode code
) {
    Course course =
            repository.findByCode(
                    code
            );

    if (course == null) {
        throw new CourseNotFoundException(
                code
        );
    }

    if (
            course.getLessons()
                    .stream()
                    .anyMatch(
                            lesson ->
                                    lesson.getContent()
                                            .trim()
                                            .length()
                                    == 0
                    )
    ) {
        throw new IllegalStateException();
    }

    course.publish();

    repository.save(
            course
    );
}

The method mixes:

High-level workflow
Low-level content validation

Better:

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

    course.publish();

    repository.save(
            course
    );
}

Now method reads like a workflow।


Extract Method

Suppose:

Course course =
        repository.findByCode(
                code
        );

if (course == null) {
    throw new CourseNotFoundException(
            code
    );
}

appears repeatedly।

Extract:

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

    if (course == null) {
        throw new CourseNotFoundException(
                code
        );
    }

    return course;
}

Then:

Course course =
        requireCourse(
                code
        );

Extract for Meaning, Not Just Reuse

A method does not need to be reused to deserve extraction।

Example:

if (
        course.getStatus()
                == CourseStatus.DRAFT
        && !course.getLessons()
                .isEmpty()
        && course.getTitle()
                .length()
                >= 10
) {
}

Can become:

if (
        course.isReadyForReview()
) {
}

Even if only used once, readability improves।


Guard Clauses

Guard clauses reject invalid conditions early।

Nested style:

public void enroll(
        User user,
        Course course
) {
    if (user != null) {
        if (course != null) {
            if (!course.isArchived()) {
                // real logic
            }
        }
    }
}

Harder to read।


Guard Clause Style

public void enroll(
        User user,
        Course course
) {
    if (user == null) {
        throw new IllegalArgumentException(
                "User is required."
        );
    }

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

    if (course.isArchived()) {
        throw new IllegalStateException(
                "Archived course cannot accept enrollment."
        );
    }

    // main flow
}

Main logic stays at a lower indentation level।


Guard Clauses Are Especially Useful for Preconditions

Examples:

Required parameter missing
Invalid state
Unauthorized operation
Unsupported value
Already completed operation

Reject early, then continue with the happy path।


Avoid Excessive Nesting

Weak:

if (course != null) {
    if (course.isPublished()) {
        if (user != null) {
            if (!alreadyEnrolled) {
                // enrollment
            }
        }
    }
}

Better:

requireCourse(
        course
);

requireUser(
        user
);

if (!course.isPublished()) {
    throw new IllegalStateException(
            "Course is not published."
    );
}

if (alreadyEnrolled) {
    throw new IllegalStateException(
            "User is already enrolled."
    );
}

// enrollment

Method Parameters

A method with too many parameters becomes hard to understand and easy to misuse।

Example:

createCourse(
        "JAVA",
        "Java",
        499000,
        true,
        false,
        8,
        "BN",
        false
);

What does:

true
false
8
false

mean?

You need to inspect the signature।


Boolean Blindness

Consider:

createCourse(
        title,
        true
);

Does true mean:

Published?
Featured?
Free?
Visible?

The call site hides meaning।


Better Than Boolean Parameters

Instead of:

course.setPublished(
        true
);

prefer behavior:

course.publish();

Instead of:

sendNotification(
        user,
        false
);

perhaps use:

sendSilentNotification(
        user
);

or a meaningful enum/config type।


Enum Instead of Boolean Modes

Weak:

exportCourse(
        course,
        true
);

Better:

exportCourse(
        course,
        ExportFormat.FULL
);

Now intent is visible।


Parameter Object

When several values belong together:

createCourse(
        code,
        title,
        description,
        price,
        language,
        duration
);

a request object may help:

CreateCourseCommand command

Example:

public record CreateCourseCommand(
        CourseCode code,
        String title,
        String description,
        long priceInPaisa,
        String language,
        int durationInWeeks
) {
}

Then:

createCourse(
        command
);

Do Not Create Parameter Objects for Every Two Parameters

This:

findCourse(
        CourseCode code,
        boolean includeArchived
);

does not automatically require a new class।

Use parameter objects when values form a meaningful concept or call sites become hard to understand।


Prefer Strong Types

Weak:

void moveLesson(
        long courseId,
        long lessonId,
        long position
)

All parameters are long

Accidental swap compiles।

Better where appropriate:

void moveLesson(
        CourseId courseId,
        LessonId lessonId,
        LessonPosition position
)

Strong types make invalid calls harder।


Command vs Query

A useful design principle:

Command → changes state
Query   → returns information

Examples:

Command:

course.publish();
repository.save(course);

Query:

course.isPublished();
repository.findByCode(code);

Command-Query Separation

A method ideally should not both:

Mutate state
and
Return surprising unrelated information

Weak:

Course publishAndReturnPreviousCourse(
        Course course
)

Better to separate responsibilities when practical।


Not an Absolute Rule

Some operations naturally change state and return useful result।

Example:

Map.put(
        key,
        value
)

returns previous value।

Or:

Files.deleteIfExists(
        path
)

returns whether deletion occurred।

The principle is about predictability, not rigid dogma।


Hidden Side Effects

This looks like a query:

Course getCourse(
        CourseCode code
)

But imagine it also:

Updates last-accessed timestamp
Writes audit file
Sends analytics event

Now the method has hidden side effects।

Readers assume getters/query methods are cheap and observational unless documented otherwise।


Name Side Effects Clearly

Instead of:

getOrCreateCourse(...)

if creation is significant, make it obvious।

Or split:

findCourse(...)
createCourse(...)

depending on desired semantics।


Avoid God Classes

A god class knows or does too much।

Example:

public class LiveKlassManager {

    public void createCourse() {
    }

    public void enrollUser() {
    }

    public void processPayment() {
    }

    public void sendEmail() {
    }

    public void uploadImage() {
    }

    public void generateReport() {
    }

    public void backupDatabase() {
    }
}

This class becomes a central dependency for everything।


Problems with God Classes

They create:

  • Low cohesion
  • High coupling
  • Difficult tests
  • Frequent merge conflicts
  • Huge constructors
  • Hard-to-understand changes
  • Fragile code

If one class changes for ten unrelated reasons, it probably owns too much।


Split by Responsibility

Instead:

CourseService
EnrollmentService
PaymentService
NotificationService
MediaStorage
ReportGenerator

Each class owns a coherent capability।


Avoid "Manager" as an Automatic Class Name

Names such as:

CourseManager
UserManager
DataManager
SystemManager

can become vague containers।

Sometimes Manager is legitimate, but ask:

What does this class actually do?

Maybe better names are:

CourseService
CourseCatalog
CoursePublisher
EnrollmentRegistry

Service Classes Can Become God Classes Too

Even a class named:

CourseService

can become too large:

create
update
publish
archive
import
export
image upload
search
billing
analytics
recommendations

Feature name alone does not guarantee cohesion।

Split when clear sub-responsibilities emerge।


Move Behavior to the Right Object

Procedural code often asks for data and then makes decisions elsewhere।

Example:

if (
        course.getStatus()
                == CourseStatus.DRAFT
        && course.getLessons()
                .size()
                > 0
) {
    course.setStatus(
            CourseStatus.PUBLISHED
    );
}

This logic belongs to:

Course

Better:

course.publish();

Tell, Don't Ask

A useful object-oriented idea:

Instead of:

Ask object for all its internal state
Decide outside
Modify object

prefer:

Tell object what you want it to do
Let it enforce its rules

Example:

course.publish();

instead of manually manipulating status।


But Do Not Put Everything into Domain Objects

This would be bad:

course.sendEmail();
course.saveToDatabase();
course.uploadVideo();

These require external infrastructure and do not belong to the Course entity।

Good domain behavior:

course.publish();
course.addLesson();
course.changeTitle();

Infrastructure:

repository.save(course);
notificationService.notifyPublished(course);

Data Ownership Helps Decide Method Placement

Ask:

Who owns the information needed for this rule?

If a rule depends entirely on:

Course status
Course lessons

then Course is probably a good owner।

If it needs:

Payment gateway
Database
Another aggregate
Current user permissions

an application/service layer may be more appropriate।


Comments vs Expressive Code

Weak:

// Check if course has at least one lesson
if (
        course.getLessons()
                .size()
        > 0
) {
}

The comment repeats the code।

Better:

if (
        course.hasLessons()
) {
}

No comment needed।


Comments Should Explain Why

Useful comment:

// Keep legacy course codes uppercase because external exports
// use the code as a case-sensitive integration identifier.

This explains a non-obvious reason।

Less useful:

// Convert course code to uppercase
code =
        code.toUpperCase();

The code already says that।


Avoid Commenting Bad Names Instead of Fixing Them

Weak:

// Calculates whether course is ready to publish
boolean x() {
}

Better:

boolean isReadyToPublish() {
}

Comments Can Become Stale

Code changes, comment does not:

// Maximum 10 lessons
if (
        lessons.size()
        >= 50
) {
}

Now the comment is misleading।

Executable code is the source of truth।

Use comments for reasoning that code cannot express well।


Avoid Magic Values

Weak:

if (
        lessons.size()
        >= 50
) {
}

If 50 has domain meaning:

private static final int MAX_LESSONS =
        50;

Then:

if (
        lessons.size()
        >= MAX_LESSONS
) {
}

Intent becomes clearer।


Avoid Clever One-Liners

Weak:

return c != null && c.getS() == DRAFT && !c.getL().isEmpty();

Better:

return course != null
        && course.isDraft()
        && course.hasLessons();

Readable code wins over compressed code।


Long Conditional Expressions

Suppose:

if (
        course.getStatus()
                == CourseStatus.DRAFT
        && !course.getLessons()
                .isEmpty()
        && course.getTitle()
                .length()
                >= 10
        && course.getPriceInPaisa()
                >= 0
) {
}

Extract:

if (
        course.isReadyForReview()
) {
}

Then put the rule where it belongs।


Avoid Negative Logic When Positive Reads Better

Harder:

if (
        !course.isNotPublished()
) {
}

Better:

if (
        course.isPublished()
) {
}

Double negatives increase cognitive load।


Temporary Variables Can Improve Readability

You do not need to chain everything।

Dense:

return repository.findAll()
        .stream()
        .filter(c -> c.getStatus() == PUBLISHED)
        .filter(c -> c.getLessons().size() > 3)
        .map(Course::getTitle)
        .sorted()
        .toList();

Readable alternative:

List<Course> courses =
        repository.findAll();

return courses.stream()
        .filter(
                Course::isPublished
        )
        .filter(
                course ->
                        course.getLessons()
                                .size()
                        > 3
        )
        .map(
                Course::getTitle
        )
        .sorted()
        .toList();

Clarity is more important than minimizing local variables।


Keep Variables Close to Usage

Weak:

String normalizedTitle =
        title.strip();

// 100 lines later

course.changeTitle(
        normalizedTitle
);

If possible, calculate values close to where they are used।

This reduces the amount of state the reader must remember।


Avoid Reusing Variables for Different Meanings

Bad:

String value =
        request.getTitle();

value =
        value.strip();

value =
        repository.save(
                value
        );

If the semantic meaning changes, use meaningful variables।

Example:

String normalizedTitle =
        request.title()
                .strip();

Return Early

Instead of:

public Course findPublished(
        CourseCode code
) {
    Course result =
            null;

    Course course =
            repository.findByCode(
                    code
            );

    if (course != null) {
        if (course.isPublished()) {
            result =
                    course;
        }
    }

    return result;
}

Use:

public Course findPublished(
        CourseCode code
) {
    Course course =
            repository.findByCode(
                    code
            );

    if (course == null) {
        return null;
    }

    if (!course.isPublished()) {
        return null;
    }

    return course;
}

Less nesting, easier flow।


Avoid Output Parameters

Weak Java API:

void findCourse(
        CourseCode code,
        List<Course> result
)

where method modifies result

Better:

Course findCourse(
        CourseCode code
)

or:

List<Course> findCourses(...)

Return values communicate data flow more clearly।


Do Not Mutate Inputs Unexpectedly

Weak:

public void normalizeTitles(
        List<String> titles
) {
    titles.replaceAll(
            String::strip
    );
}

Caller may not expect its list to change।

If mutation is intended, name/document it clearly।

Otherwise return a new list:

public List<String> normalizedTitles(
        List<String> titles
) {
    return titles.stream()
            .map(
                    String::strip
            )
            .toList();
}

Constructor Size Can Reveal Class Problems

Suppose:

public CourseService(
        CourseRepository repository,
        EnrollmentRepository enrollments,
        PaymentGateway payments,
        EmailSender emails,
        MediaStorage media,
        Analytics analytics,
        SearchIndexer indexer,
        ReportGenerator reports,
        AuditLogger auditLogger
)

Nine dependencies may indicate:

CourseService owns too much

Not always—but it is a strong signal to inspect cohesion।


Dependency Count Is a Signal, Not a Rule

A coordinating application service might legitimately have several collaborators।

Do not split classes only to hit an arbitrary number।

Ask:

Do these dependencies support one coherent use case?

Class Names Should Match Responsibilities

Suppose class only publishes courses.

Instead of:

CourseService

you may eventually use:

CoursePublisher

if that improves clarity।

But do not prematurely create:

CourseCreator
CourseUpdater
CoursePublisher
CourseArchiver

when one small coherent CourseService is easier।

Balance matters।


Refactoring Example

Consider this procedural class:

public final class CourseService {

    private final CourseRepository repository;

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

    public void publish(
            String code
    ) {
        if (
                code == null
                || code.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Code required."
            );
        }

        Course course =
                repository.findByCode(
                        new CourseCode(
                                code
                        )
                );

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

        if (
                course.getStatus()
                != CourseStatus.DRAFT
        ) {
            throw new IllegalStateException(
                    "Invalid state."
            );
        }

        if (
                course.getLessons()
                        .isEmpty()
        ) {
            throw new IllegalStateException(
                    "No lessons."
            );
        }

        for (
                Lesson lesson
                : course.getLessons()
        ) {
            if (
                    lesson.getContent() == null
                    || lesson.getContent()
                            .isBlank()
            ) {
                throw new IllegalStateException(
                        "Incomplete lesson."
                );
            }
        }

        course.setStatus(
                CourseStatus.PUBLISHED
        );

        repository.save(
                course
        );
    }
}

It works, but service knows too much about course internals।


Step 1: Use Strong Input Type

Instead of:

publish(
        String code
)

use:

publish(
        CourseCode code
)

Now invalid course code format is handled by CourseCode


Step 2: Extract Required Lookup

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

    if (course == null) {
        throw new CourseNotFoundException(
                code
        );
    }

    return course;
}

Step 3: Move Publication Rules into Course

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."
        );
    }

    boolean incompleteLesson =
            lessons.stream()
                    .anyMatch(
                            lesson ->
                                    !lesson.hasContent()
                    );

    if (incompleteLesson) {
        throw new IllegalStateException(
                "Every lesson must have content."
        );
    }

    status =
            CourseStatus.PUBLISHED;
}

Step 4: Give Lesson Its Own Behavior

Instead of:

lesson.getContent() == null
|| lesson.getContent().isBlank()

use:

lesson.hasContent()

Implementation:

public boolean hasContent() {
    return content != null
            && !content.isBlank();
}

Final Service

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 publish(
            CourseCode code
    ) {
        Course course =
                requireCourse(
                        code
                );

        course.publish();

        repository.save(
                course
        );
    }

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

        if (course == null) {
            throw new CourseNotFoundException(
                    code
            );
        }

        return course;
    }
}

Now the method reads:

Find required course
Publish it
Save it

That is a clean application workflow।


Why the Refactored Version Is Better

The service no longer knows:

How publication eligibility is calculated
How lesson completeness is determined

Course owns course rules।

Lesson owns lesson-specific knowledge।

CourseRepository owns persistence।

Each responsibility has a clearer home।


Avoid Premature Extraction

Do not take this:

return title.strip();

and create:

TitleWhitespaceRemovalStrategy

Clean design does not mean maximum abstraction।

Create abstractions when they reduce real complexity।


Duplication vs Wrong Abstraction

Two similar lines are not always a problem।

Sometimes duplicating a small simple expression is better than forcing unrelated concepts into one generic helper।

Bad abstraction:

validateThing(
        value,
        1,
        true,
        false,
        "COURSE"
);

Better:

validateCourseTitle(
        title
);

or put validation inside a stronger domain type।


Refactor When You Understand the Pattern

A useful rule:

First make it work clearly.
Notice repeated concepts.
Then extract meaningful abstractions.

Do not predict every future reuse before it exists।


Method Design Checklist

Before finalizing a method, ask:

  • Can I describe this method in one sentence?
  • Does the name describe its effect?
  • Are invalid conditions rejected early?
  • Is nesting reasonable?
  • Are parameters meaningful at the call site?
  • Are there hidden side effects?
  • Does this logic belong in another object?
  • Is the method mixing infrastructure and domain behavior?
  • Are comments compensating for unclear code?

Class Design Checklist

Ask:

  • What concept does this class represent?
  • What state does it own?
  • What behavior belongs with that state?
  • Does it have one coherent responsibility?
  • Does it depend on too many unrelated components?
  • Does it expose internal state unnecessarily?
  • Are callers making decisions that the class itself should own?
  • Would a smaller class improve cohesion?
  • Am I splitting things without a meaningful reason?

Common Mistakes

Making Methods Tiny Without Meaning

Ten one-line methods can be harder to follow than one clear method।


Using Generic Names

process(), handle(), and manage() often hide intent।


Too Many Boolean Parameters

Call sites become cryptic।


Long Nested Conditionals

Increase cognitive load।


Putting Domain Rules in Services Only

Entities become passive data bags।


Putting Infrastructure Inside Entities

Creates wrong dependencies।


Creating God Classes

One class starts owning unrelated capabilities।


Writing Comments That Repeat the Code

Better naming usually helps more।


Abstracting Before the Pattern Exists

Creates unnecessary complexity।


Treating Line Count as the Measure of Cleanliness

Readability and cohesion matter more।


Practice Exercises

Exercise 1: Rename Methods

Improve these names:

processCourse()
doIt()
check()
manageUser()
handleData()

Choose names based on specific imagined behavior।


Exercise 2: Replace Boolean Parameter

Refactor:

void exportCourse(
        Course course,
        boolean full
)

into a clearer API।


Exercise 3: Guard Clauses

Refactor:

if (user != null) {
    if (course != null) {
        if (course.isPublished()) {
            enroll(
                    user,
                    course
            );
        }
    }
}

using guard clauses।


Exercise 4: Move Behavior

Given:

if (
        order.getStatus()
                == OrderStatus.DRAFT
        && order.getItems()
                .size()
                > 0
) {
    order.setStatus(
            OrderStatus.SUBMITTED
    );
}

Move this rule to a meaningful method on Order


Exercise 5: Identify God Class

A class has methods:

createCourse
deleteCourse
chargeCreditCard
sendPasswordReset
resizeAvatar
generateInvoice

Explain how you would split responsibilities।


Exercise 6: Refactor Comments

Replace:

// Check whether the course has lessons
if (
        course.getLessons()
                .size()
        > 0
) {
}

with more expressive code।


Predict the Better Design

Question 1

Which is clearer?

course.setPublished(
        true
);

or:

course.publish();

Answer

course.publish();

It expresses domain intent and can enforce publication rules।


Question 2

Which is usually clearer?

send(
        user,
        true
);

or:

sendUrgentNotification(
        user
);

Answer

The intention-revealing method is clearer when urgency is the real distinction।


Question 3

Where should a rule based entirely on:

Course status
Course lessons

usually live?

Answer

Usually inside Course, because it owns that state and can protect its invariants।


Question 4

Should Course directly save itself to PostgreSQL?

Answer

Usually no।

Persistence is an infrastructure/repository responsibility।


Question 5

Is a 30-line method automatically bad?

Answer

No।

Its cohesion, abstraction level, readability, and responsibility matter more than an arbitrary line count।


True or False

  1. Every clean method must be under five lines.
  2. Good names reduce the need for explanatory comments.
  3. Guard clauses can reduce nesting.
  4. Boolean parameters are always wrong.
  5. A god class usually has low cohesion.
  6. Domain objects should contain every infrastructure dependency.
  7. Hidden side effects make APIs harder to reason about.
  8. Extracted methods must always be reused.
  9. Strong types can reduce parameter mistakes.
  10. More abstraction always means cleaner code.

Answers

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

Knowledge Check

Question 1

What does single responsibility mean?

Question 2

What is cohesion?

Question 3

Why are intention-revealing names important?

Question 4

What is a guard clause?

Question 5

Why can boolean parameters be confusing?

Question 6

When is a parameter object useful?

Question 7

What is command-query separation?

Question 8

What is a hidden side effect?

Question 9

What is a god class?

Question 10

What does "move behavior to the right object" mean?

Question 11

What should comments usually explain?

Question 12

Why should long conditions sometimes be extracted into named methods?

Question 13

Why are strong parameter types useful?

Question 14

Should every repeated line immediately become an abstraction?

Question 15

What is the main goal of clean method and class design?


Knowledge Check Answers

Answer 1

A method or class should own one coherent responsibility or reason to change rather than mixing unrelated concerns।

Answer 2

The degree to which a class's state and behavior belong to the same focused concept।

Answer 3

They let readers understand behavior without decoding implementation details।

Answer 4

An early check that rejects invalid conditions before the main flow continues।

Answer 5

At the call site, true or false often does not communicate what mode or behavior it selects।

Answer 6

When several parameters form one meaningful concept or a method call becomes hard to understand due to many related arguments।

Answer 7

A design principle that distinguishes operations that change state from operations that return information, helping make side effects predictable।

Answer 8

A state change or external action performed by a method whose name or apparent purpose does not make that effect clear।

Answer 9

A class that owns too many unrelated responsibilities and becomes a central dependency for much of the application।

Answer 10

Place rules close to the state and concept that actually owns the knowledge required to enforce them।

Answer 11

Primarily why a non-obvious decision exists, not simply what obvious code already does।

Answer 12

A meaningful method name can communicate the business rule directly and reduce cognitive load।

Answer 13

They distinguish semantically different values at compile time and reduce accidental argument swaps।

Answer 14

No. Extract only when the abstraction improves meaning, cohesion, reuse, or maintainability।

Answer 15

To make software easier to understand, safer to change, and harder to misuse।


Lesson Summary

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

  • Clean code মানে minimum line count নয়; minimum confusion
  • Methods should have clear coherent purposes
  • Classes should maintain high cohesion
  • Names are part of API design
  • Intention-revealing methods improve readability
  • Guard clauses reduce unnecessary nesting
  • Method size should be judged by responsibility and abstraction level
  • Long logic can often be improved through meaningful extraction
  • Extracted methods do not need to be reused to provide value
  • Boolean parameters can create ambiguous call sites
  • Enums, stronger methods, or parameter objects can improve intent
  • Strong types can prevent accidental argument swaps
  • Commands and queries should have predictable behavior
  • Hidden side effects make APIs harder to reason about
  • God classes create low cohesion and high coupling
  • Domain rules should usually live near the state they protect
  • Infrastructure concerns should stay outside domain entities
  • Comments should explain non-obvious reasons rather than repeat code
  • Named constants improve magic-value readability
  • Behavior should be moved to the object with the relevant knowledge
  • Abstraction should reduce complexity, not manufacture it
  • Clean Java design balances:
Readability
Cohesion
Encapsulation
Clear responsibility
Simple dependencies

Next Lesson

পরবর্তী lesson:

Dependency Design and Composition

আমরা শিখব:

  • What a dependency is
  • Constructor dependencies
  • Dependency injection without frameworks
  • Depending on interfaces at useful boundaries
  • Composition over unnecessary inheritance
  • Replacing hard-coded collaborators
  • Testability through dependency design
  • Avoiding service locators and global state
  • Designing small, explicit object graphs