Object-Oriented Programming Foundations

Project: Build a Course Enrollment System

ReadingPreview

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

Project Overview

এই practice project-এ আমরা একটি ছোট console-based course enrollment system তৈরি করব।

Systemটি দিয়ে আমরা পারব:

  • Free এবং paid course তৈরি করতে
  • Course publish করতে
  • Learner তৈরি করতে
  • Published course-এ learner enroll করতে
  • Completed lesson count update করতে
  • Progress calculate করতে
  • Enrollment cancel করতে
  • Invalid state prevent করতে
  • Related objects composition দিয়ে connect করতে

এই project-এ Module 2-এর প্রায় সব গুরুত্বপূর্ণ concept ব্যবহার করা হবে:

  • Classes এবং objects
  • Fields এবং methods
  • Parameters এবং return values
  • Constructors
  • this
  • Method overloading-এর পরিবর্তে meaningful naming
  • Encapsulation
  • Access control
  • Object invariants
  • Static members
  • Static factory methods
  • Object references
  • Equality এবং hashCode()
  • toString()
  • Composition
  • Immutable value objects
  • Package organization

Project Goal

আমরা নিচের domain model তৈরি করব:

CourseCode
Course
Learner
Enrollment

Relationship:

Enrollment
├── has a Learner
└── has a Course

Responsibility:

CourseCode
    একটি valid এবং immutable course code represent করবে

Course
    Course information এবং publication state manage করবে

Learner
    Learner identity এবং profile information represent করবে

Enrollment
    একটি learner-এর একটি course-এ progress এবং enrollment state manage করবে

Domain Rules

Code লেখা শুরু করার আগে business rules define করা গুরুত্বপূর্ণ।

Course Code Rules

  • Required
  • Blank হতে পারবে না
  • Uppercase format-এ store হবে
  • Letters, numbers এবং hyphens support করবে
  • Creation-এর পরে change করা যাবে না

Valid:

JAVA
JAVA-FOUNDATION
BACKEND-2026

Invalid:

""
"   "
"-JAVA"
"JAVA-"
"JAVA COURSE"

Course Rules

  • Course code required
  • Title required
  • Title সর্বোচ্চ 150 characters
  • Total lessons zero-এর বেশি
  • Price negative হতে পারবে না
  • Free course-এর price 0
  • Paid course-এর price zero-এর বেশি
  • Course একবার publish করা যাবে
  • Published course-এর title এই simplified model-এ change করা যাবে না

Learner Rules

  • Learner ID positive হতে হবে
  • Name required
  • Email required
  • Email-এ @ থাকতে হবে
  • Email lowercase format-এ store হবে
  • Learner identity ID দ্বারা determined হবে

Enrollment Rules

  • Learner required
  • Course required
  • শুধু published course-এ enroll করা যাবে
  • New enrollment zero progress দিয়ে শুরু হবে
  • Completed lessons negative হতে পারবে না
  • Completed lessons total lessons exceed করতে পারবে না
  • Cancelled enrollment-এর progress update করা যাবে না
  • Completed enrollment cancel করা যাবে না

Recommended Project Structure

src/
└── main/
    └── java/
        └── io/
            └── liveklass/
                ├── Main.java
                ├── course/
                │   ├── Course.java
                │   └── CourseCode.java
                ├── enrollment/
                │   └── Enrollment.java
                └── learner/
                    └── Learner.java

Part 1: Design Before Coding

Code দেখার আগে নিজে নিচের questions-এর answer চিন্তা করুন।

CourseCode

  • এটি entity নাকি value object?
  • কেন immutable হওয়া উচিত?
  • Equality কোন field-এর ওপর based হবে?

Course

  • কোন fields final হওয়া উচিত?
  • কোন state mutable হওয়া দরকার?
  • Free এবং paid course creation কীভাবে clear করা যায়?
  • কেন generic setPublished(boolean) avoid করা উচিত?

