Một số kỹ thuật tôi có thể sử dụng để cấu trúc lại mã hướng đối tượng thành mã chức năng là gì?


9

Tôi đã dành khoảng 20-40 giờ để phát triển một phần của trò chơi bằng cách sử dụng JavaScript và HTML5 canvas. Khi tôi bắt đầu, tôi không biết mình đang làm gì. Vì vậy, nó bắt đầu như một bằng chứng về khái niệm và hiện đang xuất hiện độc đáo, nhưng nó không có thử nghiệm tự động. Trò chơi đang bắt đầu trở nên phức tạp đến mức nó có thể được hưởng lợi từ một số thử nghiệm tự động, nhưng có vẻ khó thực hiện vì mã phụ thuộc vào việc thay đổi trạng thái toàn cầu.

Tôi muốn cấu trúc lại toàn bộ bằng cách sử dụng Underscore.js , một thư viện lập trình chức năng cho JavaScript. Một phần trong tôi nghĩ rằng tôi chỉ nên bắt đầu từ đầu bằng cách sử dụng kiểu thử nghiệm và lập trình chức năng. Nhưng, tôi nghĩ rằng việc tái cấu trúc mã mệnh lệnh thành mã khai báo có thể là một trải nghiệm học tập tốt hơn và là cách an toàn hơn để đi đến trạng thái chức năng hiện tại của tôi.

Vấn đề là, tôi biết cuối cùng tôi muốn mã của mình trông như thế nào, nhưng tôi không biết làm thế nào để biến mã hiện tại của mình thành mã. Tôi hy vọng một số người ở đây có thể cho tôi một số lời khuyên là cuốn sách Tái cấu trúc và làm việc hiệu quả với Bộ luật kế thừa.


Ví dụ, bước đầu tiên tôi nghĩ về việc "cấm" nhà nước toàn cầu. Thay vào đó, hãy lấy mọi hàm sử dụng một biến toàn cục và chuyển nó thành một tham số.

Bước tiếp theo có thể là "cấm" đột biến và luôn trả về một đối tượng mới.

Lời khuyên nào sẽ được đánh giá cao. Tôi chưa bao giờ lấy mã OO và tái cấu trúc nó thành mã chức năng trước đây.


1
"Bước tiếp theo có thể là" cấm "đột biến và luôn trả lại một đối tượng mới.": IMO bạn không cần phải cấm hoàn toàn đột biến: tính minh bạch tham chiếu là điều đáng quan tâm. Bạn có thể sử dụng các cập nhật phá hoại như một kỹ thuật tối ưu hóa với điều kiện bạn duy trì tính minh bạch tham chiếu.
Giorgio

@Giorgio tốt để biết. Tuy nhiên, tôi muốn "cấm" đột biến và tối ưu hóa thứ hai.
Daniel Kaplan

1
Tôi nghĩ rằng hai bước đầu tiên của bạn là một ý tưởng tuyệt vời! Tôi sẽ rất vinh dự nếu bạn xem một thư viện JS chức năng mà tôi đã viết (nhỏ bé) để tạo ra các chuỗi các hàm được tạo thành đơn điệu, nó có thể giúp ích; nó đi qua trạng thái thông qua chuỗi chức năng nhưng không bao giờ biến đổi; mỗi hàm bạn xâu chuỗi với nó sẽ đảm bảo các giá trị được sao chép để trả về một giá trị mới thay vì giá trị được trao cho nó. github.com/JimmyHoffa/Machinad Bản thân lib khá phức tạp nếu bạn không quen thuộc với các đơn nguyên, nhưng hãy xem ví dụ và bạn sẽ thấy nó rất dễ sử dụng; nó chỉ phức tạp để thực hiện.
Jimmy Hoffa

