Đã 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_words
sang phía phong cách chức năng của ranh giới. Không replace
hoặc split
sửa đổi chuỗi gốc. Và giao diện công cộng của chúng tôi build_trie
cũ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_list
trả 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_trie
vớ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 length
tí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 replace
và split
cá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.