1 điểm bởi GN⁺ 2024-08-17 | 1 bình luận | Chia sẻ qua WhatsApp
  • Dựa trên một câu trích dẫn của Linus Torvalds, thiết kế tốt bắt đầu từ việc xác lập ổn định cấu trúc dữ liệu và các mối quan hệ trước khi viết mã
  • Một mô hình dữ liệu được thiết kế tốt sẽ tự nhiên đơn giản hóa logic ứng dụng, đồng thời làm cho phần mềm đáng tin cậy và dễ hiểu hơn
  • Nếu trì hoãn mô hình dữ liệu, khối lượng công việc về sau sẽ tăng lên; nhưng nếu xây dựng cấu trúc tốt ngay từ đầu, việc migration và mở rộng các hệ thống phức tạp sẽ dễ dàng hơn
  • Trong một dự án, thay vì tối ưu hóa thuật toán phức tạp, việc tái cấu trúc dữ liệu đã loại bỏ hẳn một phạm trù vấn đề và thay thế một hàm 500 dòng bằng một hàm 50 dòng cùng cấu trúc dữ liệu
  • Trong thực tế, cần áp dụng type nghiêm ngặt hơn cho interface và cơ sở dữ liệu, đồng thời thiết kế luồng dữ liệu và tương tác giữa các component trước khi đi vào chi tiết mã

Cấu trúc dữ liệu quyết định thiết kế mã

  • Linus Torvalds xem Git là một thiết kế đơn giản với cấu trúc dữ liệu ổn định và được tài liệu hóa, đồng thời nhấn mạnh cách đặt mã xung quanh dữ liệu
    • Câu trích dẫn “lập trình viên tệ lo lắng về mã, lập trình viên giỏi lo lắng về cấu trúc dữ liệu và các mối quan hệ của chúng” là câu then chốt
    • Một trong những lý do Git thành công là thiết kế mã theo hướng lấy dữ liệu làm trung tâm
  • Cấu trúc dữ liệu tốt giúp việc thiết kế và bảo trì mã dễ hơn, đồng thời nâng cao độ tin cậy của phần mềm, khả năng hiểu hệ thống và khả năng đọc mã
    • Logic ứng dụng thường đi theo mô hình dữ liệu
    • Nếu nghĩ về mô hình dữ liệu sau cùng, khối lượng công việc về sau sẽ tăng lên
    • Một mô hình dữ liệu được thiết kế tốt giúp việc migration và mở rộng các hệ thống phức tạp về sau dễ dàng hơn
  • Trong một ví dụ dự án thực tế, tái cấu trúc dữ liệu mang lại hiệu quả lớn hơn so với việc tiếp tục tinh chỉnh một thuật toán phức tạp
    • Thay đổi cấu trúc dữ liệu đã loại bỏ cả một phạm trù vấn đề
    • Một hàm 500 dòng được thay thế bằng một hàm 50 dòng và cấu trúc dữ liệu được thiết kế tốt
    • Mã mới nhanh hơn, đồng thời cũng dễ hiểu và dễ bảo trì hơn
    • Tuy nhiên, vì dữ liệu hiện có phải được tái cấu trúc, công sức đã được chuyển xuống tầng bên dưới

Tốt hơn nên chuyển độ phức tạp sang phía dữ liệu

  • “Rule of Representation” trong The Art of Unix Programming giải thích rằng nên đưa tri thức vào dữ liệu để làm cho logic chương trình đơn giản và vững chắc
    • Logic thủ tục khó để con người kiểm chứng, trong khi các cấu trúc dữ liệu phức tạp dễ mô hình hóa và suy luận hơn
    • Một sơ đồ cây con trỏ 50 nút có thể có sức biểu đạt và khả năng giải thích cao hơn một lưu đồ chương trình 50 dòng
    • Biểu diễn bảng chuyển đổi bằng khởi tạo mảng sẽ minh bạch và rõ ràng hơn so với viết cùng nội dung bằng câu lệnh switch
    • Nếu phải chọn đặt độ phức tạp ở mã hay cấu trúc dữ liệu, tốt hơn nên chuyển độ phức tạp sang cấu trúc dữ liệu