Learner

  • Learner-এর equality name দিয়ে হবে, email দিয়ে হবে, নাকি ID দিয়ে?
  • Email কি toString()-এ expose করা উচিত?

Enrollment

  • Progress কার responsibility?
  • Course total lesson count duplicate করা উচিত কি?
  • Enrollment cancel হলে progress update কীভাবে prevent করা যায়?

নিজে design করার চেষ্টা করার পরে reference implementation দেখুন।


Part 2: Implement CourseCode

Path:

src/main/java/io/liveklass/course/CourseCode.java
package io.liveklass.course;

import java.util.Objects;

public final class CourseCode {

    private final String value;

    public CourseCode(
            String value
    ) {
        if (
                value == null
                || value.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        String normalizedValue =
                value.strip()
                        .toUpperCase();

        if (
                !normalizedValue.matches(
                        "[A-Z0-9]+(?:-[A-Z0-9]+)*"
                )
        ) {
            throw new IllegalArgumentException(
                    "Course code may contain only letters, numbers, and single hyphens between parts."
            );
        }

        this.value =
                normalizedValue;
    }

    public String getValue() {
        return value;
    }

    @Override
    public boolean equals(
            Object other
    ) {
        if (this == other) {
            return true;
        }

        if (
                other == null
                || getClass()
                != other.getClass()
        ) {
            return false;
        }

        CourseCode that =
                (CourseCode) other;

        return value.equals(
                that.value
        );
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                value
        );
    }

    @Override
    public String toString() {
        return value;
    }
}

CourseCode Design Review

Why Is It final?

public final class CourseCode

Subclass তৈরি করে immutability contract পরিবর্তন করা prevent করে।


Why Is the Field private final?

private final String value;
  • External caller direct access করতে পারে না
  • Constructor assignment-এর পরে reference reassign করা যায় না
  • String নিজেও immutable

Why Normalize in the Constructor?

Input:

"  java-foundation  "

Stored value:

JAVA-FOUNDATION

সব CourseCode object consistent representation ব্যবহার করবে।


Why Value-Based Equality?

new CourseCode("java")

এবং:

new CourseCode("JAVA")

normalize হওয়ার পরে same value represent করে।

তাই:

first.equals(second)

true হওয়া উচিত।


Part 3: Implement Course

Path:

src/main/java/io/liveklass/course/Course.java
package io.liveklass.course;

import java.util.Objects;

public class Course {

    public static final int MAX_TITLE_LENGTH =
            150;

    private static int totalCoursesCreated;

    private final CourseCode code;
    private final int totalLessons;
    private final long priceInPaisa;

    private String title;
    private boolean published;

    private Course(
            CourseCode code,
            String title,
            int totalLessons,
            long priceInPaisa
    ) {
        if (code == null) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        validateTitle(title);

        if (totalLessons <= 0) {
            throw new IllegalArgumentException(
                    "Total lessons must be greater than zero."
            );
        }

        if (priceInPaisa < 0) {
            throw new IllegalArgumentException(
                    "Course price cannot be negative."
            );
        }

        this.code = code;
        this.title = title.strip();
        this.totalLessons =
                totalLessons;

        this.priceInPaisa =
                priceInPaisa;

        this.published = false;

        totalCoursesCreated++;
    }

    public static Course freeCourse(
            CourseCode code,
            String title,
            int totalLessons
    ) {
        return new Course(
                code,
                title,
                totalLessons,
                0
        );
    }

    public static Course paidCourse(
            CourseCode code,
            String title,
            int totalLessons,
            long priceInPaisa
    ) {
        if (priceInPaisa <= 0) {
            throw new IllegalArgumentException(
                    "Paid course price must be greater than zero."
            );
        }

        return new Course(
                code,
                title,
                totalLessons,
                priceInPaisa
        );
    }