1
Nếu bạn muốn làm mọi thứ theo phong cách chức năng, một điều tôi chắc chắn rằng bạn sẽ không sử dụng mã của mình nếu nó thậm chí còn ở đó. buộc bản thân phải soạn thảo các hàm và sử dụng những gì javascript tuyệt vời: phạm vi từ vựng để làm cho các phương tiện có sẵn ở nơi bạn cần thay vì các cây phân cấp. Xác định cấu trúc dữ liệu của bạn giống như các cấu trúc dữ liệu đó, chỉ thêm các hàm cho chúng để hỗ trợ kiểu lưu loát trong đó hàm sẽ tốt khi lấy cấu trúc dữ liệu làm tham số đầu tiên. Trong OO đây sẽ là một mô hình thiếu máu; nhưng chúng ta đang nói về FP, thành phần là chìa khóa để tránh những nhược điểm đó.
Jimmy Hoffa

@tieTYT: Tất nhiên tôi đồng ý với bạn, tất cả chúng ta đều biết rằng tối ưu hóa sớm là xấu.
Giorgio

Câu trả lời:


9

Đã là tác giả của một số dự án theo kiểu JavaScript chức năng, cho phép tôi chia sẻ kinh nghiệm của mình. Tôi sẽ tập trung vào các cân nhắc cấp cao, không quan tâm thực hiện cụ thể. Hãy nhớ rằng, không có viên đạn bạc, hãy làm những gì tốt nhất cho tình huống cụ thể của bạn.

Pure Functional vs Functional Style

Xem xét những gì bạn muốn đạt được từ tái cấu trúc chức năng này. Bạn có thực sự muốn triển khai chức năng thuần túy hay chỉ một số lợi ích mà phong cách lập trình chức năng có thể mang lại, chẳng hạn như tính minh bạch tham chiếu.

Tôi đã tìm thấy cách tiếp cận thứ hai, cái mà tôi gọi là kiểu chức năng, để kết hợp tốt với JavaScript và thường là thứ tôi muốn. Nó cũng cho phép các khái niệm phi chức năng chọn lọc được sử dụng cẩn thận trong mã nơi chúng có ý nghĩa.

Viết mã theo kiểu hoàn toàn chức năng hơn cũng có thể có trong JavaScript, nhưng không phải lúc nào cũng là giải pháp phù hợp nhất. Và Javascript không bao giờ có thể hoàn toàn là chức năng.

Giao diện kiểu chức năng

Trong Javascript kiểu chức năng, việc xem xét quan trọng nhất là ranh giới giữa mã chức năng và mã mệnh lệnh. Rõ ràng hiểu và xác định ranh giới này là cần thiết.

Tôi tổ chức mã của tôi xung quanh các giao diện phong cách chức năng. Đây là các tập hợp logic hoạt động theo kiểu chức năng, từ các hàm riêng lẻ đến toàn bộ các mô-đun. Sự tách biệt về mặt khái niệm của giao diện và triển khai là rất quan trọng, một giao diện kiểu chức năng có thể được triển khai bắt buộc, miễn là việc thực thi bắt buộc không bị rò rỉ qua ranh giới.

Thật không may trong Javascript, gánh nặng của việc thực thi các giao diện kiểu chức năng hoàn toàn thuộc về nhà phát triển. Ngoài ra, các tính năng ngôn ngữ, chẳng hạn như ngoại lệ, làm cho một sự tách biệt thực sự sạch sẽ là không thể.

Vì bạn sẽ tái cấu trúc một cơ sở mã hiện tại, hãy bắt đầu bằng cách xác định các giao diện logic hiện có trong mã của bạn để có thể được chuyển sạch sang kiểu chức năng. Một số lượng đáng ngạc nhiên của mã có thể đã là phong cách chức năng. Điểm khởi đầu tốt là các giao diện nhỏ với một vài phụ thuộc bắt buộc. Mỗi khi bạn di chuyển qua mã, các phụ thuộc bắt buộc hiện có sẽ bị loại bỏ và nhiều mã có thể được chuyển qua.

