Nguyên tắc SOLID: Nấc thang tiến hóa từ "Thợ Code" lên "Kỹ sư Phần mềm"
Có bao giờ bạn rơi vào cảnh: Sếp yêu cầu thêm một tính năng nhỏ xíu, bạn sửa đúng một dòng code, và BÙM... 10 tính năng khác không liên quan bỗng nhiên lăn ra chết? Hiện tượng "chạm vào đâu vỡ ở đó" (Spaghetti code) là kết quả tất yếu của việc viết code theo bản năng.
Để chấm dứt thảm họa này, chú Bob (Robert C. Martin) đã tổng hợp lại 5 nguyên tắc thiết kế phần mềm cốt lõi, viết tắt là SOLID. Hiểu và áp dụng SOLID chính là ranh giới rõ ràng nhất phân biệt giữa một "Thợ gõ code" và một "Kỹ sư phần mềm" (Software Engineer).
1. S - Single Responsibility Principle (Nguyên lý Đơn trách nhiệm)
Lý thuyết: Một Class / Module / Hàm chỉ nên có ĐÚNG MỘT lý do để thay đổi (Chỉ làm đúng 1 việc).
Nỗi đau thực tế:
Trong React, các "tấm chiếu mới" rất thích nhét tất cả vào một Component duy nhất. Một file UserList.tsx dài 500 dòng: Vừa gọi API (fetch), vừa format ngày tháng (logic), vừa render giao diện (UI), vừa xử lý bắt lỗi (Error Handling). Nếu API thay đổi, bạn phải sửa file này. Nếu UI đổi màu, bạn cũng phải sửa file này. Quá nhiều rủi ro!
Cách giải quyết (Áp dụng SRP):
Hãy tách nó ra thành các mảnh ghép độc lập:
Custom Hook (
useUsers.ts): Chỉ lo việc gọi API và quản lý state (loading,data,error).Hàm Utils (
formatDate.ts): Chỉ lo việc chuyển đổi chuỗi thời gian.Component (
UserList.tsx): Lúc này chỉ còn làm đúng 1 việc: Nhận data từ Hook và vẽ lên giao diện (Render UI).
Code của bạn sẽ sạch sẽ, dễ đọc và có thể tái sử dụng hook đó ở bất cứ đâu.
2. O - Open/Closed Principle (Nguyên lý Đóng/Mở)
Lý thuyết: Code phải được MỞ để mở rộng (thêm tính năng mới), nhưng ĐÓNG với việc sửa đổi (không được chọc vào code cũ).
Nỗi đau thực tế:
Giả sử bạn viết một hàm tính chiết khấu. Ban đầu chỉ có khách VIP.
function getDiscount(customerType, price) {
if (customerType === 'VIP') return price * 0.8;
return price;
}
Tháng sau sếp đòi thêm khách SuperVIP, khách Newbie, khách Khủng Long... Bạn lại chui vào hàm này viết thêm một đống if/else hoặc switch/case. Việc liên tục sửa code cũ rất dễ làm hỏng logic của khách VIP ban đầu. Bạn đã vi phạm nguyên lý OCP.
Cách giải quyết (Áp dụng OCP):
Sử dụng Strategy Pattern hoặc một Map (Dictionary) để tách biệt logic.
// Bạn định nghĩa các luật giảm giá ở một nơi riêng biệt (MỞ để thêm mới)
const discountStrategies = {
VIP: (price) => price * 0.8,
SuperVIP: (price) => price * 0.5,
Newbie: (price) => price * 0.9,
DEFAULT: (price) => price
};
// Hàm chính (ĐÓNG, không bao giờ cần sửa lại nữa dù có thêm 100 loại khách)
function getDiscount(customerType, price) {
const strategy = discountStrategies[customerType] || discountStrategies['DEFAULT'];
return strategy(price);
}
3. L - Liskov Substitution Principle (Nguyên lý thay thế Liskov)
Lý thuyết: Lớp con (Derived Class) phải có thể thay thế được cho lớp cha (Base Class) mà không làm hỏng tính đúng đắn của chương trình.
Nghe có vẻ học thuật, nhưng bản chất của nó là: Đừng ép lớp con làm những việc mà nó không thể làm (hoặc làm sai lệch ý nghĩa của lớp cha).
Ví dụ kinh điển:
Bạn có một lớp cha là Bird (Chim) có method fly().
Bạn tạo một lớp con là Eagle (Đại bàng) kế thừa Bird Gọi fly() chạy ngon lành.
Sau đó bạn tạo lớp con Penguin (Chim cánh cụt) cũng kế thừa Bird. Nhưng chim cánh cụt đâu có biết bay? Nên bạn ghi đè hàm fly() để nó ném ra một cái lỗi: throw new Error("Tôi không biết bay").
Hậu quả: Bạn đã vi phạm LSP! Nếu hệ thống đang gọi một danh sách các Bird và bắt chúng bay, khi đụng tới con cánh cụt, chương trình sẽ crash ngay lập tức.
Cách giải quyết: Chia lại hệ thống. Tạo một lớp Bird chung (chỉ có hàm eat()), sau đó tạo 2 interface riêng: FlyingBird (có fly()) và SwimmingBird (có swim()). Đại bàng sẽ kế thừa FlyingBird, cánh cụt kế thừa SwimmingBird.
4. I - Interface Segregation Principle (Nguyên lý Phân tách Interface)
Lý thuyết: Đừng ép Client (người dùng class/component) phải phụ thuộc vào những Interface (phương thức/thuộc tính) mà họ không sử dụng.
Nỗi đau thực tế trong TypeScript/React:
Bạn có một Component Avatar dùng để hiển thị hình ảnh người dùng. Bạn "tiện tay" ném nguyên một cái Object User chà bá vào làm Props:
interface User {
id: string;
name: string;
avatarUrl: string;
email: string;
bankAccountNumber: string; // Component Avatar đâu cần cái này?
}
const Avatar = ({ user }: { user: User }) => {
return <img src={user.avatarUrl} alt={user.name} />;
}
Component Avatar chỉ cần cái hình và cái tên, nhưng bạn lại bắt nó phải cõng cả email và tài khoản ngân hàng. Nếu sau này Model User thay đổi, Component này cũng bị ảnh hưởng lây.
Cách giải quyết (Áp dụng ISP):
Tạo một Interface nhỏ bé và vừa vặn nhất cho Component đó.
interface AvatarProps {
imageUrl: string;
altText: string;
}
const Avatar = ({ imageUrl, altText }: AvatarProps) => {
return <img src={imageUrl} alt={altText} />;
}
5. D - Dependency Inversion Principle (Nguyên lý Đảo ngược Phụ thuộc)
Lý thuyết: Module cấp cao không được phụ thuộc vào Module cấp thấp. Cả hai phải phụ thuộc vào các Abstractions (Lớp trừu tượng).
Nỗi đau thực tế:
Trong dự án Frontend, bạn dùng axios để gọi API ở mọi nơi (trong component, trong redux thunk). Code của bạn đang bị "trói chặt" (tightly coupled) vào cái thư viện axios. Nhỡ ngày mai thư viện này dính lỗ hổng bảo mật, hoặc sếp yêu cầu chuyển sang dùng fetch API mặc định, bạn sẽ phải dùng tổ hợp phím Ctrl + Shift + F và đi sửa hộc máu hàng trăm file.
Cách giải quyết (Áp dụng DIP):
Đừng gọi trực tiếp thư viện. Hãy tạo ra một lớp "Trừu tượng" (như một File Service / Adapter) để làm trung gian.
// File: apiClient.js (Lớp trừu tượng trung gian)
import axios from 'axios';
export const httpService = {
get: async (url) => {
const response = await axios.get(url); // Nếu đổi sang fetch, chỉ cần sửa DUY NHẤT dòng này!
return response.data;
},
post: async (url, data) => { ... }
};
Giờ đây, các Component (Module cấp cao) chỉ cần gọi httpService.get(). Chúng không cần biết (và cũng không quan tâm) bên dưới nền đang dùng axios, fetch hay thư viện nào khác. Bạn đã đảo ngược sự phụ thuộc thành công!
6. Lời kết
SOLID là những "Nguyên tắc" (Principles) chứ không phải là "Luật pháp" (Laws).
Với những tính năng quá nhỏ, việc cố đấm ăn xôi áp dụng toàn bộ SOLID sẽ khiến dự án bị Over-engineering (thiết kế quá đà, làm cho hệ thống cồng kềnh thêm). Một "Senior" thực thụ là người nắm rõ SOLID trong lòng bàn tay, nhưng biết lúc nào cần tuân thủ tuyệt đối, và lúc nào cần lách luật để đẩy nhanh tiến độ dự án. Chúc bạn code sạch và không còn sợ bug đè!