Làm thế nào để cấu trúc các components trong React?
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 ...
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.