    public boolean changeTitle(
            String newTitle
    ) {
        if (published) {
            return false;
        }

        if (!isValidTitle(newTitle)) {
            return false;
        }

        title =
                newTitle.strip();

        return true;
    }

    public boolean publish() {
        if (published) {
            return false;
        }

        published = true;

        return true;
    }

    public boolean isFree() {
        return priceInPaisa == 0;
    }

    public CourseCode getCode() {
        return code;
    }

    public String getTitle() {
        return title;
    }

    public int getTotalLessons() {
        return totalLessons;
    }

    public long getPriceInPaisa() {
        return priceInPaisa;
    }

    public boolean isPublished() {
        return published;
    }

    public static int getTotalCoursesCreated() {
        return totalCoursesCreated;
    }

    private static void validateTitle(
            String title
    ) {
        if (!isValidTitle(title)) {
            throw new IllegalArgumentException(
                    "Course title is required and cannot exceed "
                    + MAX_TITLE_LENGTH
                    + " characters."
            );
        }
    }

    private static boolean isValidTitle(
            String title
    ) {
        return title != null
                && !title.isBlank()
                && title.strip().length()
                <= MAX_TITLE_LENGTH;
    }

    @Override
    public boolean equals(
            Object other
    ) {
        if (this == other) {
            return true;
        }

        if (
                other == null
                || getClass()
                != other.getClass()
        ) {
            return false;
        }

        Course course =
                (Course) other;

        return code.equals(
                course.code
        );
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                code
        );
    }

    @Override
    public String toString() {
        return "Course{"
                + "code="
                + code
                + ", title='"
                + title
                + '\''
                + ", totalLessons="
                + totalLessons
                + ", priceInPaisa="
                + priceInPaisa
                + ", published="
                + published
                + '}';
    }
}

Course Design Review

Why Is the Constructor Private?

private Course(...)

Caller direct constructor use করতে পারবে না।

Object creation clear static factory methods-এর মাধ্যমে হবে:

Course.freeCourse(...)

অথবা:

Course.paidCourse(...)

এতে creation intent explicit।


Why Not Use One Constructor Everywhere?

This call:

new Course(
        code,
        title,
        20,
        0
);

দেখে 0-এর meaning immediately clear নয়।

This is clearer:

Course.freeCourse(
        code,
        title,
        20
);

Why Does paidCourse() Validate Price Again?

Base constructor allows:

price >= 0

কারণ free course-এর price zero।

Paid factory-এর stronger rule:

price > 0

Different creation paths different constraints enforce করতে পারে।


Why Is Title Mutable but Code Final?

Course title editorially change হতে পারে।

Course code stable identity।

Title changes
Identity remains

Why Can Published Course Title Not Change?

এটি project-এর simplified business rule।

if (published) {
    return false;
}

Real system-এ published course title change support করা যেতে পারে।

Important point হলো ruleটি setter-এর বাইরে meaningful behavior method-এ enforce করা।


Why Is Equality Based on Code?

Course code stable identity।

এই দুইটি objects logically same course represent করতে পারে:

Course first =
        Course.freeCourse(
                new CourseCode("JAVA"),
                "Java Foundation",
                20
        );

Course second =
        Course.freeCourse(
                new CourseCode("java"),
                "Java and OOP Foundation",
                20
        );

Code normalization-এর পরে same identity।


Static Counter Limitation

private static int totalCoursesCreated;

এটি শুধু current Java process-এ created objects count করে।

এটি:

  • Database count নয়
  • Restart-এর পরে থাকবে না
  • Multiple server combine করে না
  • Thread-safe নয়

Learning purpose-এর জন্য ব্যবহার করা হয়েছে।


Part 4: Implement Learner

Path:

src/main/java/io/liveklass/learner/Learner.java
package io.liveklass.learner;

import java.util.Objects;

public final class Learner {

    private final long id;
    private final String name;
    private final String email;

