Tất cả mọi thứ là một bản đồ, tôi đang làm điều này phải không?


69

Tôi đã xem bài nói " Suy nghĩ về dữ liệu " của Stuart Sierra và lấy một trong những ý tưởng từ đó làm nguyên tắc thiết kế trong trò chơi này. Sự khác biệt là anh ấy làm việc ở Clojure và tôi đang làm việc với JavaScript. Tôi thấy một số khác biệt chính giữa các ngôn ngữ của chúng tôi ở chỗ:

  • Clojure là lập trình chức năng thành ngữ
  • Hầu hết nhà nước là bất biến

Tôi lấy ý tưởng từ slide "Mọi thứ là bản đồ" (Từ 11 phút, 6 giây đến> 29 phút). Một số điều ông nói là:

  1. Bất cứ khi nào bạn thấy một hàm có 2-3 đối số, bạn có thể tạo một trường hợp để biến nó thành bản đồ và chỉ cần truyền bản đồ vào. Có rất nhiều lợi thế cho điều đó:
    1. Bạn không phải lo lắng về thứ tự tranh luận
    2. Bạn không phải lo lắng về bất kỳ thông tin bổ sung. Nếu có thêm chìa khóa, đó không thực sự là mối quan tâm của chúng tôi. Họ chỉ chảy qua, họ không can thiệp.
    3. Bạn không phải xác định một lược đồ
  2. Trái ngược với việc truyền vào Object không có dữ liệu nào che giấu. Tuy nhiên, anh ta đưa ra trường hợp rằng việc ẩn dữ liệu có thể gây ra vấn đề và bị đánh giá quá cao:
    1. Hiệu suất
    2. Dễ thực hiện
    3. Ngay khi bạn liên lạc qua mạng hoặc qua các quy trình, bạn phải có cả hai bên đồng ý về việc trình bày dữ liệu. Đó là công việc làm thêm bạn có thể bỏ qua nếu bạn chỉ làm việc trên dữ liệu.
  3. Liên quan nhất đến câu hỏi của tôi. Đây là 29 phút trong: "Làm cho các chức năng của bạn có thể ghép lại được". Đây là mẫu mã anh ta sử dụng để giải thích khái niệm:

    ;; Bad
    (defn complex-process []
      (let [a (get-component @global-state)
            b (subprocess-one a) 
            c (subprocess-two a b)
            d (subprocess-three a b c)]
        (reset! global-state d)))
    
    ;; Good
    (defn complex-process [state]
      (-> state
        subprocess-one
        subprocess-two
        subprocess-three))
    

    Tôi hiểu phần lớn các lập trình viên không quen thuộc với Clojure, vì vậy tôi sẽ viết lại điều này theo phong cách bắt buộc:

    ;; Good
    def complex-process(State state)
      state = subprocess-one(state)
      state = subprocess-two(state)
      state = subprocess-three(state)
      return state
    

    Dưới đây là những ưu điểm:

    1. Dễ kiểm tra
    2. Dễ dàng nhìn vào các chức năng đó trong sự cô lập
    3. Dễ dàng nhận xét một dòng về điều này và xem kết quả là gì bằng cách loại bỏ một bước duy nhất
    4. Mỗi quy trình con có thể thêm thông tin vào trạng thái. Nếu một quy trình con cần truyền đạt một cái gì đó đến quy trình ba, thì đơn giản như thêm khóa / giá trị.
    5. Không có bản tóm tắt để trích xuất dữ liệu bạn cần ra khỏi trạng thái chỉ để bạn có thể lưu lại. Chỉ cần chuyển qua toàn bộ trạng thái và để quy trình con chỉ định những gì nó cần.

Bây giờ, trở lại tình huống của tôi: tôi đã học bài học này và áp dụng nó vào trò chơi của mình. Đó là, gần như tất cả các hàm cấp cao của tôi lấy và trả về một gameStateđối tượng. Đối tượng này chứa tất cả dữ liệu của trò chơi. EG: Danh sách badGuys, danh sách các menu, loot trên mặt đất, v.v ... Đây là một ví dụ về chức năng cập nhật của tôi:

update(gameState)
  ...
  gameState = handleUnitCollision(gameState)
  ...
  gameState = handleLoot(gameState)
  ...

Điều tôi ở đây để hỏi là, tôi đã tạo ra một số gớm ghiếc làm sai lệch một ý tưởng chỉ thực tế trong ngôn ngữ lập trình chức năng chưa? JavaScript không có chức năng thành ngữ (mặc dù nó có thể được viết theo cách đó) và thực sự rất khó khăn để viết các cấu trúc dữ liệu bất biến. Một điều khiến tôi lo lắng là anh ta giả định rằng mỗi quy trình con đó là thuần túy. Tại sao giả định đó cần phải được thực hiện? Thật hiếm khi bất kỳ chức năng nào của tôi là thuần túy (điều đó, ý tôi là chúng thường sửa đổi gameState. Tôi không có bất kỳ tác dụng phụ phức tạp nào khác ngoài điều đó). Những ý tưởng này có sụp đổ nếu bạn không có dữ liệu bất biến?

Tôi lo lắng rằng một ngày nào đó tôi sẽ thức dậy và nhận ra toàn bộ thiết kế này là một sự giả tạo và tôi thực sự vừa mới thực hiện mô hình chống bóng Big Ball Of Mud .


Thành thật mà nói, tôi đã làm việc với mã này trong nhiều tháng và nó thật tuyệt. Tôi cảm thấy như mình đang nhận được tất cả những lợi thế mà anh ấy đã tuyên bố. Mã của tôi là siêu dễ dàng cho tôi lý do về. Nhưng tôi là một đội một người nên tôi có lời nguyền về kiến ​​thức.

Cập nhật

Tôi đã mã hóa hơn 6 tháng với mẫu này. Thông thường đến lúc này tôi quên những gì tôi đã làm và đó là nơi "tôi đã viết điều này một cách sạch sẽ?" vào trong chơi. Nếu tôi không, tôi thực sự đấu tranh. Cho đến nay, tôi không phải vật lộn chút nào.

Tôi hiểu làm thế nào một bộ mắt khác sẽ cần thiết để xác nhận khả năng duy trì của nó. Tất cả những gì tôi có thể nói là tôi quan tâm đến khả năng bảo trì trước hết. Tôi luôn là người truyền giáo lớn nhất cho mã sạch cho dù tôi làm việc ở đâu.

Tôi muốn trả lời trực tiếp cho những người đã có trải nghiệm cá nhân tồi tệ với cách mã hóa này. Lúc đó tôi không biết, nhưng tôi nghĩ chúng ta thực sự đang nói về hai cách viết mã khác nhau. Cách tôi đã làm dường như có cấu trúc hơn những gì người khác đã trải qua. Khi ai đó có trải nghiệm cá nhân tồi tệ với "Mọi thứ là bản đồ", họ nói về việc duy trì nó khó đến mức nào bởi vì:

  1. Bạn không bao giờ biết cấu trúc của bản đồ mà hàm yêu cầu
  2. Bất kỳ chức năng nào cũng có thể làm thay đổi đầu vào theo những cách bạn không bao giờ mong đợi. Bạn phải xem xét tất cả các cơ sở mã để tìm hiểu làm thế nào một khóa cụ thể đã vào bản đồ hoặc tại sao nó biến mất.

Đối với những người có trải nghiệm như vậy, có lẽ cơ sở mã là "Mọi thứ cần 1 trong N loại bản đồ". Của tôi là, "Mọi thứ cần 1 trên 1 loại bản đồ". Nếu bạn biết cấu trúc của 1 loại đó, bạn sẽ biết cấu trúc của mọi thứ. Tất nhiên, cấu trúc đó thường phát triển theo thời gian. Đó là lý do ...

Có một nơi để tìm kiếm triển khai tham chiếu (ví dụ: lược đồ). Việc triển khai tham chiếu này là mã mà trò chơi sử dụng để nó không bị lỗi thời.