Tiếp tục lặp lại, dần dần làm việc ra bên ngoài để đẩy các phần lớn của dự án của bạn lên phía chức năng của ranh giới, cho đến khi bạn hài lòng với kết quả. Cách tiếp cận gia tăng này cho phép thử nghiệm các thay đổi riêng lẻ và giúp xác định và thực thi ranh giới dễ dàng hơn.

Suy nghĩ lại về thuật toán, cấu trúc dữ liệu và thiết kế

Các giải pháp bắt buộc và các mẫu thiết kế thường không có ý nghĩa trong phong cách chức năng. Đặc biệt đối với các cấu trúc dữ liệu, các cổng trực tiếp của mã mệnh lệnh sang kiểu chức năng có thể xấu và chậm. Một ví dụ đơn giản: bạn muốn sao chép mọi yếu tố của danh sách cho một khoản trả trước hoặc đưa phần tử vào danh sách hiện có?

Nghiên cứu và đánh giá các phương pháp tiếp cận chức năng hiện có cho các vấn đề. Lập trình chức năng không chỉ là một kỹ thuật lập trình, nó là một cách nghĩ khác và giải quyết vấn đề. Các dự án được viết bằng các ngôn ngữ lập trình chức năng truyền thống hơn, chẳng hạn như lisp hoặc haskell, là những tài nguyên tuyệt vời trong vấn đề này.

Hiểu ngôn ngữ và nội dung

Đây là một phần quan trọng để hiểu ranh giới mệnh lệnh chức năng và sẽ ảnh hưởng đến thiết kế giao diện. Hiểu những tính năng nào có thể được sử dụng một cách an toàn trong mã kiểu chức năng và những tính năng nào không thể. Mọi thứ mà từ đó các nội trang có thể đưa ra các ngoại lệ và, như [].push, làm biến đổi các đối tượng, cho các đối tượng được truyền qua tham chiếu và cách các chuỗi nguyên mẫu hoạt động. Một sự hiểu biết tương tự về bất kỳ thư viện nào khác mà bạn tiêu thụ trong dự án của bạn cũng được yêu cầu.

Kiến thức như vậy cho phép đưa ra các quyết định thiết kế sáng suốt, như làm thế nào để xử lý các trường hợp ngoại lệ và ngoại lệ xấu, hoặc điều gì xảy ra nếu một hàm gọi lại làm điều gì đó bất ngờ? Một số điều này có thể được thi hành theo mã, nhưng một số chỉ yêu cầu tài liệu về cách sử dụng phù hợp.

Một điểm khác là mức độ nghiêm ngặt của bạn khi thực hiện. Bạn có thực sự muốn thử thực thi hành vi chính xác đối với các lỗi ngăn xếp cuộc gọi tối đa hoặc khi người dùng xác định lại một số hàm dựng sẵn không?

Sử dụng các khái niệm phi chức năng khi thích hợp

Các cách tiếp cận chức năng không phải lúc nào cũng phù hợp, đặc biệt là trong một ngôn ngữ như JavaScript. Giải pháp đệ quy có thể thanh lịch cho đến khi bạn bắt đầu nhận được ngoại lệ ngăn xếp cuộc gọi tối đa cho các đầu vào lớn hơn, vì vậy có lẽ cách tiếp cận lặp lại sẽ tốt hơn. Tương tự, tôi sử dụng sự kế thừa cho tổ chức mã, gán trong các hàm tạo và các đối tượng bất biến nơi chúng có ý nghĩa.

Miễn là các giao diện chức năng không bị vi phạm, hãy làm những gì có ý nghĩa và những gì sẽ dễ kiểm tra và bảo trì nhất.


Thí dụ

Dưới đây là một ví dụ về việc chuyển đổi một số mã thực rất đơn giản sang kiểu chức năng. Tôi không có một ví dụ lớn về quy trình, nhưng cái nhỏ này chạm vào một số điểm thú vị.