    public Learner(
            long id,
            String name,
            String email
    ) {
        if (id <= 0) {
            throw new IllegalArgumentException(
                    "Learner ID must be positive."
            );
        }

        if (
                name == null
                || name.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Learner name is required."
            );
        }

        if (
                email == null
                || email.isBlank()
                || !email.contains("@")
                || email.contains(" ")
        ) {
            throw new IllegalArgumentException(
                    "A valid learner email is required."
            );
        }

        this.id = id;
        this.name = name.strip();

        this.email =
                email.strip()
                        .toLowerCase();
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    @Override
    public boolean equals(
            Object other
    ) {
        if (this == other) {
            return true;
        }

        if (
                other == null
                || getClass()
                != other.getClass()
        ) {
            return false;
        }

        Learner learner =
                (Learner) other;

        return id == learner.id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                id
        );
    }

    @Override
    public String toString() {
        return "Learner{"
                + "id="
                + id
                + ", name='"
                + name
                + '\''
                + '}';
    }
}

Learner Design Review

Why Is Learner Immutable Here?

এই simplified model-এ:

  • ID change হবে না
  • Name change support করা হয়নি
  • Email change support করা হয়নি

এই কারণে object immutable।

Real platform-এ learner profile update support করা হলে controlled mutation বা replacement strategy প্রয়োজন হতে পারে।


Why Is Equality Based on ID?

দুইজন learner-এর same name থাকতে পারে।

Nur
Nur

Email changeও হতে পারে।

Stable learner ID identity-এর stronger candidate।


Why Is Email Excluded from toString()?

Email সবসময় secret নয়, কিন্তু personal information।

Logs-এ unnecessary personal data reduce করা ভালো।

toString() শুধু debugging-এর প্রয়োজনীয় information expose করছে।


Part 5: Implement Enrollment

Path:

src/main/java/io/liveklass/enrollment/Enrollment.java
package io.liveklass.enrollment;

import io.liveklass.course.Course;
import io.liveklass.learner.Learner;

public final class Enrollment {

    private final Learner learner;
    private final Course course;

    private int completedLessons;
    private boolean active;

    private Enrollment(
            Learner learner,
            Course course
    ) {
        if (learner == null) {
            throw new IllegalArgumentException(
                    "Learner is required."
            );
        }

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

        if (!course.isPublished()) {
            throw new IllegalArgumentException(
                    "Cannot enroll in an unpublished course."
            );
        }

        this.learner = learner;
        this.course = course;

        this.completedLessons = 0;
        this.active = true;
    }

    public static Enrollment enroll(
            Learner learner,
            Course course
    ) {
        return new Enrollment(
                learner,
                course
        );
    }

    public boolean completeLesson() {
        return completeLessons(
                1
        );
    }

    public boolean completeLessons(
            int lessonCount
    ) {
        if (!active) {
            return false;
        }

        if (isCompleted()) {
            return false;
        }

        if (lessonCount <= 0) {
            return false;
        }

        int updatedCount =
                completedLessons
                + lessonCount;

        if (
                updatedCount
                > course.getTotalLessons()
        ) {
            return false;
        }

        completedLessons =
                updatedCount;

        return true;
    }

    public boolean cancel() {
        if (!active) {
            return false;
        }

        if (isCompleted()) {
            return false;
        }

        active = false;

        return true;
    }

    public int calculateRemainingLessons() {
        return course.getTotalLessons()
                - completedLessons;
    }

    public double calculateProgress() {
        return completedLessons
                * 100.0
                / course.getTotalLessons();
    }

    public boolean isCompleted() {
        return completedLessons
                == course.getTotalLessons();
    }

    public Learner getLearner() {
        return learner;
    }

    public Course getCourse() {
        return course;
    }

    public int getCompletedLessons() {
        return completedLessons;
    }

    public boolean isActive() {
        return active;
    }