Đối với điểm thứ hai, tôi không thêm / xóa các khóa vào bản đồ bên ngoài triển khai tham chiếu, tôi chỉ thay đổi những gì đã có. Tôi cũng có một bộ lớn các bài kiểm tra tự động.

Nếu kiến ​​trúc này cuối cùng sụp đổ dưới sức nặng của chính nó, tôi sẽ thêm một bản cập nhật thứ hai. Nếu không, giả sử mọi thứ đang diễn ra tốt đẹp :)


2
Câu hỏi thú vị (+1)! Tôi thấy nó là một bài tập rất hữu ích để thử và thực hiện các thành ngữ chức năng trong một ngôn ngữ không chức năng (hoặc không có chức năng rất mạnh).
Giorgio

15
Bất cứ ai sẽ nói với bạn rằng việc ẩn thông tin theo kiểu OO (với các thuộc tính và chức năng truy cập) là một điều tồi tệ vì hiệu năng (thường không đáng kể), và sau đó bảo bạn biến tất cả các tham số của bạn thành bản đồ, cung cấp cho bạn chi phí (lớn hơn nhiều) của tra cứu băm mỗi khi bạn cố gắng truy xuất giá trị, có thể được bỏ qua một cách an toàn.
Mason Wheeler

4
@MasonWheeler cho phép bạn nói đúng về điều này. Bạn sẽ làm mất hiệu lực mọi điểm khác anh ấy làm vì điều này là sai?
Daniel Kaplan

9
Trong Python (và tôi tin rằng hầu hết các ngôn ngữ động, bao gồm Javascript), đối tượng thực sự chỉ là cú pháp đường cho một dict / map.
Nói dối Ryan

6
@EvanPlaice: Ký hiệu Big-O có thể bị đánh lừa. Một thực tế đơn giản là, mọi thứ đều chậm so với truy cập trực tiếp với hai hoặc ba hướng dẫn mã máy riêng lẻ và trên một điều gì đó xảy ra thường xuyên như một cuộc gọi chức năng, chi phí đó sẽ tăng lên rất nhanh.
Mason Wheeler

Câu trả lời:


42

Trước đây tôi đã hỗ trợ một ứng dụng, "mọi thứ đều là bản đồ". Đó là một ý tưởng khủng khiếp. Xin đừng làm thế

Khi bạn chỉ định các đối số được truyền cho hàm, điều đó giúp bạn dễ dàng biết được giá trị nào mà hàm cần. Nó tránh truyền dữ liệu không liên quan đến hàm chỉ làm mất tập trung lập trình viên - mọi giá trị được truyền đều ngụ ý rằng nó cần thiết và điều đó khiến lập trình viên hỗ trợ mã của bạn phải tìm ra lý do cần dữ liệu.

Mặt khác, nếu bạn vượt qua mọi thứ dưới dạng bản đồ, lập trình viên hỗ trợ ứng dụng của bạn sẽ phải hiểu đầy đủ chức năng được gọi theo mọi cách để biết giá trị nào mà bản đồ cần có. Tệ hơn nữa, việc sử dụng lại bản đồ được truyền cho chức năng hiện tại là rất hấp dẫn để truyền dữ liệu cho các chức năng tiếp theo. Điều này có nghĩa là lập trình viên hỗ trợ ứng dụng của bạn cần biết tất cả các chức năng được gọi bởi chức năng hiện tại để hiểu chức năng hiện tại làm gì. Điều đó hoàn toàn ngược lại với mục đích viết các hàm - trừu tượng hóa các vấn đề để bạn không phải suy nghĩ về chúng! Bây giờ hãy tưởng tượng 5 cuộc gọi sâu và 5 cuộc gọi mỗi cuộc gọi rộng. Đó là một địa ngục của rất nhiều thứ để giữ trong tâm trí của bạn và một địa ngục của rất nhiều sai lầm để làm.

"Mọi thứ đều là bản đồ" dường như cũng dẫn đến việc sử dụng bản đồ làm giá trị trả về. Tôi đa nhin thây no. Và, một lần nữa, đó là một nỗi đau. Các hàm được gọi cần không bao giờ ghi đè lên giá trị trả về của nhau - trừ khi bạn biết chức năng của mọi thứ và biết rằng giá trị bản đồ đầu vào X cần được thay thế cho lệnh gọi hàm tiếp theo. Và hàm hiện tại cần sửa đổi bản đồ để trả về giá trị của nó, đôi khi phải ghi đè lên giá trị trước đó và đôi khi phải không.

chỉnh sửa - ví dụ

Đây là một ví dụ về vấn đề này. Đây là một ứng dụng web. Đầu vào của người dùng được chấp nhận từ lớp UI và được đặt trong bản đồ. Sau đó, các chức năng đã được gọi để xử lý yêu cầu. Bộ chức năng đầu tiên sẽ kiểm tra đầu vào sai. Nếu có lỗi, thông báo lỗi sẽ được đưa vào bản đồ. Hàm gọi sẽ kiểm tra bản đồ cho mục này và ghi giá trị vào ui nếu nó tồn tại.

Bộ chức năng tiếp theo sẽ bắt đầu logic kinh doanh. Mỗi chức năng sẽ lấy bản đồ, xóa một số dữ liệu, sửa đổi một số dữ liệu, vận hành trên dữ liệu trên bản đồ và đưa kết quả vào bản đồ, v.v. Các chức năng tiếp theo sẽ mong đợi kết quả từ các chức năng trước trên bản đồ. Để sửa lỗi trong một chức năng tiếp theo, bạn phải điều tra tất cả các chức năng trước đó cũng như người gọi để xác định mọi nơi giá trị mong đợi có thể đã được đặt.

Các chức năng tiếp theo sẽ kéo dữ liệu từ cơ sở dữ liệu. Hoặc, đúng hơn, họ sẽ chuyển bản đồ đến lớp truy cập dữ liệu. DAL sẽ kiểm tra xem bản đồ có chứa các giá trị nhất định để kiểm soát cách thực hiện truy vấn không. Nếu 'justcount' là một khóa, thì truy vấn sẽ là 'đếm chọn foo từ thanh'. Bất kỳ chức năng nào được gọi trước đây có thể có chức năng đã thêm 'chỉ số' vào bản đồ. Các kết quả truy vấn sẽ được thêm vào cùng một bản đồ.

Các kết quả sẽ nổi lên với người gọi (logic nghiệp vụ) sẽ kiểm tra bản đồ để làm gì. Một số điều này sẽ đến từ những thứ được thêm vào bản đồ bởi logic kinh doanh ban đầu. Một số sẽ đến từ dữ liệu từ cơ sở dữ liệu. Cách duy nhất để biết nó đến từ đâu là tìm mã đã thêm nó. Và các vị trí khác cũng có thể thêm nó.

Mã thực sự là một mớ hỗn độn nguyên khối, mà bạn phải hiểu toàn bộ để biết một mục duy nhất trong bản đồ đến từ đâu.


2
Đoạn thứ hai của bạn có ý nghĩa với tôi và điều đó thực sự nghe có vẻ tệ. Tôi đang hiểu ý từ đoạn thứ ba của bạn rằng chúng tôi không thực sự nói về cùng một thiết kế. "tái sử dụng" là điểm chính. Sẽ là sai lầm khi tránh nó. Và tôi thực sự không thể liên quan đến đoạn cuối cùng của bạn. Tôi có mọi chức năng đảm nhận gameStatemà không biết gì về những gì đã xảy ra trước hoặc sau nó. Nó chỉ đơn giản là phản ứng với dữ liệu mà nó đưa ra. Làm thế nào bạn có thể rơi vào tình huống các chức năng sẽ dẫm lên các ngón chân của nhau? Bạn có thể đưa ra một ví dụ không?
Daniel Kaplan

