Object Calisthenics in Java
Object Calisthenics?
Object Calisthenics là một tập hợp các nguyên tắc nhằm cải thiện chất lượng code và thúc đẩy tư duy hướng đối tượng (OOP). Nó được giới thiệu bởi Jeff Bay trong cuốn sách "The ThoughtWorks Anthology".
pros and cons
Lợi ích của Object Calisthenics
- Tăng tính rõ ràng: Code dễ đọc, dễ hiểu và dễ bảo trì.
- Hướng đến OOP tốt hơn: Áp dụng đúng tư duy hướng đối tượng, giảm lạm dụng kiểu nguyên thủy và getter/setter.
- Giảm lỗi: Giảm thiểu các lỗi do code phức tạp hoặc không rõ ràng.
Hạn chế
- Khó áp dụng với dự án cũ: Yêu cầu nhiều thời gian và công sức để refactor.
- Dễ over-engineering: Nếu áp dụng không phù hợp, code có thể trở nên quá phức tạp.
9 Nguyên tắc
1. Chỉ sử dụng một cấp độ indentation mỗi phương thức
- Nội dung: mỗi phương thức chỉ nên làm một nhiệm vụ cụ thể.
- lợi ích: dễ đọc, dễ kiểm tra logic, giảm phức tạp.
- ví dụ:
public class Grid {
public String displayGrid(char[][][] data) {
StringBuilder buffer = new StringBuilder();
// level 0
for (int i = 0; i < 10; i++) {
// level 1
for (int j = 0; j < 10; j++) {
// level 2
buffer.append(data[i][j]);
}
buffer.append("\\n");
}
return buffer.toString();
}
}
- cải thiện
public class Grid {
public String displayGrid(char[][][] data) {
StringBuilder buffer = new StringBuilder();
collectRows(buffer,data);
return buffer.toString();
}
private void collectRows(StringBuilder buffer, char[][][] data) {
for (int i = 0; i < 10; i++) {
collectRow(buffer, data, i);
}
}
private void collectRow(StringBuilder buffer, char[][][] data, int row) {
for (int i = 0; i < 10; i++) {
buffer.append(data[row][i]);
}
buffer.append("\\n");
}
}
2. Không dùng từ khóa else
- Nội dung: ưu tiên viết code với logic rõ ràng, giảm nesting.
- ví dụ:
if (isValid(data)) {
process(data);
} else {
handleError();
}
- sửa lại:
if (!isValid(data)) {
handleError();
return;
}
process(data);
3. Wrapper các kiểu nguyên thủy và Strings.
- Nội dung: nên gói các kiểu nguyên thủy (primitive) trong các đối tượng, để tránh Primitive Obsession anti-pattern.
If the variable of your primitive type has a behavior, you MUST encapsulate it!
- ví dụ:
public class Runner {
private int meter = 1;
private int kilometer = meter * 1000;
}
- cải thiện:
class Distance {
private int value;
private String unit;
Distance(int value, String unit) {
this.value = value;
this.unit = unit;
}
public Distance toKilometer() {
return new Distance(this.value * 1000, "kilometer");
}
}
// runner mới
public class Runner {
private Distance meter = new Distance(1, "meter");
private Distance kilometer = meter.toKilometer();
}
4. First Class Collection
- Nội dung: dùng một lớp dùng để gói một collection như
List, Set, Map
và sử lý tất cả các hành vi liên quan của nó giúp tăng tính đóng gói,tập trung sử lý logic của collection tại một chỗ, tăng tính sử dụng code. - ví dụ:
public class Classroom {
private List<Student> students;
public Classroom() {
this.students = new ArrayList<>();
}
public void addStudent(Student student) {
this.students.add(student);
}
public boolean hasStudent(String name) {
return students.stream()
.anyMatch(student -> student.getName().equalsIgnoreCase(name));
}
public List<Student> getStudents() {
return students; // Không an toàn, cho phép bên ngoài chỉnh sửa trực tiếp
}
- Nhận xét:
-
students được lấy qua
getStudents()
, dễ thay đổi trạng thái ngoài ý muốn. -
Sử lý login liên quan đến student nằm rải rác, không tập trung (ví dụ tìm, thêm).
-
Khó mở rộng khi cần thêm chức năng mới.
- Cải tiến bằng First Class Collection:
// tạo một lớp Students để đại diện cho danh sách sinh viên lớp này gói List<Sutdent> và
// sử lý toàn bộ hành vi liên quan.
// Lớp Students (First Class Collection)
public class Students {
private final List<Student> students;
public Students() {
this.students = new ArrayList<>();
}
public void add(Student student) {
if (student == null) {
throw new IllegalArgumentException("Student cannot be null");
}
this.students.add(student);
}
public boolean contains(String name) {
return students.stream()
.anyMatch(student -> student.getName().equalsIgnoreCase(name));
}
public int size() {
return this.students.size();
}
public List<Student> asList() {
return new ArrayList<>(this.students); // Trả về bản sao an toàn
}
}
// Lớp Classroom sử dụng Students:
public class Classroom {
private final Students students;
public Classroom() {
this.students = new Students();
}
public void addStudent(Student student) {
students.add(student);
}
public boolean hasStudent(String name) {
return students.contains(name);
}
public int totalStudents() {
return students.size();
}
}
// Lớp Student:
public class Student {
private final String name;
public Student(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
this.name = name;
}
public String getName() {
return name;
}
}
5. Dùng một .
mỗi dòng
- ý nghĩa: Khuyến kích làm rõ ràng khi gọi
chain method
. dựa trên một rule khá nổi tiếng làLaw of Demeter
. Bạn chỉ nên nói chuyện trực tiếp với bạn của bạn, đừng nên nói chuyện với người lạ. jf4. - ví dụ:
order.getCustomer().getAddress().getCity();
- cải thiện:
Customer customer = order.getCustomer();
Address address = customer.getAddress();
String city = address.getCity();
6. Đừng viết tắt
- nội dung: Không viết tắt và hãy dùng tên có ý nghĩa giống nguyên tắc đặt tên của code clean.
7. Giữ cho các entities nhỏ
- Nội dung: Tùy bạn nhưng khuyên là mỗi class không nên quá dài, mỗi package không nên quá nhiều. ý tưởng là file lớn thì khó đọc, khác hiểu, khó bảo trì.
8. Không có lớp với nhiều hơn 2 biến instances
- Nội dung mỗi lớp chỉ nên có ít instances để nó có thể phân chia trách nhiệm rõ ràng, giảm độ phức tạp, tăng tính đóng gói và dễ duy trì của mã nguồn.
- ví dụ. (Trong thực tế thì nên tăng giới hạn lên 5 nha).
public class Book {
private final String title;
private final Author author;
private final LocalDate publicationYear;
private final String isbn;
public Book(String title, Author author, LocalDate publicationYear, String isbn) {
this.title = title;
this.author = author;
this.publicationYear = publicationYear;
this.isbn = isbn;
}
}
class Author {
private final String name;
private final String bio;
private final String birthDate;
public Author(String name, String bio, String birthDate) {
this.name = name;
this.bio = bio;
this.birthDate = birthDate;
}
}
- cải tiến:
public class Book {
private final String title;
private final Author author;
public Book(String title, Author author) {
this.title = title;
this.author = author;
}
}
class Author {
private final String name;
private final String birthDate;
public Author(String name, String birthDate) {
this.name = name;
this.birthDate = birthDate;
}
}
9. Không getter và không setter
- Là nguyên tắc quan trọng nhất! Có thể sử dụng các hàm
accessors
để lấy trạng thái của một đối tượng, miễn là bạn không sử dụng kết quả để đưa ra quyết định bên ngoài đối tượng. Mọi quyết định hoàn toàn dựa trên trạng thái của một đối tượng phải được thực hiện bên trong chính đối tượng đó. - ví dụ
public class Wallet {
private BigDecimal balance;
public Wallet(BigDecimal balance) {
this.balance = balance;
}
void setBalance(BigDecimal balance) {
this.balance = balance;
}
public BigDecimal getBalance() {
return balance;
}
}
// USAGE
wallet.setBalance(wallet.getBalance() + NEW_AMOUNT)
- cải thiện: Wallet instance chịu trách nhiện xác định tăng giá trị lên.
public class Wallet {
private BigDecimal balance;
public Wallet(BigDecimal balance) {
this.balance = balance;
}
void increaseMoney(BigDecimal balance) {
this.balance.add(balance);
}
public BigDecimal totalAmount() {
return balance;
}
}
// USAGE
wallet.increaseMoney(NEW_AMOUNT)
References
All rights reserved