    @Override
    public String toString() {
        return "Enrollment{"
                + "learnerId="
                + learner.getId()
                + ", courseCode="
                + course.getCode()
                + ", completedLessons="
                + completedLessons
                + ", totalLessons="
                + course.getTotalLessons()
                + ", active="
                + active
                + '}';
    }
}

Enrollment Design Review

Why Does It Reference Objects?

private final Learner learner;
private final Course course;

Enrollment learner এবং course data duplicate করে না।

Avoided fields:

private String learnerName;
private String learnerEmail;
private String courseTitle;
private int totalLessons;

Related objects নিজেদের state own করে।


Why Is completedLessons Stored in Enrollment?

Progress কোনো learner-এর general property নয়।

Course-এরও general property নয়।

এটি specific relationship-এর state:

Nur's progress in Java course

তাই Enrollment এটি own করে।


Why Is totalLessons Not Duplicated?

Course already owns total lesson count।

course.getTotalLessons()

Enrollment same process-এর live course reference ব্যবহার করছে।

Historical snapshot প্রয়োজন হলে design different হতে পারে।


Why Use enroll()?

Enrollment.enroll(
        learner,
        course
);

Creation intent direct constructor-এর চেয়ে clearer।

Factory future-এ additional creation rules coordinate করার জায়গা দিতে পারে।


Why Does completeLesson() Delegate?

public boolean completeLesson() {
    return completeLessons(1);
}

Core update logic duplicate করা হয়নি।

সব validation থাকে:

completeLessons(int lessonCount)

method-এ।


Why No setCompletedLessons()?

Caller arbitrary progress set করতে পারলে:

enrollment.setCompletedLessons(
        -500
);

অথবা:

enrollment.setCompletedLessons(
        10_000
);

invariant ভাঙতে পারে।

Meaningful transition:

completeLesson()
completeLessons(3)

Why Is Completion Calculated?

Stored field নেই:

private boolean completed;

Completion existing state থেকে derive করা যায়।

completedLessons
        == course.getTotalLessons()

Duplicate derived state avoid করা হয়েছে।


Part 6: Implement Main

Path:

src/main/java/io/liveklass/Main.java
package io.liveklass;

import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import io.liveklass.enrollment.Enrollment;
import io.liveklass.learner.Learner;

public class Main {

    public static void main(
            String[] args
    ) {
        Course javaCourse =
                Course.freeCourse(
                        new CourseCode(
                                "java-foundation"
                        ),
                        "Java Foundation",
                        20
                );

        boolean titleChanged =
                javaCourse.changeTitle(
                        "Java and OOP Foundation"
                );

        boolean published =
                javaCourse.publish();

        Learner nur =
                new Learner(
                        1L,
                        "Nur",
                        "NUR@EXAMPLE.COM"
                );

        Enrollment enrollment =
                Enrollment.enroll(
                        nur,
                        javaCourse
                );

        boolean firstUpdate =
                enrollment.completeLessons(
                        5
                );

        boolean secondUpdate =
                enrollment.completeLesson();

        boolean invalidUpdate =
                enrollment.completeLessons(
                        100
                );

        System.out.println(
                "Title changed: "
                + titleChanged
        );

        System.out.println(
                "Course published: "
                + published
        );

        System.out.println(
                "Total course objects created: "
                + Course
                        .getTotalCoursesCreated()
        );

        System.out.println(
                "Course: "
                + javaCourse
        );

        System.out.println(
                "Learner: "
                + nur
        );

        System.out.println(
                "Enrollment: "
                + enrollment
        );

        System.out.println(
                "First progress update: "
                + firstUpdate
        );

        System.out.println(
                "Second progress update: "
                + secondUpdate
        );

        System.out.println(
                "Invalid progress update: "
                + invalidUpdate
        );

        System.out.println(
                "Progress: "
                + "%.2f%%".formatted(
                        enrollment
                                .calculateProgress()
                )
        );

        System.out.println(
                "Remaining lessons: "
                + enrollment
                        .calculateRemainingLessons()
        );

        System.out.println(
                "Completed: "
                + enrollment.isCompleted()
        );

        System.out.println(
                "Active: "
                + enrollment.isActive()
        );
    }
}