2
Tôi đã thêm một ví dụ để thử và làm cho nó rõ ràng hơn một chút. Hy vọng nó giúp. Ngoài ra, có một sự khác biệt giữa đi ngang qua một đối tượng trạng thái được xác định rõ so với đi qua xung quanh một blob rằng bộ thay đổi bởi nhiều nơi vì nhiều lý do, trộn Logic ui, logic kinh doanh và truy cập cơ sở dữ liệu luận lý
ATK

28

Cá nhân, tôi sẽ không đề xuất mô hình đó trong cả hai mô hình. Nó làm cho nó dễ dàng hơn để viết ban đầu với chi phí làm cho nó khó khăn hơn để lý do về sau.

Ví dụ: cố gắng trả lời các câu hỏi sau về từng chức năng của quy trình con:

  • Những lĩnh vực statenào nó yêu cầu?
  • Những lĩnh vực nào nó sửa đổi?
  • Những lĩnh vực không thay đổi?
  • Bạn có thể sắp xếp lại một cách an toàn thứ tự của các chức năng?

Với mẫu này, bạn không thể trả lời những câu hỏi đó mà không đọc toàn bộ chức năng.

Trong một ngôn ngữ hướng đối tượng, mô hình thậm chí còn ít ý nghĩa hơn, bởi vì trạng thái theo dõi là những gì đối tượng làm.


2
"Những lợi ích của sự bất biến đi xuống càng lớn đối tượng bất biến của bạn càng lớn" Tại sao? Đó là một nhận xét về hiệu suất hoặc khả năng bảo trì? Xin hãy giải thích về câu đó.
Daniel Kaplan

8
@tieTYT Bất biến hoạt động tốt khi có một cái gì đó nhỏ (ví dụ một loại số). Bạn có thể sao chép chúng, tạo chúng, loại bỏ chúng, cân chúng với chi phí khá thấp. Khi bạn bắt đầu xử lý toàn bộ trạng thái trò chơi được tạo thành từ các bản đồ sâu, lớn, cây, danh sách và hàng chục nếu không phải là hàng trăm biến, chi phí để sao chép hoặc xóa nó sẽ tăng lên (và flyweights trở nên không thực tế).

3
Tôi hiểu rồi. Đó có phải là vấn đề "dữ liệu bất biến trong ngôn ngữ mệnh lệnh" hay vấn đề "dữ liệu bất biến"? IE: Có lẽ đây không phải là vấn đề trong mã Clojure. Nhưng tôi có thể thấy nó như thế nào trong JS. Nó cũng là một nỗi đau để viết tất cả các mã soạn sẵn để làm điều đó.
Daniel Kaplan

3
@MichaelT và Karl: để công bằng, bạn thực sự nên đề cập đến khía cạnh khác của câu chuyện bất biến / hiệu quả. Vâng, sử dụng ngây thơ có thể không hiệu quả khủng khiếp, đó là lý do tại sao mọi người đã đưa ra các phương pháp tốt hơn. Xem công việc của Chris Okasaki để biết thêm thông tin.

3
@MattFenwick Cá nhân tôi khá thích bất biến. Khi làm việc với luồng tôi biết những điều về bất biến và có thể làm việc và sao chép chúng một cách an toàn. Tôi đặt nó trong một cuộc gọi tham số và chuyển nó sang một cuộc gọi khác mà không lo lắng rằng ai đó sẽ sửa đổi nó khi nó trở lại với tôi. Nếu người ta đang nói về một trạng thái trò chơi phức tạp (câu hỏi được sử dụng như một ví dụ - tôi sẽ kinh hoàng khi nghĩ về một thứ gì đó "đơn giản" như trạng thái trò chơi không thể thay đổi), thì bất biến có lẽ là cách tiếp cận sai.

12

Những gì bạn dường như đang làm là, một cách hiệu quả, một đơn vị Nhà nước thủ công; những gì tôi sẽ làm là xây dựng một tổ hợp liên kết (đơn giản hóa) và thể hiện lại các kết nối giữa các bước logic của bạn bằng cách sử dụng:

function stateBind() {
    var computation = function (state) { return state; };
    for ( var i = 0 ; i < arguments.length ; i++ ) {
        var oldComp = computation;
        var newComp = arguments[i];
        computation = function (state) { return newComp(oldComp(state)); };
    }
    return computation;
}

...

stateBind(
  subprocessOne,
  subprocessTwo,
  subprocessThree,
);

Bạn thậm chí có thể sử dụng stateBindđể xây dựng các quy trình con khác nhau từ các quy trình con và tiếp tục xuống một cây tổ hợp liên kết để cấu trúc tính toán của bạn một cách thích hợp.

Để biết giải thích về đơn nguyên Nhà nước đầy đủ, chưa được xác định và giới thiệu tuyệt vời về các đơn nguyên nói chung trong JavaScript, hãy xem bài đăng trên blog này .


1
OK, tôi sẽ xem xét điều đó (và bình luận về nó sau). Nhưng bạn nghĩ gì về ý tưởng sử dụng mô hình?
Daniel Kaplan

1
@tieTYT Tôi nghĩ rằng bản thân mô hình là một ý tưởng rất tốt; Nhà nước nói chung là một công cụ cấu trúc mã hữu ích cho các thuật toán giả đột biến (thuật toán không thay đổi nhưng mô phỏng khả năng biến đổi).
Ngọn lửa của Ptharien

2
+1 để lưu ý rằng mẫu này thực chất là một Monad. Tuy nhiên, tôi không đồng ý rằng đó là một ý tưởng tốt trong một ngôn ngữ thực sự có khả năng biến đổi. Monad là một cách để cung cấp khả năng của toàn cầu / trạng thái có thể thay đổi trong một ngôn ngữ không cho phép đột biến. IMO, trong một ngôn ngữ không thực thi sự bất biến, mô hình Monad chỉ là thủ dâm tinh thần.
Lie Ryan

6
@LieRyan Monads nói chung thực sự không liên quan gì đến tính đột biến hoặc toàn cầu; chỉ có đơn vị Nhà nước đặc biệt làm (vì đó là những gì cụ thể được thiết kế để làm). Tôi cũng không đồng ý rằng đơn nguyên Nhà nước không hữu ích trong ngôn ngữ có khả năng biến đổi, mặc dù việc triển khai dựa trên tính biến đổi bên dưới có thể hiệu quả hơn so với ngôn ngữ bất biến mà tôi đã đưa ra (mặc dù tôi không chắc lắm về điều đó). Giao diện đơn hình có thể cung cấp các khả năng cấp cao mà không dễ truy cập, bộ stateBindkết hợp tôi đưa ra là một ví dụ rất đơn giản về điều đó.
Ngọn lửa của Ptharien

1
@LieRyan Tôi nhận xét thứ hai của Ptharien - hầu hết các đơn vị không phải là về trạng thái hoặc tính đột biến, và thậm chí là một trong đó, đặc biệt không phải là về trạng thái toàn cầu . Monads thực sự hoạt động khá tốt trong các ngôn ngữ OO / mệnh lệnh / đột biến.

11

Vì vậy, dường như có rất nhiều cuộc thảo luận giữa hiệu quả của phương pháp này trong Clojure. Tôi nghĩ rằng có thể hữu ích khi xem xét triết lý của Rich Hickey về lý do tại sao ông tạo ra Clojure để hỗ trợ trừu tượng hóa dữ liệu theo cách này :

Fogus: Vì vậy, một khi sự phức tạp ngẫu nhiên đã được giảm bớt, làm thế nào Clojure có thể giúp giải quyết vấn đề trong tầm tay? Ví dụ, mô hình hướng đối tượng được lý tưởng hóa có nghĩa là để thúc đẩy việc tái sử dụng, nhưng Clojure không phải là đối tượng hướng đối tượng cổ điển, làm thế nào chúng ta có thể cấu trúc mã của mình để sử dụng lại?