Trong thực tế, hãy thiết kế luồng dữ liệu trước

  • Cách thực hành trực tiếp nhất là bắt đầu từ dữ liệu
    • Áp dụng type nghiêm ngặt hơn cho interface hoặc cơ sở dữ liệu có thể giảm độ phức tạp của mã
    • Cần dành nhiều thời gian hơn từ sớm để suy nghĩ về cấu trúc dữ liệu
    • Điều này không có nghĩa là mã không quan trọng; mọi yếu tố đều quan trọng cùng nhau
    • Trước khi đi vào chi tiết mã, cách tiếp cận ở cấp cao hơn—trước hết xem dữ liệu chảy như thế nào và các component tương tác ra sao—là hữu ích
  • Lấy ví dụ về yêu cầu đối với Senior Engineer (L5), ở FAANG thường bao gồm việc viết tài liệu thiết kế cấp cao cho các hệ thống phức tạp hơn
    • Việc dẫn dắt kế hoạch của team và xây dựng roadmap tốt cho các tính năng cỡ vừa đến lớn cũng nằm trong đó
    • Năng lực thiết kế luồng dữ liệu và tương tác giữa các component trước tiên gắn liền với tầm ảnh hưởng kỹ thuật ở cấp cao hơn

1 bình luận

 
GN⁺ 2024-08-17
Ý kiến trên Hacker News
  • Bài viết trên Substack đó có vẻ như chỉ đơn giản chép lại nhiều đoạn trích từ bài viết này trên Stack Exchange: https://softwareengineering.stackexchange.com/questions/1631...

    • URL đã được đổi ở https://read.engineerscodex.com/p/good-programmers-worry-abo.... Vài tháng trước cũng đã có một đợt spam Substack như thế này trong một thời gian, có vẻ cùng một tác nhân xấu lại bắt đầu tiếp
    • Việc hắn thậm chí còn không đổi cả thứ tự của các câu trích dẫn đúng là khá trơ trẽn
  • “Hãy cho tôi xem lưu đồ [code] của bạn và giấu các bảng [schema] đi, tôi vẫn sẽ tiếp tục bối rối. Hãy cho tôi xem các bảng [schema], thường thì tôi sẽ không cần lưu đồ [code]; chúng sẽ hiển nhiên thôi.” — Fred Brooks, “The Mythical Man Month”, chương 9

    • Hồi còn nhỏ và mê Quake, tôi từng viết thư cho John Carmack để hỏi xem ông có lời khuyên nào cho một người muốn trở thành lập trình viên và có cuốn sách yêu thích nào không. Đáng ngạc nhiên là ông đã trả lời khá chu đáo, và trong đó có câu này:
      “Hãy đọc The Mythical Man Month. Tôi nhớ mình từng nghĩ rằng một cuốn sách cũ đến vậy không thể có điều gì còn liên quan đến phát triển phần mềm ngày nay, nhưng tôi đã sai.”
    • Tôi vào đây để chia sẻ câu trích dẫn này vì nó quá đúng. Tuy vậy, có một ngoại lệ là khi chi phí thay đổi schema cơ sở dữ liệu trở nên lớn hơn rất nhiều so với chi phí thay đổi code.
      Khi đó các lập trình viên ứng dụng sẽ bắt đầu lạm dụng cơ sở dữ liệu vì họ nhanh hơn và có nhiều việc phải làm hơn
  • Cấu trúc dữ liệukiểu không phải là một. Cấu trúc dữ liệu là các mẫu bit và các tham chiếu đến những mẫu bit khác, tức là con trỏ hoặc quan hệ.
    Kiểu áp các ràng buộc lên những mẫu bit đó theo cách chúng được dùng trong ngôn ngữ lập trình, nhưng ngoài ra còn có thể biểu đạt nhiều tính năng ngôn ngữ khác. Việc tạo ra các hệ phân cấp kiểu phức tạp bằng những tầng trừu tượng không cần thiết không có nghĩa là “quan tâm đến cấu trúc dữ liệu”, và đây là một kiểu thất bại mà ngay cả các kỹ sư thông minh cũng thường mắc phải

    • Đây là một điểm tinh tế nhưng quan trọng. Kiểu có thể là công cụ hữu ích để ràng buộc và đặc tả schema của cấu trúc dữ liệu, nhưng quan tâm đến kiểu và quan tâm đến cấu trúc dữ liệu là hai việc khá khác nhau
    • Kiểu là cấu trúc dữ liệu mà ngôn ngữ nhận biết. Vì thế công cụ có thể thực hiện các kiểm tra mà nó không thể làm với cấu trúc dữ liệu thông thường
    • Cấu trúc dữ liệu là một thuật toán đứng yên. Nó xáo trộn và di chuyển mọi thứ trong từng phép toán, nhưng nhìn chung vẫn nằm yên, giống một máy Turing mà thỉnh thoảng con người mới vặn tay quay.
      Kiểu là các bit trên đĩa
    • Nhận xét hay. Đồng nhất cấu trúc dữ liệu với kiểu là một sự đơn giản hóa quá mức làm bỏ lỡ trọng tâm.
      Điều tác giả ban đầu muốn nói ở đây gần hơn với việc hãy suy nghĩ sâu hơn về vấn đề và đừng chọn một cấu trúc sau này sẽ gây vướng chân mình. Ví dụ, hãy nhìn xem Unix pipe đã lan rộng đến mức nào và đã mở rộng sang bao nhiêu lĩnh vực cùng trường hợp sử dụng. Đó là một cách tuyệt vời để hình dung cách xây dựng hệ thống trong khi vẫn tôn trọng các ràng buộc của con người và máy móc.
      Cũng phải mất khá nhiều thời gian thì Ken Thompson và những người khác mới nhận ra rằng thứ như pipe lại hợp lý trong Unix. Đó không phải là một insight có được dễ dàng, mà đòi hỏi sự kiên trì trong việc tìm ra các khối cấu thành đúng cho hệ thống và cả công việc tiếp theo sau đó
    • Có thể gán các kiểu khác nhau cho cùng một cấu trúc dữ liệu. Đó chính là điều toán tử typedef của Pascal làm
  • Linus luôn rất giỏi trong việc tóm lược những điều mà người khác chỉ nghĩ một cách mơ hồ. Nội dung bài viết cũng khá giống với DDD, một kỹ năng dường như đã bị thất truyền.
    “Bị thất truyền” ở đây có nghĩa là phần lớn lập trình viên tôi gặp ngày nay quan tâm đến việc chuyển qua chuyển lại thuật toán và JSON hơn là hiểu domain mình đang xử lý rồi mô hình hóa entity và các tương tác của chúng. Trong các thiết kế hiện đại dựa trên AWS, điều này thường hiện ra dưới dạng những cụm DynamoDB GSI thiếu cơ sở rõ ràng, các object nghèo nàn, và tầng “service” kiểu script chồng hack lên hack. Có lẽ từng tồn tại một giả định ngầm rằng ngữ cảnh domain sẽ được định nghĩa đủ rõ trong ranh giới service, nhưng tôi không nghĩ đó là một giả định tốt.
    Tôi không biết ngành của chúng ta đã đánh mất sự chặt chẽ trong thiết kế từ đâu. Ở trường học, trong quy trình phỏng vấn, do hạ thấp tiêu chuẩn, hay là tất cả những điều đó

    • Tôi cho rằng ngành này chưa bao giờ thực sự xem trọng thiết kế phần mềm. Nó luôn bị đối xử như thứ gì đó mang tính tiêu cực, gắn với những nhân vật bị xem là sai về mặt chính trị hoặc vô nghĩa, và kéo theo vô số bình luận kiểu chỉ vì một ai đó từng làm sai điều gì đó mà kết luận mọi thứ đều tệ.
      Tệ hơn nữa, thiết kế còn phạm phải cái tội lớn là không dễ tự động hóa. Vì thế mọi người làm theo một cách thiếu phản biện những thiết kế mà công cụ áp đặt, và cảm thấy khó chịu với ý nghĩ rằng họ cần suy nghĩ sâu hơn về công việc mình đang làm. Ai cũng muốn thuê ngoài kiểu tư duy này cho “chuyên gia”.
      Vấn đề còn ở chỗ nó không được dạy dỗ tử tế, phải tự học trong nhiều năm, và bị xem là kém “thực” hơn code nên bị coi là kém quan trọng hơn. Nhưng niềm tin đó rốt cuộc chỉ trói mức độ những gì ta có thể tạo ra ở trình độ người mới khá lên chút ít. Các lập trình viên, với tư cách một tập thể, dường như đã chọn giữ tiêu chuẩn ở mức thấp nhất có thể, và ở chủ đề này gần như có tâm lý kéo nhau xuống như cua trong giỏ
    • Mô hình domain nghèo nàn đã được xác định là một phản mẫu từ khá lâu rồi[1]. Nó thường đi kèm với ám ảnh kiểu nguyên thủy[2], và dẫn đến việc rải khắp nơi đủ loại mã kiểm tra, xác thực cho các kiểu nguyên thủy như chuỗi và số.
      Cũng phát sinh rất nhiều trùng lặp mã mà về mặt cú pháp thì không giống nhau nên không trông như bị lặp, nhưng về mặt chức năng lại làm cùng một việc.
      1 https://martinfowler.com/bliki/AnemicDomainModel.html
      2 https://wiki.c2.com/?PrimitiveObsession
    • Ngành này chủ yếu thưởng cho việc viết code chứ không phải thiết kế phần mềm.
      Tôi nghĩ là vì hậu quả của code tệ khó thấy hơn. Một cây cầu tệ sẽ sập, còn code tệ thì chỉ bị refactor hoặc bị thay bằng nhiều code hơn. Chỉ là một file văn bản mà ban quản lý không hiểu được đổi thành một file văn bản khác mà ban quản lý cũng không hiểu được.
      Và một khi thứ gì đó chạy được thì sẽ bị đóng băng. Không có gì lâu dài hơn một bản hack tạm thời mà lại hoạt động hoàn hảo. Nhưng 1000 bản hack tạm thời không thể tạo thành một hệ thống được kỹ nghệ hóa tốt. Tôi cho rằng sự trưởng thành trong phát triển phần mềm là tập trung vào dữ liệu và các mối quan hệ hơn là vào việc viết code. Bạn vẫn phải chuyển điều đó thành code, nhưng không phải biến code đang chạy thành mô hình dữ liệu, mà là biến dữ liệu và các mối quan hệ thành code
    • Tôi vẫn chưa thấy một lý do thực sự thuyết phục nào giải thích vì sao object nghèo nàn lại bị xem là điều cấm kỵ đến vậy. Phần lớn các hàm DDD mà tôi từng thấy cũng chỉ là getter và setter dài dòng.
      Việc domain entity có thể chứa toàn bộ logic không có nghĩa là nó bắt buộc phải như vậy. Ví dụ, nếu cần kiểm tra xem tên người dùng đã tồn tại chưa, thì làm sao bạn làm điều đó bên trong một domain entity vốn “không được phép phụ thuộc” vào tầng truy cập dữ liệu? Người ta thường khuyên dùng thứ như “domain service”, nhưng như vậy logic nghiệp vụ lại bị rải ra nhiều nơi, nên tôi thấy nó đi ngược lại tinh thần của DDD.
      Tôi khá thích DDD như một triết lý, nhưng cực kỳ không thích các mẫu “DDD chiến thuật”. Tôi nghĩ có quá nhiều người đồng nhất Domain-Driven Design với Domain-Driven Implementation. Ở nơi phù hợp, tôi vẫn cố tạo ra domain phong phú, nhưng nó không phù hợp với mọi dự án và tôi cố tránh sa vào thuật ngữ. Tôi không quan tâm kiểu Name là value object hay aggregate root. Trên hết, bounded context mới quan trọng hơn. Tôi cũng thừa nhận rằng đôi khi DDD có thể làm tăng độ phức tạp của ứng dụng mà gần như không đổi lại được bao nhiêu. Tôi chắc chắn sẽ không gọi nó là lời giải vạn năng.
      Tôi vẫn sẽ tiếp tục dùng DDD, nhưng rất khó gạt đi cảm giác rằng DDD giống như một nỗ lực để nói rằng “nhìn này, lập trình hướng đối tượng cũng không tệ đến thế, đúng không?”. Và tôi cũng không chắc nó có thật sự đạt được mục tiêu đó hay không
    • Trong hàng chục năm, hiệu năng CPU, dung lượng bộ nhớ, không gian đĩa, tốc độ mạng, v.v. đã tăng trưởng theo cấp số nhân, khiến cái giá phải trả cho thiết kế tệ gần như biến mất. Vì thế những “code monkey” có thể gõ bàn phím và sản xuất ra code rác nhanh đến đâu thì phần lớn vẫn đều cho qua được
  • Trước khi bắt đầu làm kỹ thuật chuyên nghiệp, tôi đã phân tích dữ liệu và thống kê hằng ngày bằng các hệ thống thống kê như Matlab, R, Python thời kỳ đầu, nên tôi thấy điều này khá thú vị.
    Vì thế góc nhìn kỹ thuật của tôi từ trước đến nay luôn dựa trên hai thứ: quản lý trạng thái theo hướng hàm và quy trình dữ liệu.
    Sau 10 năm làm kỹ sư phần mềm như một nghề nghiệp, tôi nhận ra rằng phần lớn các kỹ sư “mang tính khoa học” như Minsky hay Shannon đều mô tả thế giới tính toán bằng việc quản lý trạng thái, biến đổi dữ liệu và quản lý chi phí tính toán. Các tên tuổi lớn và những người tiên phong của phần mềm đều xem dữ liệu và trạng thái là cực kỳ quan trọng; ở thời kỳ đầu của điện toán thì thực chất gần như chỉ có vậy, và người ta cũng kỳ vọng các mô thức đó sẽ tiếp tục kéo dài về sau.
    Ngược lại, trong thiết kế hệ thống kỹ thuật lại hoàn toàn không có sự nhất quán nào về những giả định nền tảng vốn đúng mọi lúc và được mọi người tuân theo; nếu có thì thường cũng chỉ gần với xu hướng nhất thời. Trong phần lớn phần mềm vận hành thực tế, lịch trình kinh doanh chi phối ưu tiên và cấu trúc kỹ thuật nhiều hơn rất nhiều so với độ bền vững, khả năng phản giòn hay quản lý trạng thái.
    Các tổ chức nghề nghiệp như nghiệp đoàn hay công đoàn gần như bị giới kỹ sư phần mềm từ chối một cách phổ biến. Vì chẳng có bất lợi gì nếu không xem trọng IEEE, nên trên thực tế cũng chẳng ai xem trọng nó. Kết quả là không có cơ chế cưỡng chế hành nghề hay tự điều tiết như trong xây dựng dân dụng hay kỹ thuật y sinh, mà ngay cả ở những ngành đó các cơ chế ấy cũng chỉ được sử dụng ở mức hạn chế.
    Nhìn chung, trạng thái hiện tại của phát triển phần mềm đã hoàn toàn tách rời khỏi cội rễ từng rất cao siêu và giàu tính triết học của nó, và trên thực tế đang bị dẫn dắt bởi các công ty ưu tiên những hệ thống giúp người có tiền kiếm ra tiền. Vì vậy, “điều tốt” gần như chẳng liên quan mấy đến những gì thực sự được khuyến khích

  • “Nếu bạn cho tôi xem lưu đồ [mã] và giấu đi các bảng [cấu trúc dữ liệu], tôi sẽ vẫn tiếp tục bối rối. Nếu bạn cho tôi xem các bảng, thường tôi sẽ không cần lưu đồ nữa. Vì chúng sẽ hiển nhiên.” — Fred Brooks

    • Có vẻ như câu trích dẫn này đã bỏ qua điểm rằng mô hình bền vững và cấu trúc dữ liệu thực tế có thể khác nhau, và có lẽ nên khác nhau.
      Việc khớp 1:1 với các bảng nền tảng là cực kỳ gò bó, và theo tôi sẽ dẫn tới những mô hình bỏ lỡ khả năng biểu đạt mà các ngôn ngữ hiện đại cung cấp
  • Về bản chất, đây là góc nhìn của lập trình hàm và lý thuyết phạm trù.
    Có một đối tượng dữ liệu, và cấu trúc của nó đưa ra các ràng buộc về cách nó có thể được biến đổi. Khi đó toàn bộ logic chương trình trở thành việc xử lý các phép biến đổi bảo toàn cấu trúc đó.
    Các phép biến đổi trở nên đơn giản hơn và dễ suy luận hơn, cuối cùng còn lại một đồ thị trong đó phép biến đổi là cạnh và cấu trúc là nút. Nói chung dễ suy luận hơn một chương trình mệnh lệnh tùy ý

    • Đó không phải là góc nhìn của lập trình hàm hay lý thuyết phạm trù. Đó là góc nhìn của mọi triết lý ngôn ngữ; những người ưa thích hướng đối tượng hay thủ tục cũng sẽ lập luận y như vậy. Việc định nghĩa đúng kiểu dữ liệu là quan trọng và áp dụng cho mọi ngôn ngữ lẫn mọi mô hình.
      Góc nhìn của lập trình hàm gần hơn với việc không nên biến đổi đối tượng và nên tránh thay đổi trạng thái, và đó là chuyện khác với thảo luận này. Cốt lõi của lý thuyết phạm trù là xử lý các mẫu quan hệ xuất hiện chung trong nhiều lĩnh vực toán học, và hoàn toàn không liên quan tới nội dung đang bàn ở đây. Có lẽ ý họ là lý thuyết kiểu, nhưng cái đó cũng không liên quan
  • Đây là kết luận tôi đã rút ra từ lâu. Mọi thứ chúng ta làm trong mã đều có khả năng tồn tại ngắn hơn rất nhiều so với một quyết định đúng đắn về dữ liệu.
    https://www.swyx.io/data-outlasts-code-but

    • Những quyết định tốt thì không nhìn thấy được. Chỉ những quyết định tệ mới có vẻ như sống mãi
  • Nguyên tắc này cũng áp dụng ở cấp độ kinh doanh. Tôi vẫn thường xuyên phải làm việc với các nhà phân tích nghiệp vụ cứ chăm chăm vào quy trình (mã) mà không dành thời gian để hiểu trước các thực thể và mối quan hệ của chúng (dữ liệu).
    Kết quả là khi đến lúc phải xây dựng thứ gì đó, họ không thể trao đổi với lập trình viên về việc mô hình dữ liệu nên trông như thế nào. Quy trình thì được triển khai, còn mô hình dữ liệu thì bị chắp vá tùy hứng thay vì được thiết kế cẩn thận