Expected Output

Title changed: true
Course published: true
Total course objects created: 1
Course: Course{code=JAVA-FOUNDATION, title='Java and OOP Foundation', totalLessons=20, priceInPaisa=0, published=true}
Learner: Learner{id=1, name='Nur'}
Enrollment: Enrollment{learnerId=1, courseCode=JAVA-FOUNDATION, completedLessons=6, totalLessons=20, active=true}
First progress update: true
Second progress update: true
Invalid progress update: false
Progress: 30.00%
Remaining lessons: 14
Completed: false
Active: true

Part 7: Compile and Run

Project root থেকে:

javac -d out \
    src/main/java/io/liveklass/course/CourseCode.java \
    src/main/java/io/liveklass/course/Course.java \
    src/main/java/io/liveklass/learner/Learner.java \
    src/main/java/io/liveklass/enrollment/Enrollment.java \
    src/main/java/io/liveklass/Main.java

Run:

java -cp out io.liveklass.Main

macOS বা Linux-এ সব source একসঙ্গে compile করতে:

javac -d out \
    $(find src/main/java -name "*.java")

Then:

java -cp out io.liveklass.Main

IDE ব্যবহার করলে Main.main() run করা যাবে।


Part 8: Verify Object Invariants

Project run করার পরে নিচের invalid scenarios test করুন।


Test 1: Blank Course Code

new CourseCode(
        "   "
);

Expected:

IllegalArgumentException

Test 2: Invalid Course Code Format

new CourseCode(
        "JAVA COURSE"
);

Expected:

IllegalArgumentException

Test 3: Negative Course Price

Course.paidCourse(
        new CourseCode(
                "BACKEND"
        ),
        "Backend Development",
        30,
        -100
);

Expected:

IllegalArgumentException

Test 4: Zero Paid Course Price

Course.paidCourse(
        new CourseCode(
                "BACKEND"
        ),
        "Backend Development",
        30,
        0
);

Expected:

IllegalArgumentException

A free course should use:

Course.freeCourse(...)

Test 5: Enroll Before Publishing

Course course =
        Course.freeCourse(
                new CourseCode(
                        "JAVA"
                ),
                "Java Foundation",
                20
        );

Enrollment.enroll(
        learner,
        course
);

Expected:

IllegalArgumentException

Test 6: Complete Too Many Lessons

boolean updated =
        enrollment.completeLessons(
                100
        );

Expected:

false

Existing state unchanged থাকবে।


Test 7: Cancel Then Update Progress

enrollment.cancel();

boolean updated =
        enrollment.completeLesson();

Expected:

false

Test 8: Complete Then Cancel

একটি 2-lesson course তৈরি করুন।

Enrollment shortEnrollment =
        Enrollment.enroll(
                learner,
                shortCourse
        );

shortEnrollment.completeLessons(
        2
);

boolean cancelled =
        shortEnrollment.cancel();

Expected:

false

Completed enrollment cancel করা যাবে না।


Part 9: Equality Verification

Course Code Equality

CourseCode first =
        new CourseCode(
                "java-foundation"
        );

CourseCode second =
        new CourseCode(
                "JAVA-FOUNDATION"
        );

System.out.println(
        first == second
);

System.out.println(
        first.equals(second)
);

Expected:

false
true

References different।

Logical values same।


Course Equality

Course first =
        Course.freeCourse(
                new CourseCode(
                        "JAVA"
                ),
                "Java Foundation",
                20
        );

Course second =
        Course.freeCourse(
                new CourseCode(
                        "java"
                ),
                "Modern Java",
                30
        );

System.out.println(
        first.equals(second)
);

Expected:

true