Hickey: Tôi sẽ tranh luận về OO và tái sử dụng, nhưng chắc chắn, việc có thể tái sử dụng mọi thứ khiến vấn đề trở nên đơn giản hơn, vì bạn không phát minh lại bánh xe thay vì chế tạo ô tô. Và Clojure đang ở trên JVM làm cho rất nhiều bánh xe thư viện của Google có sẵn. Điều gì làm cho một thư viện có thể tái sử dụng? Nó nên làm một hoặc một vài điều tốt, tương đối tự túc và thực hiện một số yêu cầu đối với mã máy khách. Không có cái nào rơi ra khỏi OO và không phải tất cả các thư viện Java đều đáp ứng tiêu chí này, nhưng nhiều cái thì có.

Khi chúng tôi giảm xuống mức thuật toán, tôi nghĩ rằng OO có thể ngăn chặn nghiêm trọng việc tái sử dụng. Cụ thể, việc sử dụng các đối tượng để biểu thị dữ liệu thông tin đơn giản gần như là tội phạm trong việc tạo ra các ngôn ngữ vi mô thông tin mỗi phần, tức là các phương thức lớp, so với các phương thức khai báo và khai báo mạnh mẽ hơn nhiều như đại số quan hệ. Phát minh ra một lớp với giao diện riêng để chứa một phần thông tin cũng giống như phát minh ra một ngôn ngữ mới để viết mọi câu chuyện ngắn. Điều này là chống tái sử dụng, và, tôi nghĩ, dẫn đến sự bùng nổ mã trong các ứng dụng OO điển hình. Clojure tránh điều này và thay vào đó ủng hộ một mô hình kết hợp đơn giản cho thông tin. Với nó, người ta có thể viết các thuật toán có thể được sử dụng lại trên các loại thông tin.

Mô hình kết hợp này là một trong một số trừu tượng được cung cấp với Clojure và đây là những nền tảng thực sự của cách tiếp cận để sử dụng lại: các chức năng về trừu tượng hóa. Có một tập hợp các hàm mở và lớn, hoạt động dựa trên một tập hợp trừu tượng mở và nhỏ, là chìa khóa để tái sử dụng thuật toán và khả năng tương tác của thư viện. Phần lớn các hàm Clojure được định nghĩa theo các tóm tắt này và các tác giả thư viện cũng thiết kế các định dạng đầu vào và đầu ra của chúng theo cách của chúng, nhận ra khả năng tương tác rất lớn giữa các thư viện được phát triển độc lập. Điều này trái ngược hoàn toàn với DOM và những thứ khác mà bạn thấy trong OO. Tất nhiên, bạn có thể thực hiện trừu tượng tương tự trong OO với các giao diện, ví dụ, các bộ sưu tập java.util, nhưng bạn có thể dễ dàng không, như trong java.io.

Fogus nhắc lại những điểm này trong cuốn sách Javascript chức năng của mình :

Trong suốt cuốn sách này, tôi sẽ sử dụng cách tiếp cận sử dụng các loại dữ liệu tối thiểu để thể hiện sự trừu tượng, từ bộ đến cây đến bảng. Tuy nhiên, trong JavaScript, mặc dù các loại đối tượng của nó cực kỳ mạnh mẽ, các công cụ được cung cấp để làm việc với chúng không hoàn toàn hoạt động. Thay vào đó, mẫu sử dụng lớn hơn được liên kết với các đối tượng JavaScript là đính kèm các phương thức cho mục đích gửi đa hình. Rất may, bạn cũng có thể xem một đối tượng JavaScript chưa được đặt tên (không được xây dựng thông qua hàm tạo) như một kho lưu trữ dữ liệu kết hợp.

Nếu các hoạt động duy nhất mà chúng tôi có thể thực hiện trên một đối tượng Sách hoặc một thể hiện của loại Nhân viên là setTitle hoặc getSSN, thì chúng tôi đã khóa dữ liệu của chúng tôi thành các ngôn ngữ vi mô thông tin mỗi phần (Hickey 2011). Một cách tiếp cận linh hoạt hơn để mô hình hóa dữ liệu là một kỹ thuật dữ liệu kết hợp. Các đối tượng JavaScript, thậm chí trừ máy móc nguyên mẫu, là phương tiện lý tưởng để mô hình hóa dữ liệu liên kết, trong đó các giá trị được đặt tên có thể được cấu trúc để tạo thành các mô hình dữ liệu cấp cao hơn, được truy cập theo cách thống nhất.

Mặc dù các công cụ để thao tác và truy cập các đối tượng JavaScript dưới dạng bản đồ dữ liệu rất ít trong chính JavaScript, nhưng rất may Underscore cung cấp một loạt các hoạt động hữu ích. Trong số các hàm đơn giản nhất để nắm bắt là _.keys, _.values ​​và _.pluck. Cả _.key và _.values ​​đều được đặt tên theo chức năng của chúng, đó là lấy một đối tượng và trả về một mảng các khóa hoặc giá trị của nó ...


2
Tôi đã đọc bài phỏng vấn Fogus / Hickey này trước đây, nhưng tôi không có khả năng hiểu những gì anh ấy nói về cho đến bây giờ. Cảm ơn câu trả lời của bạn. Vẫn không chắc chắn nếu Hickey / Fogus sẽ ban cho thiết kế của tôi lời chúc phúc. Tôi lo lắng tôi đã lấy tinh thần của lời khuyên của họ đến cùng cực.
Daniel Kaplan

9

Advocate của Devil

Tôi nghĩ rằng câu hỏi này xứng đáng với sự ủng hộ của một ác quỷ (nhưng tất nhiên tôi thiên vị). Tôi nghĩ rằng @KarlBielefeldt đang tạo ra những điểm rất tốt và tôi muốn giải quyết chúng. Đầu tiên tôi muốn nói rằng điểm của anh ấy rất tuyệt.

Vì anh ấy đã đề cập đây không phải là một mô hình tốt ngay cả trong lập trình chức năng, tôi sẽ xem xét JavaScript và / hoặc Clojure trong các câu trả lời của tôi. Một điểm tương đồng cực kỳ quan trọng giữa hai ngôn ngữ này là chúng được gõ động. Tôi sẽ đồng ý hơn với quan điểm của anh ấy nếu tôi đang thực hiện điều này bằng một ngôn ngữ được gõ tĩnh như Java hoặc Haskell. Nhưng, tôi sẽ xem xét phương án thay thế cho mẫu "Mọi thứ là bản đồ" là một thiết kế OOP truyền thống trong JavaScript và không phải là một ngôn ngữ được gõ tĩnh (tôi hy vọng tôi không thiết lập một đối số rơm bằng cách làm điều này, làm ơn cho tôi biết).

Ví dụ: cố gắng trả lời các câu hỏi sau về từng chức năng của quy trình con:

  • Những lĩnh vực nhà nước nào nó yêu cầu?

  • Những lĩnh vực nào nó sửa đổi?

  • Những lĩnh vực không thay đổi?

Trong một ngôn ngữ được gõ động, bạn thường trả lời những câu hỏi này như thế nào? Tham số đầu tiên của hàm có thể được đặt tên foo, nhưng đó là gì? Một mảng? Một đối tượng? Một đối tượng của mảng các đối tượng? Làm thế nào để bạn tìm ra? Cách duy nhất tôi biết là

  1. đọc tài liệu
  2. nhìn vào cơ thể chức năng
  3. nhìn vào các bài kiểm tra
  4. đoán và chạy chương trình để xem nó hoạt động.

Tôi không nghĩ mẫu "Mọi thứ là bản đồ" tạo nên sự khác biệt ở đây. Đây vẫn là những cách duy nhất tôi biết để trả lời những câu hỏi này.

Ngoài ra, hãy nhớ rằng trong JavaScript và hầu hết các ngôn ngữ lập trình bắt buộc, mọi ngôn ngữ functionđều có thể yêu cầu, sửa đổi và bỏ qua bất kỳ trạng thái nào nó có thể truy cập và chữ ký không có sự khác biệt: Hàm / phương thức có thể làm gì đó với trạng thái toàn cầu hoặc với một đơn vị. Chữ ký thường nói dối.

