Nguyên tắc SOLID: Nền tảng Chắc chắn cho Ứng dụng JavaScript và TypeScript
Nguyên tắc SOLID là bộ nguyên tắc quan trọng trong phát triển phần mềm, giúp tạo ra code rõ ràng, dễ bảo trì và có khả năng mở rộng. Bài viết này sẽ cung cấp một cái nhìn sâu sắc về từng nguyên tắc SOLID cùng với các ví dụ minh họa trong JavaScript (JS) và TypeScript (TS).
1. Nguyên tắc Trách nhiệm Duy nhất (SRP) - Single Responsibility Principle
Mô tả: Mỗi lớp hoặc module chỉ nên có một lý do để thay đổi, tức là chỉ nên đảm nhận một trách nhiệm duy nhất.
Ví dụ trong JavaScript (React)
Trong React, các component thường bị lạm dụng khi chúng làm quá nhiều việc khác nhau. Dưới đây là ví dụ về một component vi phạm nguyên tắc SRP:
javascript
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUserData();
}, [userId]);
async function fetchUserData() {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
}
return <div>{user?.name}</div>;
}
Trong trường hợp này, component UserProfile
đang xử lý việc render UI và lấy dữ liệu cùng một lúc.
Giải pháp: Sử dụng custom hook để tách logic lấy dữ liệu ra khỏi component UI:
javascript
function useUserData(userId) {
const [user, setUser] = useState(null);
useEffect(() => {
async function fetchUserData() {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
}
fetchUserData();
}, [userId]);
return user;
}
function UserProfile({ userId }) {
const user = useUserData(userId);
return <div>{user?.name}</div>;
}
Ví dụ trong TypeScript (Angular)
Trong Angular, nếu một service có nhiều trách nhiệm, nó có thể trở nên lộn xộn:
typescript
@Injectable()
export class UserService {
constructor(private http: HttpClient) {}
getUser(userId: string) {
return this.http.get(`/api/users/${userId}`);
}
updateUserProfile(userId: string, data: any) {
return this.http.put(`/api/users/${userId}`, data).subscribe(() => {
console.log('User updated');
alert('Profile updated successfully');
});
}
}
Ở đây, UserService
có nhiều trách nhiệm, từ lấy dữ liệu đến cập nhật và xử lý thông báo.
Giải pháp: Tách việc xử lý thông báo thành một service riêng biệt:
typescript
@Injectable()
export class UserService {
...
}
@Injectable()
export class NotificationService {
notify(message: string) {
alert(message);
}
}
2. Nguyên tắc Mở/Đóng (OCP) - Open/Closed Principle
Mô tả: Các thực thể phần mềm nên mở để mở rộng nhưng đóng để sửa đổi.
Ví dụ trong JavaScript (React)
Trong JavaScript, việc thêm logic xác thực mới vào một hàm có thể vi phạm nguyên tắc OCP:
javascript
function validate(input) {
...
}
Giải pháp: Tạo các quy tắc xác thực riêng biệt.
javascript
function validate(input, rules) {
return rules.map(rule => rule(input)).find(result => result !== 'Valid') || 'Valid input';
}
Ví dụ trong TypeScript (Angular)
Dưới đây là một service vi phạm OCP:
typescript
class NotificationService {
send(type: 'email' | 'sms', message: string) {
...
}
}
Giải pháp: Tạo các lớp notification riêng biệt cho từng loại thông báo.
typescript
interface Notification {
send(message: string): void;
}
3. Nguyên tắc Thay thế Liskov (LSP) - Liskov Substitution Principle
Mô tả: Các kiểu con phải có thể thay thế cho các kiểu cơ sở mà không làm hỏng chương trình.
Ví dụ trong JavaScript (React)
Sử dụng higher-order component (HOC) có thể gây nhầm lẫn. Đã đến lúc cần một giải pháp phù hợp hơn:
javascript
function Clickable({ children, onClick }) {
return <div onClick={onClick}>{children}</div>;
}
Ví dụ trong TypeScript (Angular)
Một ví dụ về việc vi phạm nguyên tắc LSP:
typescript
class Rectangle { ... }
class Square extends Rectangle { ... }
Giải pháp: Tạo một lớp cơ sở để kế thừa cho cả Rectangle
và Square
mà không làm hỏng tính chính xác của program.
4. Nguyên tắc Phân tách Giao diện (ISP) - Interface Segregation Principle
Mô tả: Clients không nên bị ép buộc phải phụ thuộc vào các giao diện mà chúng không sử dụng.
Ví dụ trong JavaScript (React)
Đảm bảo rằng các props chỉ đưa ra những gì được dùng.
javascript
function UserProfileComponent({ user }) { ... }
Ví dụ trong TypeScript (Angular)
Tạo các interface tách biệt cho từng loại worker.
typescript
interface Worker { ... }
interface Eater { ... }
5. Nguyên tắc Đảo ngược Phụ thuộc (DIP) - Dependency Inversion Principle
Mô tả: Các module cấp cao không nên phụ thuộc vào module cấp thấp; cả hai nên phụ thuộc vào các lớp trừu tượng.
Ví dụ trong JavaScript (React)
Các component nên có thể nhận input từ bên ngoài để tránh việc bị gắn chặt:
javascript
function UserComponent({ userId, fetchUserData }) { ... }
Ví dụ trong TypeScript (Angular)
Bằng cách sử dụng interfaces, các component được tách ra khỏi các service cụ thể:
typescript
interface UserService { ... }
Kết luận
Việc áp dụng các nguyên tắc SOLID trong phát triển phần mềm không chỉ giúp code của bạn trở nên rõ ràng, dễ sửa đổi mà còn dễ mở rộng để thích nghi với sự thay đổi trong tương lai. Khi kết hợp các nguyên tắc này vào dự án của bạn, bạn sẽ tạo ra các ứng dụng JavaScript và TypeScript bền vững và hiệu quả hơn.
source: viblo