কারণ equality course code-based।

এখানে একটি গুরুত্বপূর্ণ design observation আছে:

Same identity
Different state

Real system-এ same course identity-এর conflicting copies handle করার responsibility repository, persistence context, versioning, বা application architecture-এর হতে পারে।


Part 10: Design Decisions Explained

Why Not Create One Giant Class?

Weak design:

public class CourseEnrollmentSystem {

    private String learnerName;
    private String learnerEmail;
    private String courseCode;
    private String courseTitle;
    private long price;
    private int totalLessons;
    private int completedLessons;
    private boolean published;
}

Problems:

  • Multiple responsibilities
  • Repeated data
  • Weak ownership
  • Hard-to-test logic
  • Invalid combinations
  • Future changes difficult

Separate objects better model domain concepts।


Why Not Use Public Fields?

public int completedLessons;

Caller business rules bypass করতে পারে।

Encapsulation forces:

enrollment.completeLessons(3);

Why Not Add Setters for Everything?

Avoid:

setPublished(true);
setCompletedLessons(10);
setCourseCode("NEW");
setActive(false);

These methods low-level state manipulation expose করে।

Meaningful behavior:

publish();
completeLessons(10);
cancel();

Why Is CourseCode a Separate Class?

Without value object:

String courseCode;

Every callerকে separately মনে রাখতে হয়:

  • Blank allowed নয়
  • Uppercase করতে হবে
  • Spaces allowed নয়
  • Format validate করতে হবে

CourseCode object একবার validation করে guaranteed valid value represent করে।


Why Not Validate Duplicate Enrollment?

Rule:

A learner cannot enroll in the same course twice.

শুধু নতুন Enrollment object এই rule check করতে পারে না।

কারণ existing enrollments জানতে database বা collection query প্রয়োজন।

Possible application service:

EnrollmentService.enroll(
        learner,
        course
);

Service:

  1. Existing enrollment search করবে
  2. Course availability check করবে
  3. Payment status check করতে পারে
  4. তারপর Enrollment তৈরি করবে

সব rules domain object-এর ভেতরে force করা উচিত নয়।


Part 11: Extension Tasks

Reference implementation complete করার পরে নিচের extensions নিজে implement করুন।


Extension 1: Add Course Price Display

Course-এ method add করুন:

public String formatPrice()

Expected:

Free

অথবা:

BDT 4990.00

Presentation logic domain class-এ রাখা উচিত কি না, সেটিও evaluate করুন।

Alternative:

CoursePriceFormatter

Extension 2: Add Enrollment Status

Boolean:

active

এর পরিবর্তে enum concept ব্যবহার করুন:

ACTIVE
COMPLETED
CANCELLED

Enum detailedভাবে পরবর্তী content-এ শেখানো হতে পারে।

Think about allowed transitions:

ACTIVE → COMPLETED
ACTIVE → CANCELLED
COMPLETED → ?
CANCELLED → ?

Extension 3: Add Completion Time

Enrollment complete হলে timestamp record করুন।

Possible type:

LocalDateTime

Questions:

  • Field initial value কী হবে?
  • Completion-এর আগে null acceptable?
  • Immutable time type কেন useful?

Extension 4: Add Money

Raw:

long priceInPaisa

এর পরিবর্তে immutable Money value object ব্যবহার করুন।

Requirements:

  • Negative amount reject
  • Value-based equality
  • add()
  • applyDiscount()
  • Meaningful toString()

Extension 5: Prevent Duplicate Completion

Current code:

if (isCompleted()) {
    return false;
}

Complete হওয়ার পরে progress update reject করে।

Test লিখে verify করুন।


Extension 6: Add Enrollment Summary

Add:

public String createSummary()

Possible result:

Nur is 30.00% through Java and OOP Foundation.

Evaluate whether this is domain behavior or presentation concern।


Extension 7: Add Multiple Enrollments

