CodeGym /Các khóa học /JAVA 25 SELF /Tách dự án thành mô-đun: best practices

Tách dự án thành mô-đun: best practices

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Khi nào và vì sao nên chia dự án thành mô-đun

Vì sao không nên làm mọi thứ trong một mô-đun duy nhất

Nếu bạn chỉ viết một bài lab nhỏ hoặc "Hello, World!", hệ thống mô-đun có thể có vẻ thừa. Nhưng khi dự án lớn dần — hàng chục, hàng trăm lớp, nhiều package, thư viện bên thứ ba — thì hỗn độn là điều khó tránh. Nó giống như một thư viện không có kệ: khi sách còn ít thì tạm ổn, nhưng sau đó tìm thứ gì đó sẽ rất khó. Mô-đun chính là “kệ sách” của bạn: chúng giúp sắp xếp trật tự và che đi “bếp núc” (implementation), chỉ để lộ “mặt tiền” (API).

Vì sao nên chia thành mô-đun

  • Tách biệt trách nhiệm: mỗi mô-đun phụ trách một mảng riêng (ví dụ: DB, business logic, UI).
  • Tái sử dụng mã: có thể đưa mô-đun vào dự án khác.
  • Cải thiện khả năng kiểm thử: các mô-đun được kiểm thử độc lập.
  • Bảo mật và đóng gói: bên ngoài chỉ thấy API, phần triển khai được ẩn.
  • Dễ bảo trì hơn: ít liên kết “ma thuật”, sơ đồ phụ thuộc rõ ràng.
  • Build và deploy nhanh: chỉ mô-đun thay đổi mới cần biên dịch lại.

Khi nào nên chia thành mô-đun

  • Dự án trở nên quá lớn đối với một lập trình viên (hoặc IDE bắt đầu bị “lag”).
  • Các phần được phân tách rõ: core, ui, utils, api, impl.
  • Dự định tái sử dụng mã ở các dự án khác.
  • Có các phụ thuộc bên ngoài chỉ cần cho một phần của dự án.
  • Cần ẩn chi tiết triển khai (thuật toán, lớp nội bộ).

2. Các sơ đồ mô-đun điển hình

Dưới đây là các sơ đồ phổ biến, phù hợp cho cả dự án học tập và sản phẩm thực tế.

Kiến trúc “hành tây” (Onion Architecture)

Lớp ngoài phụ thuộc vào lớp trong, nhưng không ngược lại.

[ app (UI) ]
     ↓
[ core (logic) ]
     ↓
[ utils (tiện ích) ]
  • app — mô-đun ngoài: giao diện đồ họa, ứng dụng web, điểm vào (main).
  • core — logic nghiệp vụ, model, service.
  • utils — các lớp tiện ích.

Quy tắc: lớp bên trong không được phụ thuộc vào lớp bên ngoài. Nhờ vậy core có thể tái sử dụng với nhiều giao diện khác nhau (console, web, desktop).

Mô-đun cho API và phần triển khai

Với thư viện, nên tách riêng interface và phần triển khai:

[ mylib.api ]  ← chỉ export interface
[ mylib.impl ] ← chứa phần triển khai, không export

Mô-đun cho kiểm thử

Thường tách test sang mô-đun riêng để không lọt vào artifact chạy thật.

[ app ]
[ core ]
[ core.tests ]

Sơ đồ cho dự án học tập

myeditor/
  ├─ app/         ← điểm vào, khởi chạy ứng dụng
  ├─ core/        ← logic nghiệp vụ (làm việc với tệp, văn bản)
  └─ utils/       ← tiện ích (ghi log, parsing)

3. Phụ thuộc giữa các mô-đun

Trong các mô-đun Java, phụ thuộc được khai báo rõ ràng trong module-info.java bằng từ khóa requires. Điều này vừa tăng tính dễ đọc, vừa cho phép compiler/JVM kiểm soát khả năng truy cập API qua exports.

Ví dụ về phụ thuộc

core/module-info.java

module myeditor.core {
    exports myeditor.core.api; // chỉ export package api ra ngoài
    requires myeditor.utils;   // sử dụng tiện ích
}

app/module-info.java

module myeditor.app {
    requires myeditor.core;    // sử dụng core
    requires myeditor.utils;   // có thể sử dụng tiện ích trực tiếp
}

Quy tắc và best practices

  • Tránh phụ thuộc vòng. Nếu A requires BB requires A — đó là lỗi thiết kế. Thường xử lý bằng cách tách common/api dùng chung.
  • Tối thiểu hóa phụ thuộc. Đừng thêm mô-đun nếu thực sự không cần.
  • Export các package được sử dụng. Các lớp phải nằm trong những package được khai báo bằng exports, nếu không sẽ lỗi biên dịch.
  • Utilities — độc lập tối đa. utils không nên phụ thuộc vào logic nghiệp vụ.

4. Thực hành: ví dụ tách một dự án học tập thành 3 mô-đun

