React Native - SOLID Principles
Xây dựng ứng dụng React Native với nguyên tắc SOLID Principles: Cho người mới bắt đầu và ví dụ

Bạn đã từng phải đấu tranh khi phải bảo trì code ứng dụng React Native của bạn khi ứng dụng của bạn ngày càng phát triển lên Nó trở nên khó khăn để theo dõi mọi thứ, đặt biệt là các components làm quá nhiều công việc một lần, Đó là lí do tại sao chúng ta phải cần đến SOLID Principles!
SOLID là từ viết tắt (acronym) 5 ký tự đầu của design principles nó giúp bạn viết code gọn gàng, dễ bảo trì và dễ hiểu hơn
1. Single Responsibility Principle (SRP)
Hãy tưởng tượng components của bạn như những nhân viên được đào tạo một cách bài bản Mỗi nhân viên chỉ nên làm một nhiệm vụ cụ thể hoặc có trách nhiệm hoàn thành tốt một nhiệm vụ Trong ứng dụng của bạn component chỉ nên có m ột nhiệm vụ duy nhất và được định nghĩa rõ ràng, nó làm cho nó dễ dàng sử dụng dễ hiểu, chỉnh sửa và dễ sử dụng lại
- Ví dụ :
-
Before (SRP Violation) : Một component hiển thị chi tiết sản phẩm và chỉ lắng nghe sự kiện thêm sản phẩm vào giỏ hàng, Nó giống như bạn hỏi một nhân viên có 2 nhiệm vụ đó sale và thu ngân
-
After (SRP Applied) : Chia components để hiện thị chi tiết sản phẩm và thêm vào giỏ hàng, code sẽ dễ hiểu và dễ dàng quản lý hơn
const ProductCard = ({ product, onAddToCart }) => {
const [quantity, setQuantity] = useState(1);
const handleAddToCart = () => {
onAddToCart(product.id, quantity);
};
return (
<View>
<Text>{product.name}</Text>
<Text>{product.price}</Text>
<TextInput
value={quantity}
onChangeText={setQuantity}
keyboardType="numeric"
/>
<Button title="Add to Cart" onPress={handleAddToCart} />
</View>
);
};
Component này kết hợp hiển thị chi tiết sản phẩm, quản lý trạng thái số lượng và funtion thêm vào giỏ hàng
- • Component này với nhiều nhiệm vụ bên trong
- • Hiển thị chi tiết sản phẩm với (tên và giá)
- • Quản lý số lượng sản phẩm được thêm vào giỏ hàng
- • Lắng nghe sự kiện "Add to Card" khi nhấn button và logic
const ProductDetails = ({ product }) => (
<View>
<Text>{product.name}</Text>
<Text>{product.price}</Text>
</View>
);
const AddToCartForm = ({ product, onAddToCart }) => {
const [quantity, setQuantity] = useState(1);
const handleAddToCart = () => {
onAddToCart(product.id, quantity);
};
return (
<View>
<TextInput
value={quantity}
onChangeText={setQuantity}
keyboardType="numeric"
/>
<Button title="Add to Cart" onPress={handleAddToCart} />
</View>
);
};
- Ở đây có 2 components riêng biệt xử lý 2 nhiệm vụ
- ProductDetails : Hiển thị chi tiết sản phẩm, và chỉ được sử dụng để render data
- AddToCartForm : Quản lý số lượng sản phẩm và Handles function "Add to Card", chỉ chịu trách nhiệm cho user
- tương tác và xử lý dữ liệu (thao tác với data)
Tách biệt này có cấu trúc đẹp hơn và dễ dàng bảo trì
2. Open/Close Principle (OCP)
Hãy coi các components của bạn như các toà nhà, Bạn muốn chúng có thể được mở rộng (thêm mới chức năng) nhưng không cho chỉnh sửa (chỉnh sửa code hiện có), Với OCP, bạn có thể thêm những function mới cho components của bạn mà không đụng đến code gốc của nó
- Ví dụ :
- Bạn có thể có một component Button cơ bản, Bạn có thể tạo cho nó các biến thể
PrimatyButtonvàSecondaryButtonbằng base component và thêm chỉnh sửa những style đặt biệt cho 2 loại button này, mà không làm thay đổi code hiện có, nó giống như mở rộng thêm cho ngôi nhà của bạn mà không cần phải xây dựng lại nó từ đầu
const Button = ({ children, style, ...props }) => (
<TouchableOpacity style={[styles.button, style]} {...props}>
<Text>{children}</Text>
</TouchableOpacity>
);
const PrimaryButton = styled(Button)`
background-color: #007bff;
color: #fff;
`;
const SecondaryButton = styled(Button)`
background-color: #fff;
border: 1px solid #007bff;
color: #007bff;
`;
Trên ví dụ trên sử dụng thư viện styled-components để style cho button
Component button ở trên là base component để khai báo core function (nhận truyền vào text content và style)
Nó có thể nhận vào props (ví dụ : styles để chỉnh sửa, children) và styled-components (PrimaryButton, SecondaryButton) là 2 styles cụ thể mà không cần chỉnh sửa lại code của base button
Cách làm này cho phép tạo một button mới mà không cần thay đổi code của components ban đầu theo quy tắt OCP
3. Liskov Substitution Principle (LSP):
Hãy tưởng tượng rặng bạn có các loại khác nhau của những chiếc xe (sports car, SUV), Bạn mong rằng chúng có tất cả các thuộc tính là giống nhau (volang, bánh xe, cửa xe etc...) cho dùng chúng có khác loại, Trong React Native, LSP đảm bảo rằng các lớp con (các component riêng biệt) có thể được sử dụng ở một base class (general component) mà đảm bảo rằng không có lỗi
Ví dụ :
Bạn có một base Input component để cho user nhập dữ liệu, Bạn có thể tạo nó là EmailInput là class con và được kế thừa từ base nhưng thêm vào thuộc tính valitation email, 2 components này có thể được sử dụng thay thế cho nhau giống như các loại xe khác nhau, nhưng EmailInput được cung cấp như là một chức năng thêm vào
class Input {
constructor(props) {
this.value = props.value;
}
getValue() {
return this.value;
}
setValue(newValue) {
this.value = newValue;
}
}
class EmailInput extends Input {
constructor(props) {
super(props);
this.validateEmail = (email) => {
// Email validation logic
};
}
setValue(newValue) {
if (this.validateEmail(newValue)) {
super.setValue(newValue);
} else {
// Handle invalid email input
}
}
}
// Usage
const nameInput = new Input({ value: "John Doe" });
const emailInput = new EmailInput({ value: "invalid@email" });
console.log(nameInput.getValue()); // "John Doe"
console.log(emailInput.getValue()); // "invalid@email" (if validation fails)
Base Input là class để khau báo những hành động cơ bản mà tất cả các input phải có trong ứng dụng (set data hoặc return data)
Components EmailInput kế thừa từ Input nhưng thêm login xử lý validate email
Chúng có thể được sử dụng thay thế (thay thế cho nhau) để user có thể nhập, nhưng EmailInput cung cấp thêm một function để validation Email
Nó đảm bảo khi user sử dụng EmailInput hoạt động đúng như mong đợi khi nó được thay cho lớp Input thông thường
4. Interface Segregation Principle (ISP):
Hãy tưởng tượng rằng bạn nhận được những thông tin mà bạn không cần thiết, ISP nó giống như đưa components của bạn chỉ những thông tin mà nó cần,Chia các giao diện lớn thành nhỏ hơn, cụ thể hơn Bạn tránh buộc các components phải phụ thuộc và tính năng mà không sử dụng chúng
- Exmaple :
- Thay vì bạn sử dụng chỉ một giao diện để hiện thị tất cả các fileds của
User, Bạn có thể chia nhỏ các thành phần giống nhưUserBasicInfo(name,email) vàUserFullInfo(có bao gồm địa chỉ, số điện thoại), Nó giống như chỉ cung cấp các thông tin cần thiết cho component đó, không đưa tất cả và chúng
- Thay vì bạn sử dụng chỉ một giao diện để hiện thị tất cả các fileds của
// Before (ISP violation)
const UserList = ({ users }) => (
<FlatList
data={users}
renderItem={({ item }) => <UserCard user={item} />}
/>
);
const UserCard = ({ user }) => (
<View>
<Text>{user.name}</Text>
<Text>{user.email}</Text>
<Button title="Load User Details" onPress={() => getUserDetails(user.id)} />
</View>
);
// After (ISP applied)
// Define an interface for data fetching
interface DataService {
getUsers(): Promise<User[]>;
getUserDetails(userId: number): Promise<User>;
}
// Implement a concrete data service (e.g., using API calls)
class ApiService implements DataService {
async getUsers() {
// Fetch users from API
}
async getUserDetails(userId: number) {
// Fetch user details from API
}
}
// Implement a mock data service for testing
class MockDataService implements DataService {
async getUsers() {
return [
{ id: 1, name: "John Doe", email: "john.doe@example.com" },
// ...
];
}
async getUserDetails(userId: number) {
return { id: userId, name: "Mocked User Name", email: "mocked@example.com" };
}
}
// UserList component with dependency injection
const UserList = ({ users, dataService }) => (
<FlatList
data={users}
renderItem={({ item }) => <UserCard user={item} dataService={dataService} />}
/>
);
// UserCard component with dependency injection
const UserCard = ({ user, dataService }) => (
<View>
<Text>{user.name}</Text>
<Text>{user.email}</Text>
<Button
title="Load User Details"
onPress={() => dataService.getUserDetails(user.id)}
/>
</View>
);
// Usage
const apiService = new ApiService();
const mockService = new MockDataService();
<UserList users={[]} dataService={apiService} /> // Production usage
<UserList users={mockUsers} dataService={mockService} /> // Testing usage
:::textinfo
DataService interface định nghĩa chia methods để lấy dữ liệu users (getUsers) và lấy thông tin cụ thể của một user (getUserDetails)
Điều này cho phép dependency Injection, nơi mà bạn có thể inject specific implementations (a.g., ApiService, MockDataService) dự trên môi trường (production, testing)
Components chỉ liên quan đến một method cụ thể mà nó cần, tránh liên quan đến những phụ thuộc không cần thiết và đó gọi là ISP :::
5. Dependency Inversion Principle (DIP)
Tưởng tượng rặng code của bạn thay đổi một thư viện hoặc tools cụ thể, DIP khuyến khích phụ thuộc vào abstractions (Trừu tượng hoá) thay vì concertions (Cụ thể hoá), Nó làm cho code của bạn thêm linh động và dễ dàng để test.
- Ví dụ: Thay vì import trực tiếp và sử dụng thư viện fetching cụ thể bên trong component, Bạn có thể khai báo nó là "DataService" và inject những code khác vào base (ví dụ ApiCall, LocalStorage) bên trong môi trường của bạn hoặc để test, nó giống như có một bộ sạc đa năng phù hợp với các loại adapter khác nhau, không giới hạn bất cứ một nhãn hiệu cụ thể nào
// Before (DIP violation)
const HomeScreen = () => {
const [products, setProducts] = useState([]);
useEffect(() => {
// Fetch products using a concrete data fetching function (e.g., fetchProducts)
setProducts(fetchProducts());
}, []);
// ... (rest of the component logic using products)
};
// After (DIP applied)
// Define an interface for data fetching
interface ProductService {
getProducts(): Promise<Product[]>;
}
// Implement a concrete product service (e.g., using API calls)
class ApiProductService implements ProductService {
async getProducts() {
// Fetch products from API
}
}
// Implement a mock product service for testing
class MockProductService implements ProductService {
async getProducts() {
return [
{ id: 1, name: "Product 1", price: 10 },
// ...
];
}
}
// HomeScreen component with dependency injection
const HomeScreen = ({ productService }) => {
const [products, setProducts] = useState([]);
useEffect(() => {
productService.getProducts().then(setProducts);
}, [productService]);
// ... (rest of the component logic using products)
};
// Usage
const apiService = new ApiProductService();
const mockService = new MockProductService();
<HomeScreen productService={apiService} /> // Production usage
<HomeScreen productService={mockService} /> // Testing usage
ProductService interface khai báo getProducts methods để lấy data của sản phẩm Nó có thể mở rộng dependency injection nơi mà HomeScreen nhận vào ProductServices và nó có thể sử dụng getProducs, thúc đẩy sự kết nối thiếu chặc chẽ giữa các api
Bởi khi sử dụng interfaces and dependency injection, chúng ta có thể dễ dàng chuyển đổi giữa các services khác nhau cho productuon hoặc testing nó gọi là DIP
Như vậy là chúng ta vừa xem qua các thành phần quan trọng của nguyên tắt SOLID, bạn có thể xây dựng một ứng dụng React Native không chỉ một function mà còn có thể có một cấu trúc tốt, dễ maintain và dễ dàng phát triển trong tương lai, nên nhớ rắng SOLID nói về cách bạn viết code dễ đọc, dễ tái sử dụng, dễ hiểu cho bạn và các developer khác về lâu về dài