Tôi không cố thiết lập sự phân đôi giả giữa "Mọi thứ là bản đồ" và mã OO được thiết kế kém . Tôi chỉ đang cố gắng chỉ ra rằng việc có các chữ ký có các tham số hạt nhỏ hơn / mịn hơn không đảm bảo bạn biết cách cô lập, thiết lập và gọi một hàm.

Nhưng, nếu bạn cho phép tôi sử dụng sự phân đôi sai lầm đó: So với việc viết JavaScript theo cách OOP truyền thống, "Mọi thứ là bản đồ" có vẻ tốt hơn. Theo cách OOP truyền thống, hàm có thể yêu cầu, sửa đổi hoặc bỏ qua trạng thái mà bạn chuyển vào hoặc trạng thái mà bạn không vượt qua. Với mẫu "Mọi thứ là bản đồ" này, bạn chỉ yêu cầu, sửa đổi hoặc bỏ qua trạng thái mà bạn vượt qua trong.

  • Bạn có thể sắp xếp lại một cách an toàn thứ tự của các chức năng?

Trong mã của tôi, có. Xem bình luận thứ hai của tôi để trả lời của @ Evicatos. Có lẽ điều này chỉ bởi vì tôi đang làm một trò chơi, tôi không thể nói. Trong một trò chơi cập nhật 60 lần một giây, điều đó thực sự không quan trọng nếu dead guys drop lootsau đó good guys pick up loothoặc ngược lại. Mỗi chức năng vẫn thực hiện chính xác những gì nó phải làm bất kể thứ tự chúng đang chạy. Dữ liệu tương tự sẽ được đưa vào chúng tại các updatecuộc gọi khác nhau nếu bạn trao đổi thứ tự. Nếu bạn có good guys pick up lootsau đó dead guys drop loot, những người tốt sẽ nhận được các chiến lợi phẩm tiếp theo updatevà nó không phải là vấn đề lớn. Một con người sẽ không thể nhận thấy sự khác biệt.

Ít nhất đây là kinh nghiệm chung của tôi. Tôi cảm thấy thực sự dễ bị tổn thương thừa nhận điều này công khai. Có lẽ xem xét điều này là ổn là một điều rất , rất xấu để làm. Hãy cho tôi biết nếu tôi đã phạm một số sai lầm khủng khiếp ở đây. Nhưng, nếu tôi có, nó rất dễ dàng để sắp xếp lại các chức năng do đó thứ tự là dead guys drop lootsau đó good guys pick up lootmột lần nữa. Sẽ mất ít thời gian hơn thời gian để viết đoạn này: P

Có thể bạn nghĩ rằng "những kẻ chết nên bỏ loot trước. Sẽ tốt hơn nếu mã của bạn thi hành lệnh đó". Nhưng, tại sao kẻ thù phải thả loot trước khi bạn có thể nhặt được đồ? Đối với tôi điều đó không có ý nghĩa. Có lẽ các loot đã bị rơi 100 updatestrước. Không cần thiết phải kiểm tra xem một kẻ xấu tùy tiện có phải nhặt đồ cướp đã ở trên mặt đất hay không. Đó là lý do tại sao tôi nghĩ rằng thứ tự của các hoạt động này là hoàn toàn tùy ý.

Việc viết các bước tách rời với mẫu này là điều tự nhiên, nhưng thật khó để nhận thấy các bước được ghép nối của bạn trong OOP truyền thống. Nếu tôi đang viết OOP truyền thống, cách suy nghĩ tự nhiên, ngây thơ là biến sự dead guys drop loottrở lại thành một Lootđối tượng mà tôi phải truyền vào good guys pick up loot. Tôi sẽ không thể sắp xếp lại các hoạt động đó vì lần đầu tiên trả về đầu vào của giây.

Trong một ngôn ngữ hướng đối tượng, mô hình thậm chí còn ít ý nghĩa hơn, bởi vì trạng thái theo dõi là những gì đối tượng làm.

Các đối tượng có trạng thái và nó là thành ngữ để biến đổi trạng thái làm cho lịch sử của nó biến mất ... trừ khi bạn tự viết mã để theo dõi nó. Theo cách nào là theo dõi trạng thái "những gì họ làm"?

Ngoài ra, lợi ích của sự bất biến đi xuống càng lớn đối tượng bất biến của bạn nhận được.

Đúng như tôi đã nói, "Thật hiếm khi bất kỳ chức năng nào của tôi là thuần túy". Họ luôn chỉ hoạt động trên các tham số của họ, nhưng họ đột biến các tham số của họ. Đây là một sự thỏa hiệp mà tôi cảm thấy tôi phải thực hiện khi áp dụng mô hình này vào JavaScript.


4
"Tham số đầu tiên của hàm có thể được đặt tên là foo, nhưng đó là gì?" Đó là lý do tại sao bạn không đặt tên cho tham số của mình là "foo", nhưng "sự lặp lại", "cha mẹ" và các tên khác làm cho nó rõ ràng những gì được mong đợi khi kết hợp với tên hàm.
Sebastian Redl

1
Tôi phải đồng ý với bạn về tất cả các điểm. Vấn đề duy nhất mà Javascript thực sự đặt ra với mẫu này, đó là bạn đang làm việc trên dữ liệu có thể thay đổi và do đó, bạn rất có khả năng biến đổi trạng thái. Tuy nhiên, có một thư viện cho phép bạn truy cập vào các cơ sở dữ liệu clojure trong javascript đơn giản, nhưng tôi quên mất nó được gọi là gì. Truyền các đối số như một đối tượng cũng không phải là chưa từng nghe thấy, jquery thực hiện nhiều vị trí này, nhưng ghi lại những phần nào của đối tượng mà chúng sử dụng. Cá nhân tôi, tôi sẽ tách riêng các trường UI và các trường GameLogic, nhưng bất cứ điều gì phù hợp với bạn :)
Robin Heggelund Hansen

@SebastianRedl Tôi phải vượt qua để làm parentgì? Là repetitionsvà mảng của số hoặc chuỗi hoặc nó không quan trọng? Hoặc có thể sự lặp lại chỉ là một con số để đại diện cho số lần hối tiếc tôi muốn? Có rất nhiều apis ngoài kia chỉ cần lấy một đối tượng tùy chọn . Thế giới là một nơi tốt hơn nếu bạn đặt tên chính xác, nhưng nó không đảm bảo bạn sẽ biết cách sử dụng api, không có câu hỏi nào được hỏi.
Daniel Kaplan

8

Tôi đã thấy rằng mã của tôi có xu hướng kết cấu như vậy:

  • Các chức năng lấy bản đồ có xu hướng lớn hơn và có tác dụng phụ.
  • Các hàm lấy các đối số có xu hướng nhỏ hơn và thuần túy.

Tôi đã không bắt đầu tạo ra sự khác biệt này nhưng đó thường là cách nó kết thúc trong mã của tôi. Tôi không nghĩ sử dụng một phong cách nhất thiết phải phủ nhận phong cách khác.

Các chức năng thuần túy dễ dàng để kiểm tra đơn vị. Những cái lớn hơn với bản đồ sẽ đi sâu hơn vào khu vực thử nghiệm "tích hợp" vì chúng có xu hướng liên quan đến nhiều bộ phận chuyển động hơn.

Trong javascript, một điều có ích rất nhiều là sử dụng thứ gì đó như thư viện Match của Meteor để thực hiện xác thực tham số. Nó làm cho nó rất rõ những gì chức năng mong đợi và có thể xử lý bản đồ khá sạch sẽ.

Ví dụ,

function foo (post) {
  check(post, {
    text: String,
    timestamp: Date,
    // Optional, but if present must be an array of strings
    tags: Match.Optional([String])
    });

  // do stuff
}

Xem http://docs.meteor.com/#match để biết thêm.

:: CẬP NHẬT ::

