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 B và B 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 exports ở module-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”.
GO TO FULL VERSION