Main-এ:

  • Subu
  • Sumu
  • Jalisa

তিনজন learner তৈরি করুন।

একই course-এ separate enrollments তৈরি করুন।

Verify করুন প্রতিটি enrollment independent progress maintain করে।


Part 12: Review Questions

Question 1

CourseCode কেন immutable?

Question 2

Course কেন static factory methods ব্যবহার করছে?

Question 3

Enrollment কেন learner name এবং course title copy করছে না?

Question 4

Course code এবং course title-এর mutability আলাদা কেন?

Question 5

completeLessons() assignment-এর আগে candidate state calculate করে কেন?

Question 6

isCompleted() কেন stored boolean নয়?

Question 7

Course equality কোন field-এর ওপর based?

Question 8

Enrollment duplicate enrollment rule enforce করতে পারে না কেন?

Question 9

Static course counter production database count নয় কেন?

Question 10

toString()-এ learner email না রাখার benefit কী?


Review Answers

Answer 1

Course code stable value এবং identity represent করে। Value change মানে নতুন code।

Answer 2

Free এবং paid creation paths meaningful names দিয়ে express করতে এবং invalid combinations prevent করতে।

Answer 3

Learner এবং Course objects নিজেদের data own করে। Copy করলে stale বা inconsistent state তৈরি হতে পারে।

Answer 4

Code stable identity। Title editable descriptive state।

Answer 5

Invalid state commit হওয়ার আগে updated value validate করতে।

Answer 6

Completion existing progress এবং total lesson count থেকে derive করা যায়। আলাদা boolean stale হতে পারে।

Answer 7

Immutable CourseCode

Answer 8

Existing enrollments জানতে external collection, repository, বা database প্রয়োজন।

Answer 9

Counter process-local, non-persistent, multi-server unaware এবং concurrency-safe নয়।

Answer 10

Logs-এ unnecessary personal information exposure কমে।


Completion Checklist

Project complete ধরা যাবে যদি:

  • Package structure declaration-এর সঙ্গে match করে
  • সব source files compile করে
  • CourseCode immutable
  • Course code normalize হয়
  • Free course price zero
  • Paid course price positive
  • Course publish করা যায়
  • Published course-এ learner enroll করা যায়
  • Unpublished course enrollment rejected
  • Progress total lessons exceed করতে পারে না
  • Cancelled enrollment update reject করে
  • Completed enrollment cancel reject করে
  • Course equality code-based
  • Learner equality ID-based
  • Private fields direct mutation prevent করে
  • No blind setters
  • Meaningful toString() implementations আছে
  • Sensitive data unnecessarily log হয় না

Module Practice Summary

এই project-এ আমরা একটি small কিন্তু production-minded domain model তৈরি করেছি।

আমরা দেখেছি:

  • Value object কীভাবে validation centralize করে
  • Constructor valid initial state establish করে
  • Static factory methods creation intent clear করে
  • Encapsulation arbitrary mutation prevent করে
  • Meaningful methods state transition express করে
  • Composition related objects connect করে
  • Duplicate data avoid করলে consistency improve হয়
  • Immutable identity equality stable রাখে
  • Derived state duplicate না করে calculate করা যায়
  • Package structure code ownership communicate করে
  • Domain object শুধু নিজের জানা rules enforce করে
  • External data-dependent rules application service-এর responsibility হতে পারে
  • Simple console application-এর মধ্যেও strong object design apply করা যায়

Strong OOP design-এর লক্ষ্য বেশি classes তৈরি করা নয়।

লক্ষ্য হলো:

সঠিক responsibility
সঠিক object-এর কাছে রাখা

Next Lesson

পরবর্তী lesson:

Module Review and Assessment

আমরা Module 2-এর complete revision করব:

  • Core concepts summary
  • Design judgment questions
  • Code review exercises
  • Predict-the-output problems
  • Bug-fixing tasks
  • Short implementation challenges
  • Final module assessment