Đoạn video ghi lại "Clojure in the Large" của Stuart Sierra cũng chạm vào chủ đề này. Giống như OP, anh ta kiểm soát các tác dụng phụ như một phần của bản đồ để việc kiểm tra trở nên dễ dàng hơn nhiều. Ông cũng có một bài đăng trên blog phác thảo quy trình làm việc Clojure hiện tại của mình có vẻ phù hợp.


1
Tôi nghĩ ý kiến ​​của tôi cho @Evicatos sẽ giải thích rõ hơn về vị trí của tôi ở đây. Vâng, tôi đang biến đổi và các chức năng không thuần túy. Nhưng, các chức năng của tôi thực sự dễ kiểm tra, đặc biệt là trong nhận thức về các khiếm khuyết hồi quy tôi không có kế hoạch kiểm tra. Một nửa tín dụng dành cho JS: Rất dễ dàng để xây dựng một "bản đồ" / đối tượng chỉ với dữ liệu tôi cần cho thử nghiệm của mình. Sau đó, nó đơn giản như truyền nó vào và kiểm tra các đột biến. Các tác dụng phụ luôn được thể hiện trên bản đồ, vì vậy chúng rất dễ kiểm tra.
Daniel Kaplan

1
Tôi tin rằng việc sử dụng thực tế cả hai phương pháp là cách "chính xác" để đi. Nếu việc kiểm tra dễ dàng đối với bạn và bạn có thể giảm thiểu vấn đề meta trong việc truyền đạt các trường bắt buộc cho các nhà phát triển khác, thì điều đó nghe có vẻ như là một chiến thắng. Cảm ơn câu hỏi của bạn; Tôi rất thích đọc các cuộc thảo luận thú vị mà bạn đã bắt đầu.
bắt đầu từ

5

Lập luận chính mà tôi có thể nghĩ ra để chống lại thực tiễn này là rất khó để nói được dữ liệu mà một chức năng thực sự cần là gì.

Điều đó có nghĩa là các lập trình viên tương lai trong codebase sẽ phải biết cách hàm được gọi hoạt động bên trong - và bất kỳ lệnh gọi hàm lồng nhau nào - để gọi nó.

Tôi càng nghĩ về nó, đối tượng gameState của bạn càng có mùi như toàn cầu. Nếu đó là cách nó được sử dụng, tại sao lại vượt qua nó?


1
Vâng, vì tôi thường biến đổi nó, nó là một toàn cầu. Tại sao phải đi qua nó? Tôi không biết, đó là một câu hỏi hợp lệ. Nhưng ruột của tôi nói với tôi rằng nếu tôi ngừng vượt qua nó, chương trình của tôi sẽ ngay lập tức trở nên khó khăn hơn để lý luận. Mọi chức năng có thể làm mọi thứ hoặc không có gì cho nhà nước toàn cầu. Hiện tại, bạn thấy tiềm năng đó trong chữ ký hàm. Nếu bạn không thể nói, tôi không tự tin về bất kỳ điều gì trong số này :)
Daniel Kaplan

1
BTW: re: đối số chính chống lại nó: Điều đó có vẻ đúng cho dù điều này là trong clojure hoặc javascript. Nhưng đó là một điểm có giá trị để thực hiện. Có lẽ những lợi ích được liệt kê vượt xa những tiêu cực của điều đó.
Daniel Kaplan

2
Bây giờ tôi biết lý do tại sao tôi bận tâm chuyển nó xung quanh mặc dù đó là một biến toàn cục: Nó cho phép tôi viết các hàm thuần túy. Nếu tôi đổi gameState = f(gameState)sang f(), việc kiểm tra sẽ khó khăn hơn nhiều f. f()có thể trả lại một điều khác nhau mỗi lần tôi gọi nó. Nhưng thật dễ dàng để f(gameState)trả lại cùng một thứ mỗi khi nó được cung cấp cùng một đầu vào.
Daniel Kaplan

3

Có nhiều tên phù hợp cho những gì bạn đang làm hơn Big ball of Mud . Những gì bạn đang làm được gọi là mô hình đối tượng Thiên Chúa . Nó không giống như vậy từ cái nhìn đầu tiên, nhưng trong Javascript có rất ít sự khác biệt giữa

update(gameState)
  ...
  gameState = handleUnitCollision(gameState)
  ...
  gameState = handleLoot(gameState)
  ...

{
  ...
  handleUnitCollision: function() {
    ...
  },
  ...
  handleLoot: function() {
    ...
  },
  ...
  update: function() {
    ...
    this.handleUnitCollision()
    ...
    this.handleLoot()
    ...
  },
  ...
};

Có hay không đó là một ý tưởng tốt có lẽ phụ thuộc vào hoàn cảnh. Nhưng nó chắc chắn phù hợp với cách Clojure. Một trong những mục tiêu của Clojure là loại bỏ cái mà Rich Hickey gọi là "sự phức tạp ngẫu nhiên". Nhiều đối tượng giao tiếp chắc chắn phức tạp hơn một đối tượng. Nếu bạn phân chia chức năng thành nhiều đối tượng, bạn đột nhiên phải lo lắng về giao tiếp và phối hợp và phân chia trách nhiệm. Đó là những điều phức tạp chỉ phụ thuộc vào mục tiêu ban đầu của bạn là viết chương trình. Bạn sẽ thấy cuộc nói chuyện của Rich Hickey trở nên đơn giản . Tôi nghĩ rằng đây là một ý tưởng rất tốt.


