Cây giả: dùng thụt lề để có UI đơn giản hơn
(ratfactor.com)- Dù có vẻ cần UI phân cấp, điều cần kiểm tra trước tiên là dữ liệu có thật sự cần quan hệ cha-con hay chỉ cần trông giống như vậy
- Nếu không cần cây thật, có thể biểu diễn cấu trúc trên màn hình chỉ bằng thứ tự sắp xếp tuyệt đối của toàn bộ danh sách và giá trị
indent, thay vì ID cha - Trình biên tập game Hiss sắp xếp các tên như
banana.eat, rồi hiển thị phần sau dấu chấm (.) với thụt lề để tạo ra UI trông như namespace - Cách này gần với kiểu chỉnh sửa như trong trình xử lý văn bản, nơi người dùng di chuyển mục lên xuống và tăng/giảm thụt lề, nên giảm gánh nặng của cấu trúc dữ liệu cây
- Nếu thực sự cần truy vấn hoặc duy trì quan hệ giữa các mục, thì thay vì dùng thụt lề hay mẹo ký hiệu trong chuỗi, cần một mô hình cây thực thụ
Danh sách không phải cây nhưng trông như cây
- Khi muốn hiển thị các danh sách động như
Foo,Bartrong ứng dụng bằng tree view, ta thường nghĩ ngay đến cấu trúc nối mỗi mục với mục cha của nó - Trong cơ sở dữ liệu quan hệ, chẳng hạn có thể lưu ID cha bằng cột
parentFoocóparentlànullFoo 1cóparentlàFooFoo 1.acóparentlàFoo 1
- Để lấy dữ liệu cây kiểu này bằng SQL, có thể cần cách như recursive CTE
- Nhưng trong nhiều danh sách, điều quan trọng hơn quan hệ thực tế có thể là hình thức được sắp xếp gọn gàng để con người dễ nhìn
Cách lưu giá trị thụt lề như dữ liệu
- Nếu không cần quan hệ cha-con thực sự, có thể lưu danh sách chỉ với các trường sau
idsortindentname
sortkhông phải thứ tự bên trong từng mục con, mà là thứ tự tuyệt đối của toàn bộ danh sáchindentbiểu thị trực tiếp lượng khoảng trắng đặt trước mục, nên việc render giao diện trở nên đơn giản- UI chỉnh sửa cũng có thể đơn giản hơn thao tác trên cây
- Người dùng có thể di chuyển mục lên hoặc xuống
- Có thể tăng hoặc giảm thụt lề cho mục
- Nếu cần, có thể thêm các quy tắc đơn giản để ép thụt lề hợp lệ
- Kết quả là, thay vì thao tác trực tiếp trên cấu trúc dữ liệu kiểu giáo trình khoa học máy tính, trải nghiệm này gần hơn với chỉnh sửa danh sách trong trình xử lý văn bản
Namespace giả dựa trên dấu chấm (.) của Hiss
- Trình biên tập game phiêu lưu văn bản Hiss hiển thị các tên như
banana,banana.eat,banana.peeltheo kiểu phân cấp trong UI - Điều này không có nghĩa là HissScript đã triển khai tính năng namespace thực sự
- Cách triển khai rất đơn giản
- Sắp xếp tên đối tượng theo thứ tự chữ cái
- Nếu tên có dấu chấm (
.), cắt bỏ phần phía trước - In ra phần còn lại với thụt lề
- Logic cốt lõi trong đoạn mã ví dụ cũng đi theo đúng luồng đó
- Sắp xếp
things.keys - Nếu mỗi tên có dấu chấm thì thụt lề rồi bỏ phần trước dấu chấm khi in ra
- Nếu không có dấu chấm thì in nguyên tên
- Sắp xếp
- Sau đó có thêm vài dòng kiểm tra xem có tồn tại mục “cha” với tiền tố tương ứng hay không
- Cũng có thể bổ sung lồng nhau với độ sâu tùy ý, nhưng hiện tại vẫn đang chờ đến khi thực sự cần
- UI trông như namespace này quan trọng với người tổ chức game, nhưng không mang ý nghĩa đặc biệt với trình biên tập game hay người chơi
- Tên có dấu chấm cũng chỉ là tên bình thường
- Phần trông như namespace chỉ có vai trò giúp giữ cho tên là duy nhất
Những trường hợp giống cây nhưng được xử lý như danh sách phẳng
- Dave Long đề xuất một cách “cây thật công nghệ thấp”, trong đó đường dẫn và thông tin được lưu trong danh sách phẳng
- Đây là một nhận định tương tự ví dụ
banana.eat - Có thể hình dung danh sách đường dẫn theo dạng như đầu ra của
find./foo/zonk./foo/bonk./bar/boop/bop./bar/boop/bleep
- Nếu cần duyệt theo chiều sâu, chỉ cần sắp xếp từ điển các đường dẫn
- Nếu cần duyệt theo chiều rộng, có thể đảo ngược đường dẫn theo dấu phân cách, thêm các mục rỗng để cân bằng độ sâu rồi sắp xếp
- Ví dụ này nhằm minh họa khái niệm; trên thực tế, cách tự nhiên hơn là tách chuỗi theo dấu phân cách thành các mảng rồi xử lý
- Nhìn chung, danh sách phẳng dễ làm việc hơn, và nếu có thể thì nên ưu tiên đưa các mục vào plain old lists
Ẩn dụ scrapbook trên sàn nhà
- Khi làm scrapbook cá nhân, bạn có thể trải ảnh, ghi chú, bưu thiếp, vé và các thứ khác ra sàn rồi tạo thành các nhóm
- Với con người, quan hệ nhóm có thể nhìn rất rõ, nhưng bản thân sàn nhà không có cơ chế vật lý nào để ép buộc các quan hệ đó
- Điểm cốt lõi của ẩn dụ này là quan hệ được biểu đạt và quan hệ cấu trúc thực tế có thể khác nhau
- Danh sách trong UI cũng vậy: cách sắp xếp khiến con người thấy có phân cấp không nhất thiết có nghĩa là mô hình dữ liệu bên trong thật sự có phân cấp
Khi nào cần cây thực sự
- Cách dựa trên thụt lề hoặc ký hiệu trong chuỗi cần được điều chỉnh rất nhiều tùy từng tình huống, và trong ngữ cảnh lập trình nói chung có thể bị xem là một kiểu hack
- Nếu thực sự cần biết quan hệ giữa các mục, nên dùng cấu trúc cây thực thụ phù hợp với mô hình dữ liệu, như ID cha hoặc bảng join cha-con
- Nếu cần mức tổ chức như tủ hồ sơ vật lý và các cấp thư mục để phân loại những dự án nghiên cứu quy mô lớn, thì “cách trên sàn” sẽ không phù hợp
- Nếu một dự án về sau thực sự cần biết quan hệ giữa các mục mà ngay từ đầu lại giả lập cấu trúc bằng thụt lề hay số lượng ký hiệu trong chuỗi, đó có thể trở thành con đường đau đớn suốt vòng đời và thời gian bảo trì của dự án
1 bình luận
Các ý kiến trên Hacker News
Cách thứ nhất, tức cách trông như “hiển nhiên chỉ có cách này”, được gọi là danh sách kề (adjacency list)
Cách thứ hai, “đơn giản hơn nhiều”, tôi không nhớ đã từng thấy trước đây; nó có những nhược điểm rõ ràng, nhưng trong một số trường hợp có vẻ là đủ dùng
Cách thứ ba, “đặt namespace”, được gọi là đường dẫn cụ thể hóa (materialized path); còn có một cách khác để biểu diễn cây là tập lồng nhau (nested sets): https://www.ibase.ru/files/articles/programming/dbmstrees/sq...
Thời người ta còn nghiêm túc làm việc với cơ sở dữ liệu quan hệ, những điều này đều rất quen thuộc; chẳng hạn cũng có bài như http://www.dbazine.com/oracle/or-articles/tropashko4/
Giờ thì có vẻ như kiến thức đã bị lãng quên
Khi đang tự mình nắm bắt nhiều mặt của vấn đề, tôi thấy thật sự rất khó tìm ra tên gọi đã có của khái niệm đó
Cuối cùng họ xử lý toàn bộ logic hiển thị cây trong code, trong khi chỉ với cơ sở dữ liệu quan hệ hiện đại và một vài CTE cũng đã có thể xử lý rất nhiều use case một cách gọn gàng mà gần như miễn phí, nên khá đáng tiếc
https://www.oreilly.com/library/view/joe-celkos-trees/978155...
Postgres có kiểu dữ liệu ltree và các toán tử tìm kiếm hoạt động native theo kiểu này: https://www.postgresql.org/docs/current/ltree.html
Ví dụ, có thể thêm dữ liệu như
CREATE TABLE test (path ltree);,INSERT INTO test VALUES ('Top');,INSERT INTO test VALUES ('Top.Science');,INSERT INTO test VALUES ('Top.Science.Astronomy');rồi dùng
SELECT path FROM test WHERE path <@ 'Top.Science';để tìmTop.SciencevàTop.Science.AstronomyTrong ví dụ trên, dù xóa bản ghi
Top.Science, bản ghiTop.Science.Astronomycũng không bị cắt bỏCác label của giá trị ltree gợi ra một cây logic thông qua đường dẫn cụ thể hóa, nhưng không bắt buộc phải có bản ghi tương ứng với mọi nút cha được ngụ ý
Tùy ứng dụng, đây có thể chính xác là hành vi bạn muốn, hoặc hoàn toàn ngược lại. Nếu là trường hợp sau, cần có cơ chế riêng để duy trì tính toàn vẹn
/làm dấu phân cách không[1] https://learn.microsoft.com/en-us/sql/relational-databases/h...
Tuy nhiên cũng lo rằng index JSON có thể không hoạt động tốt bằng index ltree
Vấn đề ở đây là giá trị trong cấu trúc thường không nằm ở cây dùng để hiển thị, mà nằm ở cấu trúc phân cấp của dữ liệu
Rất có thể bạn sẽ phải làm những việc như duyệt dữ liệu, thể hiện quan hệ, sắp xếp lại, v.v.
Đưa thông tin trực quan vào cấu trúc dữ liệu của cơ sở dữ liệu có vẻ nguy hiểm và thiển cận
Câu trả lời là “Không, không thể thế được” sao?
YAGNI là một heuristic thiết kế nổi tiếng là có lý do. “Hãy luôn giả định là sẽ cần” không đúng
Chỉ là thay vì lưu trong một cột chuyên dụng với kiểu dữ liệu được tối ưu, nó được gắn vào đầu chuỗi dữ liệu
Nó có thể không phải số và cũng có thể không phải cột ID, nhưng vẫn là một định danh trỏ tới một giá trị kỳ vọng khác, nên việc đổi định dạng không khiến nó không còn là ID cha
Tất nhiên cần đảm bảo không lưu các thụt lề sai kiểu như con không có cha
Vì vậy tôi nghĩ cách dễ nhất là trước hết lưu theo thứ tự/độ sâu, rồi khi triển khai các tính năng cần thiết thì migrate sang mô hình cha/con
Tuy nhiên, “thụt lề” nên được định nghĩa trừu tượng hơn là độ sâu trong cây, chứ không phải số khoảng trắng để render. Như vậy dễ tìm dữ liệu sai, dễ migrate về sau, và cũng có độ linh hoạt render theo từng người dùng như dùng
/lồng nhau, tab, 8 dấu cách, 4 dấu cách, 1 dấu cách, v.v.struct item_t { char key[255]; char display_value[255]; }, và key có dấu phân cách đường dẫn nhất quán nhưa/b/c, thì việc tìm cha và con rất dễTrường hợp tệ nhất thì quét tuyến tính mảng là được; nếu đã được sắp xếp, chỉ cần nhìn các mục trước đó cho tới khi tới cha
Tôi từng bắt đầu một công ty có rất nhiều dữ liệu dạng cây. Chuyển cấu trúc cây thành danh sách thụt lề có thể làm trong thời gian O(n)
Đó từng là một trong các câu hỏi phỏng vấn khi ấy, và có nhiều cách lưu trữ trong các cơ sở dữ liệu SQL để có thể lấy nhanh một phần cây và render mà không cần truy vấn đệ quy
Khi đã hiểu các khái niệm này, việc lưu dữ liệu đúng nghĩa là cây có nhiều lợi thế hơn hẳn so với kiểu thụt lề như thế này
“Một cách để lấy dữ liệu cấu trúc cây từ cơ sở dữ liệu quan hệ bằng truy vấn SQL là dùng CTE (Common Table Expressions) đệ quy, và nó thú vị đúng như cái tên của nó”
CTE, kể cả CTE đệ quy, không có gì đáng sợ; khi quen rồi tôi đảm bảo là nó thực sự thú vị
Để dựng đường dẫn nút có độ sâu phân cấp là d, thời gian nhận kết quả truy vấn chậm hơn ít nhất d lần
Ưu điểm là thao tác chỉnh sửa cây rẻ, nhưng chuyện đó xảy ra hiếm hơn nhiều so với đọc
Có thể thấy sự khác biệt giữa HN và Reddit ở điểm “thường thì mọi người không thực sự muốn hay cần một cây, mà chỉ cần thứ trông giống cây”
Trên HN, bình luận con là
nextSiblingcủa bình luận cha, và được làm cho trông như cây bằng cách cộng thêm 1 vào giá trị thụt lề của chaCòn Reddit, ít nhất là old.reddit.com, thì bình luận con thực sự được lồng bên trong bình luận cha. Trang mới thì tôi không rõ
Mọi thao tác trên dữ liệu sẽ trở thành một mớ phức tạp: suy luận cấu trúc cây rồi dịch ngược lại sang định dạng cây ngầm định
Ý chính của bài rất đơn giản: hãy dùng cấu trúc phù hợp với vấn đề
Tuy nhiên tôi cho rằng phần kể chuyện bị sai. Không nhất thiết phải dùng CTE để lấy cây từ cơ sở dữ liệu; có thể lấy một danh sách phẳng rồi dựng cây cục bộ. Dù sao sau đó cũng rất có thể phải làm vậy để thao tác tiếp
Theo cùng logic đó, có thể bảo người dùng cơ sở dữ liệu quan hệ để lưu danh sách rằng hãy lưu vào file văn bản. Tại sao phải chịu chi phí độ trễ mạng?
Ngược lại, cấu trúc được đề xuất hoạt động không tốt khi cần di chuyển nhánh và thay đổi độ sâu trong một cây đủ lớn, vì nó tốn chi phí tuyến tính
Lẽ ra nên nói rõ ý định ngay từ đầu. Đừng giải thích ba ví dụ rồi đến kết luận lại vô hiệu hóa bằng câu “nếu cần cây thì hãy dùng cây”. Dù vậy, nếu đặt điều đó ở đầu bài thì đã đỡ clickbait hơn nhiều
Vài năm trước tôi có một nhận ra tương tự về OpenGL. Điều cần vẽ không phải là một thế giới các đối tượng 3D phân cấp, mà là một danh sách tam giác đã sắp xếp
Ý nghĩ đó như bật công tắc trong đầu tôi, và nhiều tối ưu hóa trở nên rất dễ dàng
Ngay cả trong các game có hệ phân cấp entity phức tạp, khi đưa vào render queue thì thường vẫn phải làm phẳng vì các lý do như sắp xếp theo độ trong suốt
“Danh sách phẳng các sự vật” cũng là nền tảng của ECS/DOD
Có hẳn cả một cuốn sách về cách xử lý những việc như vậy trong cơ sở dữ liệu
https://www.oreilly.com/library/view/joe-celkos-trees/978155...
Một cách khác để tạo cây giả là lưu blob JSON
Nếu dữ liệu chỉ có các quan hệ nội bộ, cách này có thể dễ hơn việc cố giữ số thứ tự vừa duy nhất vừa có thứ tự