Mã này xây dựng một trie từ một chuỗi. Tôi bắt đầu với một số mã dựa trên phần trên cùng của mô-đun trie-js build-trie của John Resig . Nhiều đơn giản hóa / định dạng thay đổi đã được thực hiện và mục đích của tôi không phải là để bình luận về chất lượng của mã gốc (Có nhiều cách rõ ràng hơn và sạch hơn nhiều để xây dựng một Trie, vậy tại sao mã c phong cách này đi lên đầu tiên trong google là thú vị. Đây là một thực hiện nhanh mà sử dụng giảm ).

Tôi thích ví dụ này bởi vì tôi không cần phải thực hiện tất cả các cách để thực hiện chức năng thực sự, chỉ với các giao diện chức năng. Đây là điểm bắt đầu:

var txt = SOME_USER_STRING,
    words = txt.replace(/\n/g, "").split(" "),
    trie = {};

for (var i = 0, l = words.length; i < l; i++) {
    var word = words[i], letters = word.split(""), cur = trie;
    for (var j = 0; j < letters.length; j++) {
        var letter = letters[j];
        if (!cur.hasOwnProperty(letter))
            cur[letter] = {};
        cur = cur[ letter ];
    }
    cur['$'] = true;
}

// Output for text = 'a ab f abc abf'
trie = {
    "a": {
        "$":true,
        "b":{
            "$":true,
            "c":{"$":true}
        "f":{"$":true}}},
    "f":{"$":true}};

Hãy giả vờ đây là toàn bộ chương trình. Chương trình này thực hiện hai điều: chuyển đổi một chuỗi thành một danh sách các từ và xây dựng một bộ ba từ danh sách các từ. Đây là các giao diện hiện có trong chương trình. Đây là chương trình của chúng tôi với các giao diện được thể hiện rõ ràng hơn:

var _get_words = function(txt) {
    return txt.replace(/\n/g, "").split(" ");
};

var _build_trie_from_list = function(words, trie) {
    for (var i = 0, l = words.length; i < l; i++) {
        var word = words[i], letters = word.split(""), cur = trie;
        for (var j = 0; j < letters.length; j++) {
            var letter = letters[j];
            if (!cur.hasOwnProperty(letter))
                cur[letter] = {};
            cur = cur[ letter ];
        }
        cur['$'] = true;
    }
};

// The 'public' interface
var build_trie = function(txt) { 
    var words = _get_words(txt), trie = {};
    _build_trie_from_list(words, trie);
    return trie;
};

Chỉ bằng cách xác định các giao diện của chúng tôi, chúng tôi có thể di chuyển _get_wordssang phía phong cách chức năng của ranh giới. Không replacehoặc splitsửa đổi chuỗi gốc. Và giao diện công cộng của chúng tôi build_triecũng có phong cách chức năng, mặc dù nó tương tác với một số mã rất bắt buộc. Trong thực tế, đây là một điểm dừng tốt trong hầu hết các trường hợp. Nhiều mã hơn sẽ trở nên áp đảo, vì vậy hãy để tôi tổng quan một vài thay đổi khác.

Đầu tiên, làm cho tất cả các giao diện phong cách chức năng. Điều này là không quan trọng trong trường hợp này, chỉ cần _build_trie_from_listtrả lại một đối tượng thay vì biến đổi một đối tượng.

Xử lý đầu vào xấu

Xem xét những gì xảy ra nếu chúng ta gọi build_trievới một loạt các ký tự , build_trie(['a', ' ', 'a', 'b', 'c', ' ', 'f']). Người gọi cho rằng điều này sẽ hành xử theo kiểu chức năng, nhưng thay vào đó, nó đưa ra một ngoại lệ khi .replaceđược gọi trên mảng. Đây có thể là hành vi dự định. Hoặc chúng ta có thể thực hiện kiểm tra loại rõ ràng và đảm bảo đầu vào như chúng ta mong đợi. Nhưng tôi thích gõ vịt hơn.