Liên quan, câu hỏi tổng quát hơn [ lập trình
viên.stackexchange.com/questions/260309/

"Trong lập trình hướng đối tượng, một đối tượng thần là một đối tượng biết quá nhiều hoặc làm quá nhiều. Đối tượng thần là một ví dụ về phản mẫu." Vì vậy, đối tượng thần không phải là một điều tốt, nhưng thông điệp của bạn dường như đang nói ngược lại. Điều đó hơi khó hiểu với tôi.
Daniel Kaplan

@tieTYT bạn không làm lập trình hướng đối tượng, vì vậy nó ổn
user7610

Làm thế nào bạn đi đến kết luận đó (cái "nó ổn")?
Daniel Kaplan

Vấn đề với đối tượng Thần trong OO là "Đối tượng trở nên nhận thức được mọi thứ hoặc tất cả các đối tượng trở nên quá phụ thuộc vào đối tượng thần đến nỗi khi có thay đổi hoặc lỗi để khắc phục, nó trở thành cơn ác mộng thực sự." nguồn Trong mã của bạn có một đối tượng khác bên cạnh đối tượng Thần, vì vậy phần thứ hai không phải là vấn đề. Về phần đầu tiên, đối tượng Thiên Chúa của bạn chương trình và chương trình của bạn nên nhận thức được mọi thứ. Vì vậy, điều đó cũng OK.
user7610

2

Tôi vừa mới đối mặt với chủ đề này sớm hơn hôm nay trong khi chơi với một dự án mới. Tôi đang làm việc tại Clojure để tạo ra một trò chơi Poker. Tôi đã đại diện cho các giá trị khuôn mặt và phù hợp làm từ khóa và quyết định đại diện cho một thẻ dưới dạng bản đồ như

{ :face :queen :suit :hearts }

Tôi cũng có thể tạo ra danh sách hoặc vectơ của hai yếu tố từ khóa. Tôi không biết liệu nó có tạo ra sự khác biệt về bộ nhớ / hiệu suất hay không vì vậy bây giờ tôi chỉ đi với bản đồ.

Mặc dù trong trường hợp tôi đổi ý sau này, tôi đã quyết định rằng hầu hết các phần trong chương trình của tôi nên đi qua một "giao diện" để truy cập vào các mảnh của thẻ, để chi tiết thực hiện được kiểm soát và ẩn đi. Tôi đã có chức năng

(defn face [card] (card :face))
(defn suit [card] (card :suit))

phần còn lại của chương trình sử dụng. Thẻ được chuyển qua các chức năng dưới dạng bản đồ, nhưng các chức năng sử dụng giao diện đã được thống nhất để truy cập vào bản đồ và do đó không nên gây rối.

Trong chương trình của tôi, một thẻ có thể sẽ chỉ là một bản đồ có giá trị 2. Trong câu hỏi, toàn bộ trạng thái trò chơi được truyền qua dưới dạng bản đồ. Trạng thái trò chơi sẽ phức tạp hơn nhiều so với một thẻ duy nhất, nhưng tôi không nghĩ có lỗi khi sử dụng bản đồ. Trong một ngôn ngữ bắt buộc đối tượng, tôi cũng có thể có một đối tượng GameState lớn và gọi các phương thức của nó, và có cùng một vấn đề:

class State
  def complex-process()
    state = clone(this) ; or just use 'this' below if mutation is fine
    state.subprocess-one()
    state.subprocess-two()
    state.subprocess-three()
    return state

Bây giờ nó hướng đối tượng. Có điều gì đặc biệt sai với nó? Tôi không nghĩ vậy, bạn chỉ ủy thác công việc cho các chức năng biết cách xử lý một đối tượng Bang. Và cho dù bạn đang làm việc với bản đồ hay đồ vật, bạn nên cảnh giác khi chia nó thành các phần nhỏ hơn. Vì vậy, tôi nói rằng việc sử dụng bản đồ là hoàn toàn tốt, miễn là bạn sử dụng chính sự chăm sóc mà bạn sẽ sử dụng với các đối tượng.


2

Từ những gì (ít) tôi đã thấy, sử dụng bản đồ hoặc các cấu trúc lồng nhau khác để tạo một đối tượng trạng thái bất biến toàn cầu duy nhất như thế này là khá phổ biến trong các ngôn ngữ chức năng, ít nhất là các ngôn ngữ thuần túy, đặc biệt là khi sử dụng Bang Monad như @ Ptharien'sFlame mentioend .

Hai rào cản để sử dụng điều này một cách hiệu quả mà tôi đã thấy / đọc về (và các câu trả lời khác ở đây đã đề cập) là:

  • Đột biến một giá trị lồng nhau (sâu sắc) ở trạng thái (bất biến)
  • Ẩn phần lớn trạng thái khỏi các chức năng không cần đến nó và chỉ cung cấp cho họ một chút mà họ cần để làm việc / biến đổi

Có một số kỹ thuật / mô hình phổ biến khác nhau có thể giúp giảm bớt các vấn đề này:

Đầu tiên là Dây khóa kéo : những cái này cho phép một người đi qua và biến đổi trạng thái sâu bên trong một hệ thống phân cấp lồng nhau bất biến.

Một cái khác là Lenses : chúng cho phép bạn tập trung vào cấu trúc đến một vị trí cụ thể và đọc / biến đổi giá trị ở đó. Bạn có thể kết hợp các ống kính khác nhau lại với nhau để tập trung vào những thứ khác nhau, giống như một chuỗi thuộc tính có thể điều chỉnh trong OOP (nơi bạn có thể thay thế các biến cho tên thuộc tính thực tế!)

Prismatic gần đây đã có một bài đăng trên blog về việc sử dụng loại kỹ thuật này, trong số những thứ khác, trong JavaScript / ClojureScript, mà bạn nên kiểm tra. Họ sử dụng các con trỏ (mà chúng so sánh với khóa kéo) để trạng thái cửa sổ cho các chức năng:

Om khôi phục đóng gói và mô đun hóa bằng con trỏ. Các con trỏ cung cấp các cửa sổ có khả năng cập nhật vào các phần cụ thể của trạng thái ứng dụng (giống như khóa kéo), cho phép các thành phần lấy tham chiếu đến các phần có liên quan của trạng thái toàn cầu và cập nhật chúng theo cách không ngữ cảnh.

IIRC, họ cũng đề cập đến tính bất biến trong JavaScript trong bài đăng đó.


Cuộc thảo luận mà OP đề cập cũng thảo luận về việc sử dụng chức năng cập nhật để giới hạn phạm vi mà một chức năng có thể cập nhật thành một cây con của bản đồ trạng thái. Tôi nghĩ rằng chưa có ai đưa ra điều này.
dùng7610

@ user7610 Bắt tốt, tôi không thể tin rằng tôi đã quên đề cập đến cái đó - Tôi thích chức năng đó (và assoc-inet al). Đoán tôi chỉ có Haskell trên não. Tôi tự hỏi nếu có ai thực hiện một cổng JavaScript của nó? Mọi người có lẽ đã không đưa nó lên bởi vì (như tôi) họ đã không xem cuộc nói chuyện :)
paul

@paul theo nghĩa mà họ có vì nó có sẵn trong ClojureScript, nhưng tôi không chắc điều đó có "tính" trong tâm trí bạn không. Nó có thể tồn tại trong PureScript và tôi tin rằng có ít nhất một thư viện cung cấp các cấu trúc dữ liệu bất biến trong JavaScript. Tôi hy vọng ít nhất một trong số họ có những thứ này, nếu không họ sẽ bất tiện khi sử dụng.
Daniel Kaplan

@tieTYT Tôi đã từng nghĩ đến việc triển khai JS nguyên gốc khi tôi đưa ra nhận xét đó nhưng bạn đã đưa ra một quan điểm tốt về ClojureScript / PureScript. Tôi nên xem xét về JS bất biến và xem những gì ở ngoài đó, tôi chưa từng làm việc với nó trước đây.
paul

1

Đây có phải là một ý tưởng tốt hay không sẽ thực sự phụ thuộc vào những gì bạn đang làm với trạng thái bên trong các quy trình con đó. Nếu tôi hiểu chính xác ví dụ Clojure, các từ điển trạng thái được trả về không phải là từ điển trạng thái giống nhau đang được truyền vào. Chúng là các bản sao, có thể có bổ sung và sửa đổi, mà (tôi giả sử) Clojure có thể tạo ra một cách hiệu quả bởi vì bản chất chức năng của ngôn ngữ phụ thuộc vào nó. Các từ điển trạng thái ban đầu cho mỗi chức năng không được sửa đổi theo bất kỳ cách nào.

Nếu tôi hiểu chính xác, bạn đang sửa đổi các đối tượng trạng thái bạn chuyển vào các hàm javascript của mình thay vì trả về một bản sao, điều đó có nghĩa là bạn đang làm một cái gì đó rất, rất khác với những gì mà mã Clojure đang làm. Như Mike Partridge đã chỉ ra, về cơ bản đây chỉ là một toàn cầu mà bạn rõ ràng chuyển đến và trở về từ các chức năng mà không có lý do thực sự. Tại thời điểm này tôi nghĩ rằng nó chỉ đơn giản là làm cho bạn nghĩ rằng bạn đang làm một cái gì đó mà bạn thực sự không.

Nếu bạn thực sự đang tạo ra các bản sao của trạng thái, sửa đổi nó và sau đó trả lại bản sao đã sửa đổi đó, sau đó tiếp tục. Tôi không chắc đó là cách tốt nhất để thực hiện những gì bạn đang cố gắng thực hiện trong Javascript, nhưng có lẽ nó "gần gũi" với những gì mà ví dụ Clojure đang làm.


1
Cuối cùng thì nó thực sự "rất, rất khác"? Trong ví dụ Clojure, anh ta ghi đè lên trạng thái cũ của mình bằng trạng thái mới. Vâng, không có đột biến thực sự xảy ra, danh tính chỉ đang thay đổi. Nhưng trong ví dụ "tốt" của mình, như đã viết, anh ta không có cách nào để có được bản sao được chuyển vào quy trình con - hai. Việc xác định giá trị đó đã được ghi đè. Do đó, tôi nghĩ rằng điều "rất, rất khác" thực sự chỉ là một chi tiết thực hiện ngôn ngữ. Ít nhất là trong bối cảnh của những gì bạn đưa lên.
Daniel Kaplan

