Hiểu về Sự liên kết (Cohesion) và Nguyên lý Trách nhiệm Đơn lẻ (SRP)
Bài viết giải thích khái niệm sự liên kết trong thiết kế hướng đối tượng, phân tích ví dụ về lớp tính lương có độ liên kết thấp và cách tái cấu trúc (refactor) để tăng tính kết dính, tuân thủ nguyên lý SRP nhằm dễ bảo trì và tái sử dụng mã nguồn.

Trong phát triển phần mềm hướng đối tượng, việc thiết kế các lớp (class) sao cho gọn gàng và dễ bảo trì là cực kỳ quan trọng. Hai khái niệm cốt lõi giúp chúng ta đạt được điều này là Sự liên kết (Cohesion) và Nguyên lý Trách nhiệm Đơn lẻ (SRP - Single Responsibility Principle). Bài viết này sẽ phân tích sâu về hai khái niệm này thông qua ví dụ thực tế về việc tính lương.
Lớp có độ liên kết cao là gì?
Một lớp có độ liên kết cao (cohesive class) là lớp chỉ thực hiện duy nhất một trách nhiệm. Những lớp này thường có kích thước nhỏ, được tổ chức khoa học, dễ dàng bảo trì và tái sử dụng trong các ngữ cảnh khác nhau.
Ngược lại, một lớp có độ liên kết thấp sẽ đảm nhận quá nhiều việc, dẫn đến mã nguồn rối rắm và khó sửa đổi sau này.
Ví dụ về lớp có độ liên kết thấp
Hãy xem xét đoạn mã JavaScript dưới đây mô phỏng một lớp tính lương:
class CalculadoraDeSalario {
calcula(funcionario) {
if (funcionario.cargo === "DESENVOLVEDOR") {
return this.dezOuVintePorcento(funcionario);
}
if (funcionario.cargo === "DBA" || funcionario.cargo === "TESTER") {
return this.quinzeOuVinteCincoPorcento(funcionario);
}
throw new Error("Funcionário inválido");
}
dezOuVintePorcento(funcionario) {
if (funcionario.salarioBase > 3000) {
return funcionario.salarioBase * 0.8;
}
return funcionario.salarioBase * 0.9;
}
quinzeOuVinteCincoPorcento(funcionario) {
if (funcionario.salarioBase > 2000) {
return funcionario.salarioBase * 0.75;
}
return funcionario.salarioBase * 0.85;
}
}
Vấn đề của lớp này
Mặc dù đoạn mã trên hoạt động đúng, nhưng nó mắc phải nhiều vấn đề thiết kế:
- Kiến thức thừa thãi: Lớp
CalculadoraDeSalariophải biết và liệt kê tất cả các chức vụ (DESENVOLVEDOR, DBA, TESTER). - Khó mở rộng: Sử dụng quá nhiều câu lệnh
if. Mỗi khi có một chức vụ mới được thêm vào, chúng ta buộc phải sửa đổi mã nguồn của lớp này. - Khó tái sử dụng: Các quy tắc tính toán lương bị gắn chặt vào lớp, không thể tách ra để dùng ở nơi khác.
- Đa trách nhiệm: Lớp này đang làm hai việc: vừa xác định chức vụ của nhân viên, vừa tính toán số tiền. Đây là dấu hiệu của độ liên kết thấp và sự phụ thuộc (coupling) cao.
Lý tưởng nhất là mỗi quy tắc tính toán nên nằm trong một lớp riêng biệt. Điều này giúp tăng sự liên kết và tạo điều kiện thuận lợi cho việc tái sử dụng cũng như bảo trì.
Tại sao lớp này khó tái sử dụng?
Hãy tưởng tượng bạn muốn sử dụng lại quy tắc tính toán dezOuVintePorcento() ở một phần khác của hệ thống.
Bạn không thể chỉ gọi phương thức đó đơn độc vì nó nằm "nhốt" bên trong lớp CalculadoraDeSalario. Để sử dụng nó, bạn buộc phải:
- Khởi tạo (instantiate) lớp
CalculadoraDeSalario. - Tạo ra một đối tượng
funcionariogiả lập. - Truyền đối tượng đó vào.
- Mới có thể gọi được phương thức mong muốn.
Nói cách khác, để tái sử dụng một quy tắc nhỏ, bạn phải "mang theo" cả một lớp lớn. Đây là dấu hiệu rõ ràng của độ liên kết thấp, vì lớp đang phải làm quá nhiều việc.
Ví dụ về lớp đã tái cấu trúc với độ liên kết cao
Chúng ta có thể giải quyết vấn đề trên bằng cách tách biệt các quy tắc tính toán. Dưới đây là đoạn mã đã được refactor:
class Cargo {
calculaSalario(salarioBase) {
throw new Error("Método deve ser implementado pela subclasse");
}
}
class Desenvolvedor extends Cargo {
calculaSalario(salarioBase) {
return salarioBase > 3000
? salarioBase * 0.8
: salarioBase * 0.9;
}
}
class Dba extends Cargo {
calculaSalario(salarioBase) {
return salarioBase > 2000
? salarioBase * 0.75
: salarioBase * 0.85;
}
}
class Tester extends Cargo {
calculaSalario(salarioBase) {
return salarioBase > 2000
? salarioBase * 0.75
: salarioBase * 0.85;
}
}
const RegrasPorCargo = {
DESENVOLVEDOR: new Desenvolvedor(),
DBA: new Dba(),
TESTER: new Tester()
};
class CalculadoraDeSalarioRefatorada {
calcula(funcionario) {
const regra = RegrasPorCargo[funcionario.cargo];
if (!regra) {
throw new Error("Funcionário inválido");
}
return regra.calculaSalario(funcionario.salarioBase);
}
}
// Ví dụ sử dụng
const funcionario2 = {
cargo: "DESENVOLVEDOR",
salarioBase: 3500
};
const calculadora2 = new CalculadoraDeSalarioRefatorada();
console.log(calculadora2.calcula(funcionario2));
Bước đầu tiên để hiểu về tái cấu trúc
Trước khi tái cấu trúc, lớp CalculadoraDeSalario thay đổi vì hai lý do:
- Khi có một chức vụ mới xuất hiện.
- Khi có một quy tắc tính toán mới xuất hiện.
Tức là, lớp này có nhiều hơn một lý do để thay đổi, điều này vi phạm Nguyên lý Trách nhiệm Đơn lẻ (SRP).
Nếu quan sát các phương thức tính toán, dù khác nhau về con số, chúng đều có cùng một khuôn mẫu:
- Nhận mức lương cơ bản.
- Áp dụng một quy tắc nhất định.
- Trả về mức lương đã tính.
Tất cả chúng đều tuân theo cùng một sự trừu tượng (abstraction). Do đó, ý tưởng của việc tái cấu trúc là tạo ra một lớp riêng cho mỗi quy tắc tính toán, và tất cả các lớp này đều chia sẻ cùng một phương thức calculaSalario().
Chúng ta đạt được gì từ việc này?
- Mỗi quy tắc được cô lập hoàn toàn.
- Thay đổi ở một quy tắc không ảnh hưởng đến các quy tắc khác.
- Các lớp trở nên nhỏ gọn hơn.
- Các lớp có độ liên kết cao hơn.
- Hệ thống dễ bảo trì hơn.
- Có thể thêm mới chức vụ mà không cần sửa đổi mã nguồn đã có (Open/Closed Principle).
Bây giờ, mỗi lớp chỉ có một lý duy nhất để thay đổi: chính quy tắc tính toán của nó.
Tại sao không chỉ chia nhỏ phương thức?
Một câu hỏi thường gặp là: "Tại sao không chia phương thức lớn thành các phương thức nhỏ bên trong cùng một lớp?"
Câu trả lời là: Nếu chỉ chia nhỏ phương thức mà vẫn giữ chúng trong cùng một lớp, chúng ta sẽ không đạt được sự tái sử dụng cô lập. Các phương thức nhỏ vẫn bị ràng buộc với ngữ cảnh của lớp chứa nó, khiến việc sử dụng riêng lẻ ở nơi khác trở nên khó khăn như ví dụ ban đầu.
Tham khảo: Ví dụ và ý tưởng tái cấu trúc này được lấy cảm hứng từ cuốn sách "Orientação a Objetos e SOLID para Ninjas - Projetando classes flexíveis" của Maurício Aniche.