Chuỗi chỉ là mảng các ký tự và mảng chỉ là các đối tượng có thuộc lengthtính và số nguyên không âm làm khóa. Vì vậy, nếu chúng ta tái cấu trúc và viết mới replacesplitcác phương thức hoạt động trên các đối tượng chuỗi giống như mảng, chúng thậm chí không phải là chuỗi, mã của chúng ta có thể thực hiện đúng. ( String.prototype.*sẽ không hoạt động ở đây, nó chuyển đổi đầu ra thành một chuỗi). Gõ vịt là một cách hoàn toàn tách biệt với lập trình chức năng, nhưng điểm lớn hơn là đầu vào xấu phải luôn được xem xét.

Thiết kế lại cơ bản

Ngoài ra còn có những cân nhắc cơ bản hơn. Giả sử rằng chúng tôi muốn xây dựng bộ ba theo phong cách chức năng là tốt. Trong mã bắt buộc, cách tiếp cận là xây dựng một từ một lần. Một cổng trực tiếp sẽ yêu cầu chúng tôi sao chép toàn bộ bộ ba mỗi lần chúng tôi cần chèn để tránh làm biến đổi các đối tượng. Rõ ràng là sẽ không làm việc. Thay vào đó, trie có thể được xây dựng từng nút theo từng nút, từ dưới lên, để một khi nút được hoàn thành, nó không bao giờ phải chạm lại. Hoặc một cách tiếp cận khác hoàn toàn có thể tốt hơn, một tìm kiếm nhanh cho thấy nhiều triển khai chức năng hiện có của các lần thử.

Hy vọng ví dụ làm rõ mọi thứ một chút.


Nhìn chung, tôi thấy viết mã kiểu chức năng trong Javascript là một nỗ lực đáng giá và thú vị. Javascript là một ngôn ngữ chức năng có khả năng đáng ngạc nhiên, mặc dù quá dài dòng ở trạng thái mặc định.

Chúc may mắn với dự án của bạn.

Một vài dự án cá nhân được viết theo phong cách chức năng Javascript:

  • parse.js - Bộ kết hợp phân tích cú pháp
  • Nu - Suối lười
  • Atum - Trình thông dịch Javascript được viết theo phong cách chức năng Javascript.
  • Khepri - Ngôn ngữ lập trình nguyên mẫu tôi sử dụng để phát triển Javascript chức năng. Thực hiện theo phong cách chức năng Javascript.

Cảm ơn rất nhiều vì đã dành thời gian để viết nó lên. Một số ví dụ sẽ được đánh giá rất cao (và các dự án cá nhân không giúp ích gì cho việc này vì chúng hiển thị "sau", chúng không chỉ ra cách đến đó từ "trước"). Điều này sẽ hữu ích nhất trong phần của bạn có tiêu đề "Giao diện kiểu chức năng".
Daniel Kaplan

Tôi đã thêm một ví dụ đơn giản. Tìm một ví dụ lớn hơn thể hiện quá trình chuyển đổi sẽ khó khăn, vì vậy tôi khuyên bạn nên cố gắng bắt đầu làm việc với mã thế giới thực càng sớm càng tốt. Nó thực sự là một quá trình cụ thể của dự án và thực sự hoạt động mặc dù mã sẽ dạy cho bạn rất nhiều. Có thể đường chuyền đầu tiên không tuyệt vời, nhưng hãy lặp đi lặp lại và học hỏi cho đến khi bạn hài lòng với kết quả. Và có rất nhiều ví dụ tuyệt vời về thiết kế chức năng trực tuyến cho các ngôn ngữ khác (cá nhân tôi thấy lược đồ và clojure là điểm khởi đầu tốt nhất cho nhiều chủ đề).
Matt Bierner

Tôi ước câu trả lời này được chia thành nhiều phần để tôi có thể nâng cao từng phần
paul
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.