2
Có hai điều xảy ra với ví dụ Clojure: 1) ví dụ đầu tiên phụ thuộc vào các hàm được gọi theo một thứ tự nhất định và 2) các hàm là thuần túy để chúng không có bất kỳ tác dụng phụ nào. Vì các hàm trong ví dụ thứ hai là thuần túy và chia sẻ cùng một chữ ký, bạn có thể sắp xếp lại chúng mà không phải lo lắng về bất kỳ phụ thuộc ẩn nào theo thứ tự mà chúng được gọi. Nếu bạn đang thay đổi trạng thái trong các chức năng của mình thì bạn không có sự bảo đảm tương tự. Đột biến trạng thái có nghĩa là phiên bản của bạn không thể ghép lại được, đó là lý do ban đầu của từ điển.
Evicatos

1
Bạn sẽ phải cho tôi xem một ví dụ bởi vì theo kinh nghiệm của tôi với điều này, tôi có thể di chuyển mọi thứ xung quanh theo ý muốn và nó có rất ít hiệu quả. Chỉ để chứng minh điều đó với bản thân mình, tôi đã di chuyển hai cuộc gọi quy trình con ngẫu nhiên ở giữa update()chức năng của mình . Tôi di chuyển một đến đỉnh và một xuống đáy. Tất cả các thử nghiệm của tôi vẫn vượt qua và khi tôi chơi trò chơi của mình, tôi nhận thấy không có hiệu ứng xấu. Tôi cảm thấy các chức năng của tôi chỉ có thể ghép lại như ví dụ Clojure. Cả hai chúng ta đều vứt bỏ dữ liệu cũ của mình sau mỗi bước.
Daniel Kaplan

1
Các xét nghiệm của bạn vượt qua và không nhận thấy bất kỳ tác động xấu nào có nghĩa là bạn hiện không biến đổi bất kỳ trạng thái nào có tác dụng phụ không mong muốn ở những nơi khác. Vì các chức năng của bạn không thuần túy mặc dù bạn không có gì đảm bảo sẽ luôn như vậy. Tôi nghĩ rằng về cơ bản tôi phải hiểu nhầm điều gì đó về việc triển khai của bạn nếu bạn nói rằng các chức năng của bạn không thuần túy nhưng bạn đang vứt bỏ dữ liệu cũ của mình sau mỗi bước.
Evicatos

1
@Evicatos - Tinh khiết và có cùng chữ ký không ngụ ý rằng thứ tự của các chức năng không quan trọng. Hãy tưởng tượng tính toán một mức giá với chiết khấu cố định và tỷ lệ phần trăm được áp dụng. (-> 10 (- 5) (/ 2))trả về 2,5. (-> 10 (/ 2) (- 5))trả về 0.
Zak

1

Nếu bạn có một đối tượng trạng thái toàn cầu, đôi khi được gọi là "đối tượng thần", được truyền cho mỗi quá trình, cuối cùng bạn sẽ gây ra một số yếu tố, tất cả đều làm tăng sự liên kết, trong khi giảm sự gắn kết. Những yếu tố này đều tác động đến khả năng duy trì lâu dài theo cách tiêu cực.

Khớp nối Tramp Điều này phát sinh từ việc truyền dữ liệu thông qua các phương pháp khác nhau mà không cần gần như tất cả dữ liệu, để đưa nó đến nơi thực sự có thể xử lý. Loại khớp nối này tương tự như sử dụng dữ liệu toàn cầu, nhưng có thể được chứa nhiều hơn. Khớp nối tramp ngược lại với "cần biết", được sử dụng để bản địa hóa các hiệu ứng và để chứa thiệt hại mà một đoạn mã sai lầm có thể gây ra trên toàn hệ thống.

Điều hướng dữ liệu Mỗi quy trình phụ trong ví dụ của bạn cần biết cách lấy chính xác dữ liệu cần thiết và nó cần có khả năng xử lý nó và có thể xây dựng một đối tượng trạng thái toàn cầu mới. Đó là hệ quả logic của khớp nối tramp; toàn bộ bối cảnh của một mốc thời gian là cần thiết để hoạt động trên mốc. Một lần nữa, kiến ​​thức phi địa phương là một điều xấu.

Nếu bạn đang đi trong "dây kéo", "ống kính" hoặc "con trỏ", như được mô tả trong bài viết của @paul, đó là một điều. Bạn sẽ có quyền truy cập và cho phép khóa kéo, v.v., kiểm soát việc đọc và ghi dữ liệu.

Vi phạm trách nhiệm đơn lẻ Yêu cầu mỗi "quy trình-một", "quy trình hai-hai" và "quy trình-ba" chỉ có một trách nhiệm duy nhất, đó là tạo ra một đối tượng trạng thái toàn cầu mới với các giá trị phù hợp trong đó, là chủ nghĩa giảm thiểu nghiêm trọng. Đó là tất cả các bit cuối cùng, phải không?

Quan điểm của tôi ở đây là có tất cả các thành phần chính của trò chơi của bạn có tất cả các trách nhiệm giống như trò chơi của bạn đánh bại mục đích ủy thác và bao thanh toán.

Tác động hệ thống

Tác động chính của thiết kế của bạn là khả năng bảo trì thấp. Thực tế là bạn có thể giữ toàn bộ trò chơi trong đầu nói rằng bạn rất có thể là một lập trình viên xuất sắc. Có rất nhiều thứ tôi đã thiết kế mà tôi có thể giữ trong đầu cho toàn bộ dự án. Đó không phải là điểm của kỹ thuật hệ thống, mặc dù. Vấn đề là làm cho một hệ thống có thể hoạt động cho một cái gì đó lớn hơn một người có thể giữ trong đầu cùng một lúc .

Thêm một lập trình viên khác, hoặc hai hoặc tám, sẽ khiến hệ thống của bạn sụp đổ gần như ngay lập tức.

  1. Đường cong học tập cho các đối tượng thần là bằng phẳng (nghĩa là, phải mất một thời gian rất dài để trở nên có năng lực trong chúng). Mỗi lập trình viên bổ sung sẽ cần học mọi thứ bạn biết và giữ nó trong đầu. Bạn sẽ chỉ có thể thuê các lập trình viên giỏi hơn bạn, giả sử bạn có thể trả cho họ đủ để chịu đựng thông qua việc duy trì một đối tượng thần khổng lồ.
  2. Cung cấp thử nghiệm, trong mô tả của bạn, chỉ là hộp màu trắng. Bạn cần biết từng chi tiết của đối tượng thần, cũng như mô-đun đang thử nghiệm, để thiết lập thử nghiệm, chạy nó và xác định rằng a) nó đã làm đúng và b) nó không làm gì cả trong số 10.000 điều sai. Các tỷ lệ cược được xếp chồng lên nhau rất nhiều.
  3. Thêm một tính năng mới yêu cầu bạn a) trải qua mọi quy trình con và xác định xem tính năng đó có ảnh hưởng đến bất kỳ mã nào trong đó không và ngược lại, b) đi qua trạng thái toàn cầu của bạn và thiết kế các bổ sung, và c) trải qua mọi thử nghiệm đơn vị và sửa đổi nó để kiểm tra xem không có đơn vị nào đang thử nghiệm ảnh hưởng xấu đến tính năng mới .

Cuối cùng

  1. Các đối tượng thần đột biến là lời nguyền của sự tồn tại lập trình của tôi, một số việc tôi làm và một số mà tôi đã bị mắc kẹt.
  2. Các nhà nước không quy mô. Nhà nước phát triển theo cấp số nhân, với tất cả các hàm ý cho thử nghiệm và hoạt động ngụ ý. Cách chúng ta kiểm soát trạng thái trong các hệ thống hiện đại là bằng cách ủy quyền (phân vùng trách nhiệm) và phạm vi (hạn chế quyền truy cập chỉ một tập hợp con của trạng thái). Cách tiếp cận "mọi thứ là bản đồ" trái ngược hoàn toàn với trạng thái kiểm soát.
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.