Làm thế nào để cấu trúc các components trong React?
Bài đăng này đã không được cập nhật trong 6 năm
Lập trình là một nhiệm vụ khá phức tạp. Đặc biệt tạo ra clean code là rất khó. Chúng ta cần phải quan tâm nhiều yếu tố - đặt tên các biến, phạm vi function, xử lý các lỗi, đảm bảo security, giám sát performance, ... Còn để đặt tên điều khó nhất trong lập trình, tôi sẽ bắt đầu với bài viết các components lỏng lẻo & gắn kết rất chặt chẽ. Nó không quan trọng nếu chúng ta đang nói về lập trình OOP hoặc lập trình functional. Cấu trúc hệ thống là điều khó nhất và nó có tác động lớn đến tổng thể project. Phải mất nhiều năm để trở nên thành thạo trong thiết kế cấu trúc phần mềm (và có lẽ một người không bao giờ có thể nắm chắc nó - trong một ngành công nghiệp phát triển nhanh như vậy luôn luôn là một bước tiến, luôn luôn có một cách để cải thiện thiết kế).
Tôi thực sự thích làm việc với React & tôi nghĩ rằng lợi thế lớn nhất của nó là React đơn giản như thế nào. Có một sự khác biệt giữa đơn giản và dễ dàng https://www.infoq.com/presentations/Simple-Made-Easy. Và tôi thực sự có nghĩa là React rất đơn giản. Tất nhiên, bạn cần dành chút thời gian để tìm hiểu nó. Nhưng sau khi bạn hiểu các khái niệm cốt lõi, mọi thứ khác chỉ là kết quả. Phần khó sẽ xuất hiện tiếp theo.
Khớp nối & Gắn kết (Coupling & Cohesion)
Đó là các chỉ số ít nhiều mô tả sự khó khăn như nào để thay đổi behaviour của code. Khớp nối & gắn kết được sử dụng trong OOP và tham khảo một số dạng class. Chúng ta sẽ dùng chúng tham chiếu đến các components React kể từ khi áp dụng các quy tắc tương tự.
Khớp nối là sự kết nối hay phụ thuộc giữa các components. Nếu thay đổi một element yêu cầu thay đổi element khác thì ta nói có kết nối chặt chẽ. Nếu các elements là cặp lỏng lẻo, việc thay đổi một element không bao hàm thay đổi trong element khác. Ví dụ, hãy xem sự hiển thị số tiền chuyển khoản ngân hàng. Nếu số tiền hiển thị biết tỷ lệ được tính như thế nào thì bất cứ khi nào cấu trúc bên trong giao dịch thay đổi, code hiển thị cũng cần phải được update. Nếu chúng ta thiết kế hệ thống ở dạng lỏng lẻo, dựa trên giao diện của một element, thì thay đổi để chuyển khoản không nên đưa đến kết quả thay đổi view layer. Các components lỏng lẻo là dễ dàng hơn để quản lý và maintain.
Gắn kết cho biết vai trò của một element có tạo thành một thứ gì đó. Nó được kết nối với nguyên tắc Single Responsibility hay Unix: Do one thing and do it well - Làm 1 thứ và làm cho nó tốt. Nếu định dạng số dư tài khoản cũng tính lãi suất và kiểm tra sự cho phép để hiển thị lịch sử, thì nó có nhiều trách nhiệm và không liên quan đến nhau. Có lẽ, cần có các components khác nhau cho phép xử lý hoặc lãi suất. Mặt khác, nếu có nhiều components: một phần số nguyên, một cho số thập phân và một cho tiền tệ, thì bất cứ lúc nào lập trình viên muốn hiển thị sự cân bằng, họ sẽ cần phải tìm tất cả các elements. Thách thức là tạo ra các components gắn kết cao.
Cấu trúc các components
Có rất nhiều cách chúng ta có thể cấu trúc các components. Chúng ta muốn các components có thể tái sử dụng, nhưng chỉ ở mức độ hợp lý. Chúng ta muốn xây dựng các components nhỏ có thể được sử dụng để xây dựng các khái niệm lớn hơn. Lý tưởng nhất là chúng ta muốn xây dựng các components lỏng lẻo và gắn kết cao, nên hệ thống của chúng ta sẽ dễ dàng maintain và phát triển. Trong React các components props có thể được xử lý như các function arguments và đó chính xác là trường hợp cho các components không có chức năng. Làm thế nào chúng ta xác định props trong một component, định nghĩa cách một component có thể được tái sử dụng.
Chúng ta sẽ sử dụng tên miền quản lý chi phí và chúng ta sẽ phân tích các trình bày chi tiết chi phí. Giả sử rằng mô hình chi tiêu sẽ như sau:
type Expense {
description: string
category: string
amount: number
doneAt: moment
}
Có một số khả năng để mô hình chi tiết chi phí định dạng:
- không có props
- truyền đối tượng chi phí (expense object)
- chỉ truyền các thuộc tính cần thiết
- truyền mảng các thuộc tính
- truyền định dạng như là một con
Chúng ta sẽ thảo luận từng thứ trong số chúng để xem những gì là lợi ích và sai sót của việc sử dụng mỗi và mọi. Hãy nhớ rằng context là vua và tất cả mọi thứ phụ thuộc vào hệ thống. Đó là chính xác những gì chúng ta được trả tiền - xây dựng proper abstraction (sự trừu tượng thích hợp).
Không có props
Giải pháp đơn giản nhất và thường là điểm khởi đầu là xây dựng một component với dữ liệu hard-code cứng.
const ExpenseDetails = () => (
<div className='expense-details'>
<div>Category: <span>Food</span></div>
<div>Description: <span>Lunch</span></div>
<div>Amount: <span>10.15</span></div>
<div>Date: <span>2017-10-12</span></div>
</div>
)
Không truyền props, tất nhiên, không cho chúng ta sự linh hoạt và component chỉ thích hợp khi sử dụng ở nơi đơn lẻ. Tất nhiên, trong ví dụ về chi tiết chi phí, chúng ta có thể thấy ngay từ đầu rằng component cần phải chấp nhận một số props. Tuy nhiên, có những trường hợp mà các components không có bất kỳ props lại là giải pháp tốt. Đầu tiên, chúng ta có thể sử dụng các component mà không có props cho "không đổi" nội dung như huy hiệu, biểu tượng, thông tin công ty, ...
const Logo = () => (
<div className='logo'>
<img src='/logo.png' alt='DayOne logo'/>
</div>
)
Xây dựng các components nhỏ thậm chí làm cho một hệ thống dễ maintain hơn. Giữ thông tin ở một nơi cho phép thay đổi ở một nơi. Đừng lặp lại chính mình.
Truyền đối tượng expense
Trong trường hợp miêu tả chi tiết chi phí, chúng ta cần phải truyền dữ liệu cho component. Đầu tiên, chúng ta sẽ xem xét truyền expense object.
const ExpenseDetails = ({ expense }) => (
<div className='expense-details'>
<div>Category: <span>{expense.category}</span></div>
<div>Description: <span>{expense.description}</span></div>
<div>Amount: <span>{expense.amount}</span></div>
<div>Date: <span>{expense.doneAt}</span></div>
</div>
)
Truyền expense object vào component chi tiết expense làm ý nghĩa hoàn hảo. Định dạng chi tiết expense rất chặt chẽ -> nó hiển thị dữ liệu của expense. Bất cứ khi nào chúng ta muốn thay đổi định dạng, đây là nơi duy nhất sẽ thay đổi. Và thay đổi định dạng chi tiết expense không giới thiệu bất kỳ tác dụng phụ nào đối với expense object.
Component được kết hợp chặt chẽ với expense object. Đó có phải là một điều tồi tệ? Chắc chắn không phải, nhưng chúng ta phải biết nó ảnh hưởng đến hệ thống của chúng ta như thế nào. Truyền expense object như props, kết quả mà component chi tiết expense phụ thuộc vào cơ cấu bên trong của expense. Bất cứ khi nào chúng ta thay đổi cơ cấu bên trong của expense, chúng ta sẽ cần phải thay đổi các chi tiết expense. Tất nhiên, chúng ta sẽ chỉ cần thay đổi ở một nơi.
Thiết kế đó ảnh hưởng đến những thay đổi trong tương lai như thế nào? Nếu chúng ta muốn thêm, thay đổi hoặc loại bỏ một trường, chúng ta chỉ cần thay đổi một component. Điều gì sẽ xảy ra nếu chúng ta muốn thêm các định dạng ngày khác? Chúng ta có thể thêm một cách khác để định dạng ngày.
const ExpenseDetails = ({ expense, dateFormat }) => (
<div className='expense-details'>
<div>Category: <span>{expense.category}</span></div>
<div>Description: <span>{expense.description}</span></div>
<div>Amount: <span>{expense.amount}</span></div>
<div>Date: <span>{expense.doneAt.format(dateFormat)}</span></div>
</div>
)
Chúng ta bắt đầu thêm các thuộc tính bổ sung để làm cho component linh hoạt hơn. Miễn là chỉ có một vài lựa chọn, mọi thứ đều tuyệt vời. Vấn đề bắt đầu sau khi hệ thống phát triển và chúng ta có rất nhiều props cho các trường hợp sử dụng khác nhau.
const ExpenseDetails = ({ expense, dateFormat, withCurrency, currencyFormat, isOverdue, isPaid ... })
Thêm props làm cho các component có thể tái sử dụng nhiều hơn, nhưng nó cũng có thể là một dấu hiệu cho thấy có nhiều trách nhiệm của component. Quy tắc tương tự áp dụng cho function. Chúng ta có thể tạo function với một số parameters, nhưng ngay khi số đó lớn hơn 3-4, nó bắt đầu làm rất nhiều thứ. Và có lẽ đó là lúc để chia function thành nhỏ hơn.
Khi số lượng props thành phần tăng lên, chúng ta có thể quyết định chia thành các component chính xác hơn như: OverdueExpenseDetails, PaidExpenseDetails, ...
Chỉ truyền các thuộc tính cần thiết
Để ít kết hợp với chính đối tượng expense, chúng ta chỉ có thể chuyển các thuộc tính cần thiết.
const ExpenseDetails = ({ category, description, amount, date }) => (
<div className='expense-details'>
<div>Category: <span>{category}</span></div>
<div>Description: <span>{description}</span></div>
<div>Amount: <span>{amount}</span></div>
<div>Date: <span>{date}</span></div>
</div>
)
Chúng ta đang truyền mỗi và mọi thuộc tính riêng biệt, vì vậy chúng ta đang chuyển một phần trách nhiệm đến người đang sử dụng component. Nếu cấu trúc bên trong expense thay đổi, nó sẽ không ảnh hưởng đến định dạng chi tiết expense -> nhưng có lẽ nó có thể ảnh hưởng tới mọi nơi đang sử dụng component bởi props cần được thay đổi. Khi truyền props thành các thuộc tính riêng lẻ, một component sẽ trừu tượng hơn.
Chỉ truyền các trường cần thiết ảnh hưởng như thế nào đến thiết kế tương lai?
Adding/updating/removing các trường không phải là dễ dàng ngay bây giờ. Bất cứ khi nào chúng ta muốn thêm 1 trường, chúng ta không chỉ cần thay đổi thực hiện của chi tiết expense mà còn phải thay đổi mọi nơi component được sử dụng.
const ExpenseDetails = ({ category, description, amount, date, account, comment, case ... }) => ( ... )
Mặt khác, hỗ trợ nhiều định dạng ngày được thực hiện gần như out-of-the-box. Kể từ khi chúng ta truyền ngày thành prop, chúng ta có thể truyền ngày đã định dạng.
<ExpenseDetails
category={expense.category}
description={expense.description}
amount={expense.amount}
date={expense.doneAt.format('YYYY-MM-DD')}
/>
Quyết định làm thế nào để hiển thị trường cụ thể nằm trong tay của người sử dụng các components. Đó không còn là trường hợp thực hiện component chi tiết expense.
Truyền map/array các thuộc tính
Sẽ trừu tượng hơn nữa, chúng ta sẽ truyền một map các thuộc tính.
const ExpenseDetails = ({ expense }) => (
<div class='expense-details'>
{
_.reduce(expense, (acc, value, key) => {
acc.push(<div>{key}<span>{value}</span></div>)
}, [])
}
</div>
)
Người sử dụng component kiểm soát định dạng chi tiết expense. Object được truyền đến component phải được định dạng đúng.
const expense = {
"Category": "Food",
"Description": "Lunch",
"Amount": 10.15,
"Date": 2017-10-12
}
Giải pháp đó có nhiều sai sót. Chúng ta có rất ít sự kiểm soát đối với component này. Thứ tự của reduce không được chỉ định, nên chúng ta sẽ cần thêm vài loại đặt hàng. Thay vì một map, chúng ta có thể truyền một mảng với các object để vượt qua vấn đề đó, nhưng nó vẫn sẽ có những nhược điểm.
Truyền map/array thành props không phải là kết hợp để expense tất cả, nhưng cũng không gắn kết tất cả. Adding/removing các thuộc tính mới chỉ là vấn đề thay đổi prop, nhưng chúng ta không kiểm soát được định dạng của chính component. Nếu chúng ta muốn thay đổi chỉ định dạng của thể loại, thì không thể là giải pháp này. (Chính xác, luôn có một cách để tinh chỉnh các công cụ. Ví dụ bằng cách truyền một props khác với cấu hình định dạng. Tuy nhiên, giải pháp đó không còn sạch sẽ và đơn giản.)
Truyền định dạng như là một đứa trẻ
Chúng ta cũng có thể có trách nhiệm ít nhất có thể và truyền dữ liệu như một đứa trẻ.
const ExpenseDetails = ({ children }) => (
<div class='expense-details'>
{ children }
</div>
)
Trong trường hợp đó, chi tiết expense chỉ là một container để cung cấp một số cấu trúc và kiểu dáng. Để hiển thị chi tiết một thành phần sử dụng component phải cung cấp tất cả thông tin.
<ExpenseDetails>
<div>Category: <span>{expense.category}</span></div>
<div>Description: <span>{expense.description}</span></div>
<div>Amount: <span>{expense.amount}</span></div>
<div>Date: <span>{expense.doneAt}</span></div>
</ExpenseDetails>
Có lẽ trong trường hợp chi tiết expense, nó không phải là một giải pháp tốt, vì chúng ta sẽ cần phải lặp lại rất nhiều. Tuy nhiên, tính linh hoạt là rất lớn và có rất nhiều khả năng định dạng khác nhau. Adding/removing/updating các trường chỉ là vấn đề thay đổi việc sử dụng component. Cùng với định dạng ngày. Chúng ta mất sự gắn kết, nhưng đó là cái giá mà chúng ta phải trả.
Context là vua
Như bạn thấy, chúng ta đang trao đổi những lợi thế và khả năng khác nhau. Vậy cái nào là tốt nhất? Nó phụ thuộc vào:
- bản thân project
- giai đoạn của project
- các component - chúng ta có muốn các component cụ thể hơn hay một vài options
- theo sở thích
- các yêu cầu - là thành phần cần thay đổi thường xuyên và được sử dụng thường xuyên
Không có giải pháp tốt duy nhất. Một size không thể vừa tất cả. Cách chúng ta cấu trúc các thành phần của chúng ta có tác động lớn tới cách chúng ta sẽ maintain một hệ thống và khả năng mở rộng nó như thế nào. Tất cả phụ thuộc vào context. Rất may chúng ta có rất nhiều lựa chọn và chúng ta có thể chọn và lựa chọn. Các components là một sự trừu tượng tuyệt vời để xây dựng cả các hệ thống nhỏ và lớn. Nó chỉ là một trường hợp chọn đúng giải pháp.
Reference: https://reallifeprogramming.com/how-to-structure-components-in-react-54fc43e71546
All rights reserved