Cấu trúc thư mục

myeditor/
  ├─ app/
  │    ├─ src/
  │    │    └─ myeditor/app/Main.java
  │    └─ module-info.java
  ├─ core/
  │    ├─ src/
  │    │    ├─ myeditor/core/api/TextService.java
  │    │    └─ myeditor/core/impl/TextServiceImpl.java
  │    └─ module-info.java
  └─ utils/
       ├─ src/
       │    └─ myeditor/utils/Logger.java
       └─ module-info.java

Ví dụ module-info.java

core/module-info.java

module myeditor.core {
    exports myeditor.core.api;
    requires myeditor.utils;
}

app/module-info.java

module myeditor.app {
    requires myeditor.core;
    requires myeditor.utils;
}

utils/module-info.java

module myeditor.utils {
    exports myeditor.utils;
}

Ví dụ mã (TextService)

myeditor/core/api/TextService.java

package myeditor.core.api;

public interface TextService {
    String toUpperCase(String text);
}

myeditor/core/impl/TextServiceImpl.java

package myeditor.core.impl;

import myeditor.core.api.TextService;

public class TextServiceImpl implements TextService {
    @Override
    public String toUpperCase(String text) {
        return text.toUpperCase();
    }
}

myeditor/app/Main.java

package myeditor.app;

import myeditor.core.api.TextService;
import myeditor.core.impl.TextServiceImpl;

public class Main {
    public static void main(String[] args) {
        TextService service = new TextServiceImpl();
        System.out.println(service.toUpperCase("hello, modules!"));
    }
}

Trong IntelliJ IDEA trông như thế nào

  • Mỗi thư mục là một Module riêng trong cấu trúc dự án.
  • Mỗi mô-đun có module-info.java ở gốc src.
  • Khi chạy main từ app, IDE sẽ tự cấu hình module-path.
  • Cố dùng lớp từ package không được export sẽ dẫn đến lỗi biên dịch.

5. Ảnh hưởng đến build: Maven/Gradle và mô-đun

Maven

Dự án đa mô-đun là dự án “cha” (parent) và một số mô-đun “con”.

myeditor/
  ├─ pom.xml        ← parent
  ├─ app/
  │    └─ pom.xml
  ├─ core/
  │    └─ pom.xml
  └─ utils/
       └─ pom.xml

Ví dụ parent pom.xml

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>myeditor</groupId>
  <artifactId>myeditor-parent</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <modules>
    <module>app</module>
    <module>core</module>
    <module>utils</module>
  </modules>
</project>

Đặc điểm:

  • Maven tính tới module-info.java khi biên dịch.
  • Khi chạy dùng --module-path thay vì --classpath.
  • Nếu quên exports hoặc requires — sẽ lỗi biên dịch.

Gradle

Đa mô-đun được cấu hình qua settings.gradle và các build.gradle riêng cho từng mô-đun.

settings.gradle

rootProject.name = 'myeditor'
include 'app', 'core', 'utils'

build.gradle cho mô-đun

plugins {
    id 'java'
}

java {
    modularity.inferModulePath = true
}

IntelliJ IDEA

  • IDEA có thể tạo module-info.java khi bạn tạo Java module.
  • Với Maven/Gradle, cấu trúc mô-đun được nhận diện tự động.
  • Khi chạy main từ app, IDE sẽ cấu hình module-path.
  • Hộp thoại import/export gợi ý về khả năng thấy được của package và mô-đun.

Các lỗi điển hình khi chia mô-đun

Lỗi №1: Phụ thuộc vòng giữa các mô-đun. Nếu hai mô-đun cùng khai báo requires lẫn nhau, compiler sẽ báo lỗi. Thường đây là dấu hiệu kiến trúc bị “trôi”. Cách xử lý — tách một mô-đun api chung hoặc xem lại ranh giới.

Lỗi №2: Sử dụng lớp từ các package không được export. Lớp có thể là public, nhưng nếu package không được chỉ ra trong exportsmodule-info.java, mô-đun khác sẽ không thấy nó. Kết quả — lỗi biên dịch.

Lỗi №3: Quên thêm requires cho mô-đun đang sử dụng. Import từ mô-đun khác mà không có dòng tương ứng trong module-info.java sẽ không biên dịch được. Luôn khai báo phụ thuộc một cách rõ ràng.

Lỗi №4: Trùng tên mô-đun. Tên mô-đun phải là duy nhất trong phạm vi build (đặc biệt với Maven/Gradle). Bị trùng sẽ làm hỏng quá trình build.

Lỗi №5: Cấu trúc thư mục sai. Tệp module-info.java phải nằm ở gốc src của mô-đun tương ứng. Nếu không compiler sẽ không tìm thấy mô-đun.

Lỗi №6: module-path sai khi chạy. Khi chạy thủ công, hãy dùng --module-path thay cho --classpath, nếu không bạn sẽ gặp “module not found”.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION