Tôi có quá 'thông minh' để có thể đọc được bởi các nhà phát triển không? Quá nhiều lập trình chức năng trong JS của tôi? [đóng cửa]


133

Tôi là một dev-end dev, mã hóa trong Babel ES6. Một phần trong ứng dụng của chúng tôi thực hiện cuộc gọi API và dựa trên mô hình dữ liệu mà chúng tôi nhận được từ cuộc gọi API, một số biểu mẫu nhất định cần phải được điền vào.

Các biểu mẫu đó được lưu trữ trong một danh sách liên kết đôi (nếu phần cuối cho biết một số dữ liệu không hợp lệ, chúng tôi có thể nhanh chóng đưa người dùng quay lại một trang mà họ đã gửi nhầm và sau đó đưa chúng trở lại mục tiêu, chỉ bằng cách sửa đổi danh sách.)

Dù sao, có một loạt các chức năng được sử dụng để thêm các trang và tôi tự hỏi liệu tôi có quá thông minh không. Đây chỉ là một tổng quan cơ bản - thuật toán thực tế phức tạp hơn nhiều, với hàng tấn trang và loại trang khác nhau, nhưng điều này sẽ cho bạn một ví dụ.

Đây là cách tôi nghĩ, một lập trình viên mới sẽ xử lý nó.

export const addPages = (apiData) => {
   let pagesList = new PagesList(); 

   if(apiData.pages.foo){
     pagesList.add('foo', apiData.pages.foo){
   }

   if (apiData.pages.arrayOfBars){
      let bars = apiData.pages.arrayOfBars;
      bars.forEach((bar) => {
         pagesList.add(bar.name, bar.data);
      })
   }

   if (apiData.pages.customBazes) {
      let bazes = apiData.pages.customBazes;
      bazes.forEach((baz) => {
         pagesList.add(customBazParser(baz)); 
      })
   } 

   return pagesList;
}

Bây giờ, để dễ kiểm chứng hơn, tôi đã lấy tất cả những câu lệnh đó và làm cho chúng tách rời nhau, hoạt động độc lập và sau đó tôi ánh xạ chúng.

Bây giờ, có thể kiểm tra là một điều, nhưng cũng có thể đọc được và tôi tự hỏi liệu tôi có làm cho mọi thứ trở nên ít đọc hơn ở đây không.

// file: '../util/functor.js'

export const Identity = (x) => ({
  value: x,
  map: (f) => Identity(f(x)),
})

// file 'addPages.js' 

import { Identity } from '../util/functor'; 

export const parseFoo = (data) => (list) => {
   list.add('foo', data); 
}

export const parseBar = (data) => (list) => {
   data.forEach((bar) => {
     list.add(bar.name, bar.data)
   }); 
   return list; 
} 

export const parseBaz = (data) => (list) => {
   data.forEach((baz) => {
      list.add(customBazParser(baz)); 
   })
   return list;
}


export const addPages = (apiData) => {
   let pagesList = new PagesList(); 
   let { foo, arrayOfBars: bars, customBazes: bazes } = apiData.pages; 

   let pages = Identity(pagesList); 

   return pages.map(foo ? parseFoo(foo) : x => x)
               .map(bars ? parseBar(bars) : x => x)
               .map(bazes ? parseBaz(bazes) : x => x)
               .value

}

Đây là mối quan tâm của tôi. Đối với tôi phía dưới có tổ chức hơn. Bản thân mã được chia thành các phần nhỏ hơn có thể kiểm tra được trong sự cô lập. NHƯNG tôi đang nghĩ: Nếu tôi phải đọc rằng với tư cách là một nhà phát triển cơ sở, không được sử dụng các khái niệm như sử dụng chức năng nhận dạng danh tính, currying, hoặc các câu lệnh ternary, liệu tôi có thể hiểu giải pháp sau này đang làm gì không? Đôi khi làm những việc theo cách "sai, dễ dàng" hơn?


13
với tư cách là một người chỉ có 10 năm tự học trong JS, tôi sẽ coi mình là một người và bạn đã mất tôi tạiBabel ES6
RozzA

26
OMG - đã hoạt động trong ngành công nghiệp từ năm 1999 và mã hóa từ năm 1983, và bạn là loại nhà phát triển có hại nhất. Những gì bạn nghĩ là "thông minh" thực sự được gọi là "đắt đỏ" và "khó duy trì" và "một nguồn lỗi" và nó không có chỗ trong môi trường kinh doanh. Ví dụ đầu tiên là đơn giản, dễ hiểu và nó hoạt động trong khi ví dụ thứ hai phức tạp, khó hiểu và không thể chứng minh chính xác. Hãy ngừng làm điều này. Nó KHÔNG tốt hơn, ngoại trừ có thể trong một số ý nghĩa học thuật không áp dụng cho thế giới thực.
dùng1068

15
Tôi chỉ muốn trích dẫn Brian Kerninghan ở đây: "Mọi người đều biết rằng việc gỡ lỗi khó gấp đôi so với việc viết chương trình ở nơi đầu tiên. Vì vậy, nếu bạn thông minh như bạn có thể khi bạn viết nó, bạn sẽ gỡ lỗi bằng cách nào? " - en.wikiquote.org/wiki/Brian_Kernighan / "Các yếu tố của phong cách lập trình", phiên bản 2, chương 2.
MarkusSchaber

7
@Logister Coolness không còn là mục tiêu chính hơn là sự đơn giản. Sự phản đối ở đây là sự phức tạp vô cớ , là kẻ thù của sự đúng đắn (chắc chắn là mối quan tâm chính) bởi vì nó làm cho mã khó lý luận hơn và có nhiều khả năng chứa các trường hợp góc bất ngờ. Với sự hoài nghi đã nêu trước đây của tôi về tuyên bố rằng nó thực sự dễ kiểm tra hơn, tôi chưa thấy bất kỳ lập luận thuyết phục nào cho phong cách này. Tương tự như quy tắc bảo mật wrt đặc quyền tối thiểu, có lẽ có thể có một quy tắc nói rằng người ta nên cảnh giác khi sử dụng các tính năng ngôn ngữ mạnh mẽ để làm những việc đơn giản.
sdenham

6
Mã của bạn trông giống như mã cơ sở. Tôi mong đợi một cấp cao để viết ví dụ đầu tiên.
sed

Câu trả lời:


322

Trong mã của bạn, bạn đã thực hiện nhiều thay đổi:

  • phá hủy phân công để truy cập các trường trong pageslà một thay đổi tốt.
  • trích xuất các parseFoo()chức năng, vv là một thay đổi có thể tốt.
  • giới thiệu một functor là rất khó hiểu.

Một trong những phần khó hiểu nhất ở đây là cách bạn pha trộn lập trình chức năng và mệnh lệnh. Với functor của bạn, bạn không thực sự chuyển đổi dữ liệu, bạn đang sử dụng nó để chuyển một danh sách có thể thay đổi thông qua các chức năng khác nhau. Điều đó dường như không phải là một sự trừu tượng rất hữu ích, chúng ta đã có các biến cho điều đó. Điều đáng lẽ có thể đã được trừu tượng hóa - chỉ phân tích cú pháp mục đó nếu nó tồn tại - vẫn còn trong mã của bạn một cách rõ ràng, nhưng bây giờ chúng ta phải suy nghĩ xung quanh. Ví dụ, nó hơi không rõ ràng parseFoo(foo)sẽ trả về một hàm. JavaScript không có hệ thống loại tĩnh để thông báo cho bạn liệu điều này có hợp pháp hay không, vì vậy mã đó thực sự dễ bị lỗi mà không có tên tốt hơn ( makeFooParser(foo)?). Tôi không thấy bất kỳ lợi ích trong việc che giấu này.

Thay vào đó, những gì tôi mong đợi sẽ thấy:

if (foo) parseFoo(pages, foo);
if (bars) parseBar(pages, bars);
if (bazes) parseBaz(pages, bazes);
return pages;

Nhưng điều đó cũng không lý tưởng, bởi vì không rõ ràng từ trang web cuộc gọi rằng các mục sẽ được thêm vào danh sách các trang. Nếu thay vào đó, các chức năng phân tích cú pháp là thuần túy và trả về một danh sách (có thể trống) mà chúng ta có thể thêm vào các trang một cách rõ ràng, điều đó có thể tốt hơn:

pages.addAll(parseFoo(foo));
pages.addAll(parseBar(bars));
pages.addAll(parseBaz(bazes));
return pages;

Thêm lợi ích: logic về những việc cần làm khi mục trống hiện đã được chuyển vào các chức năng phân tích cú pháp riêng lẻ. Nếu điều này không phù hợp, bạn vẫn có thể giới thiệu các điều kiện. Khả năng biến đổi của pagesdanh sách hiện được kết hợp thành một chức năng duy nhất, thay vì trải rộng nó qua nhiều cuộc gọi. Tránh các đột biến không cục bộ là một phần lớn hơn nhiều của lập trình chức năng so với trừu tượng với những cái tên ngộ nghĩnh như thế Monad.

Vì vậy, có, mã của bạn là quá thông minh. Hãy áp dụng sự thông minh của bạn không phải để viết mã thông minh, mà là tìm ra những cách thông minh để tránh sự cần thiết cho sự thông minh trắng trợn. Các thiết kế tốt nhất không trông lạ mắt, nhưng nhìn rõ ràng cho bất cứ ai nhìn thấy chúng. Và trừu tượng tốt là có để đơn giản hóa việc lập trình, không phải thêm các lớp bổ sung mà tôi phải gỡ rối trong đầu (ở đây, nhận ra rằng functor tương đương với một biến và có thể được loại bỏ một cách hiệu quả).

Xin vui lòng: nếu nghi ngờ, hãy giữ mã của bạn đơn giản và ngu ngốc (nguyên tắc KISS).


2
Từ quan điểm đối xứng, có gì let pages = Identity(pagesList)khác với parseFoo(foo)? Cho rằng, tôi có lẽ sẽ phải ... {Identity(pagesList), parseFoo(foo), parseBar(bar)}.flatMap(x -> x).
ArTs

8
Cảm ơn bạn đã giải thích rằng có ba biểu thức lambda lồng nhau để thu thập một danh sách được ánh xạ (với con mắt chưa được huấn luyện của tôi) có thể là một chút quá thông minh.
Thorbjørn Ravn Andersen

2
Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
yannis

Có lẽ một phong cách trôi chảy sẽ làm việc tốt trong ví dụ thứ hai của bạn?
dùng1068

225

Nếu bạn nghi ngờ, có lẽ nó quá thông minh! Ví dụ thứ hai giới thiệu độ phức tạp ngẫu nhiên với các biểu thức như foo ? parseFoo(foo) : x => xvà tổng thể mã phức tạp hơn có nghĩa là khó theo dõi hơn.

Lợi ích có mục đích, mà bạn có thể kiểm tra từng khối riêng lẻ, có thể đạt được theo cách đơn giản hơn bằng cách chỉ xâm nhập vào các chức năng riêng lẻ. Và trong ví dụ thứ hai, bạn kết hợp các lần lặp khác nhau, do đó bạn thực sự ít bị cô lập hơn .

Bất kể cảm nhận của bạn về phong cách chức năng nói chung, đây rõ ràng là một ví dụ nơi nó làm cho mã phức tạp hơn.

Tôi tìm thấy một chút tín hiệu cảnh báo ở chỗ bạn liên kết mã đơn giản và đơn giản với "nhà phát triển người mới". Đây là một tâm lý nguy hiểm. Theo kinh nghiệm của tôi thì ngược lại: các nhà phát triển Novice có xu hướng mã quá phức tạp và thông minh, bởi vì nó đòi hỏi nhiều kinh nghiệm hơn để có thể thấy giải pháp đơn giản và rõ ràng nhất.

Lời khuyên chống lại "mã thông minh" không thực sự là liệu mã có sử dụng các khái niệm nâng cao mà người mới có thể không hiểu hay không. Thay vào đó là về việc viết mã phức tạp hoặc phức tạp hơn mức cần thiết . Điều này làm cho mã khó theo dõi hơn đối với tất cả mọi người , người mới và các chuyên gia, và có lẽ cũng cho chính bạn một vài tháng.


156
"Các nhà phát triển Novice có xu hướng mã quá phức tạp và thông minh, bởi vì nó đòi hỏi nhiều kinh nghiệm hơn để có thể thấy giải pháp đơn giản và rõ ràng nhất" không thể đồng ý nhiều hơn với bạn. Câu trả lời tuyệt vời!
Bonifacio

23
Mã quá phức tạp cũng khá thụ động. Bạn đang cố tình tạo mã mà ít người khác có thể đọc hoặc gỡ lỗi dễ dàng ... điều đó có nghĩa là bảo mật công việc cho bạn và hoàn toàn là địa ngục cho những người khác khi bạn vắng mặt. Bạn cũng có thể viết tài liệu kỹ thuật của bạn hoàn toàn bằng tiếng Latin.
Ivan

14
Tôi không nghĩ mã thông minh luôn là một thứ phô trương. Đôi khi nó cảm thấy tự nhiên, và chỉ trông thật lố bịch khi kiểm tra lần thứ hai.

5
Tôi đã loại bỏ các cụm từ về "khoe" vì nó nghe có vẻ phán xét nhiều hơn dự định.
JacquesB

11
@BaileyS - Tôi nghĩ rằng nhấn mạnh tầm quan trọng của việc xem xét mã; những gì cảm thấy tự nhiên và đơn giản đối với người viết mã, đặc biệt là khi dần dần phát triển theo cách đó, có thể dễ dàng có vẻ khó hiểu đối với người đánh giá. Mã sau đó không vượt qua đánh giá cho đến khi tái cấu trúc / viết lại để loại bỏ tích chập.
Myles

21

Câu trả lời này của tôi đến hơi muộn, nhưng tôi vẫn muốn bấm máy. Chỉ vì bạn đang sử dụng các kỹ thuật ES6 mới nhất hoặc sử dụng mô hình lập trình phổ biến nhất không nhất thiết có nghĩa là mã của bạn đúng hơn hoặc mã của thiếu niên đó sai. Lập trình chức năng (hoặc bất kỳ kỹ thuật nào khác) nên được sử dụng khi thực sự cần thiết. Nếu bạn cố gắng tìm cơ hội nhỏ nhất để nhồi nhét các kỹ thuật lập trình mới nhất vào mọi vấn đề, bạn sẽ luôn kết thúc với một giải pháp quá kỹ thuật.

Lùi lại một bước và cố gắng diễn đạt bằng lời về vấn đề bạn đang cố gắng giải quyết trong một giây. Về bản chất, bạn chỉ muốn một hàm addPagesbiến đổi các phần khác nhau apiDatathành một tập hợp các cặp khóa-giá trị, sau đó thêm tất cả chúng vào PagesList.

Và nếu đó là tất cả để có nó, tại sao bận tâm sử dụng identity functionvới ternary operator, hoặc sử dụng functorcho phân tích đầu vào? Bên cạnh đó, tại sao bạn thậm chí nghĩ rằng đó là một cách tiếp cận phù hợp để áp dụng functional programminggây ra tác dụng phụ (bằng cách thay đổi danh sách)? Tại sao tất cả những điều đó, khi tất cả những gì bạn cần chỉ là thế này:

const processFooPages = (foo) => foo ? [['foo', foo]] : [];
const processBarPages = (bar) => bar ? bar.map(page => [page.name, page.data]) : [];
const processBazPages = (baz) => baz ? baz.map(page => [page.id, page.content]) : [];

const addPages = (apiData) => {
  const list = new PagesList();
  const pages = [].concat(
    processFooPages(apiData.pages.foo),
    processBarPages(apiData.pages.arrayOfBars),
    processBazPages(apiData.pages.customBazes)
  );
  pages.forEach(([pageName, pageContent]) => list.addPage(pageName, pageContent));

  return list;
}

(một jsfiddle có thể chạy ở đây )

Như bạn có thể thấy, phương pháp này vẫn sử dụng functional programming, nhưng trong chừng mực. Cũng lưu ý rằng vì cả 3 chức năng biến đổi không gây ra tác dụng phụ nào, chúng rất dễ kiểm tra. Mã trong addPagescũng tầm thường và vô duyên mà người mới hoặc chuyên gia có thể hiểu chỉ bằng một cái liếc mắt.

Bây giờ, so sánh mã này với những gì bạn đã đưa ra ở trên, bạn có thấy sự khác biệt không? Không còn nghi ngờ gì nữa, functional programmingvà cú pháp ES6 rất mạnh mẽ, nhưng nếu bạn xử lý vấn đề sai cách với các kỹ thuật này, bạn sẽ kết thúc với mã thậm chí còn lộn xộn hơn.

Nếu bạn không gặp phải vấn đề và áp dụng đúng kỹ thuật vào đúng chỗ, bạn có thể có mã có chức năng tự nhiên, trong khi vẫn được tổ chức và duy trì bởi tất cả các thành viên trong nhóm. Những đặc điểm này không loại trừ lẫn nhau.


2
+1 để chỉ ra thái độ phổ biến rộng rãi này (không nhất thiết phải áp dụng cho OP): "Chỉ vì bạn đang sử dụng các kỹ thuật ES6 mới nhất hoặc sử dụng mô hình lập trình phổ biến nhất không nhất thiết có nghĩa là mã của bạn đúng hơn, hoặc mã của thiếu niên đó là sai. ".
Giorgio

+1. Chỉ cần một nhận xét nhỏ về phạm vi, bạn có thể sử dụng toán tử lây lan (...) thay vì _.concat để loại bỏ sự phụ thuộc đó.
YoTengoUnLCD

1
@YoTengoUnLCD À, bắt tốt. Bây giờ bạn biết rằng tôi và nhóm của tôi vẫn đang trên hành trình hủy bỏ một số lodashsử dụng của chúng tôi . Mã đó có thể sử dụng spread operatorhoặc thậm chí [].concat()nếu một người muốn giữ nguyên hình dạng của mã.
b0nyb0y

Xin lỗi, nhưng danh sách mã này vẫn còn ít rõ ràng hơn nhiều so với "mã cơ sở" ban đầu trong bài đăng của OP. Về cơ bản: Không bao giờ sử dụng toán tử ternary nếu bạn có thể tránh nó. Nó quá căng thẳng. Trong ngôn ngữ chức năng "thực", câu lệnh if sẽ là biểu thức chứ không phải câu lệnh và do đó dễ đọc hơn.
Olle Härstedt

@ OlleHärstedt Umm, đó là một yêu cầu thú vị mà bạn đã đưa ra. Vấn đề là, mô hình lập trình chức năng, hoặc bất kỳ mô hình nào ngoài đó, không bao giờ bị ràng buộc với bất kỳ ngôn ngữ chức năng "thực" cụ thể nào, ít hơn nhiều cú pháp của nó. Do đó, việc đưa ra những cấu trúc có điều kiện nên hoặc không bao giờ nên được sử dụng chỉ là không có ý nghĩa gì. A ternary operatorcó giá trị như một iftuyên bố thông thường , cho dù bạn có muốn hay không. Cuộc tranh luận về khả năng đọc giữa if-else?:trại là không bao giờ kết thúc, vì vậy tôi sẽ không tham gia vào nó. Tất cả những gì tôi sẽ nói là, với đôi mắt được đào tạo, những dòng như thế này hầu như không "quá căng thẳng".
b0nyb0y

5

Đoạn mã thứ hai không thể kiểm tra hơn đoạn đầu tiên. Sẽ rất đơn giản để thiết lập tất cả các bài kiểm tra cần thiết cho một trong hai đoạn.

Sự khác biệt thực sự giữa hai đoạn trích là tính dễ hiểu. Tôi có thể đọc đoạn trích đầu tiên khá nhanh và hiểu chuyện gì đang xảy ra. Đoạn trích thứ hai, không quá nhiều. Nó ít trực quan hơn, cũng như lâu hơn đáng kể.

Điều đó làm cho đoạn mã đầu tiên dễ bảo trì hơn, đó là chất lượng mã có giá trị. Tôi tìm thấy rất ít giá trị trong đoạn trích thứ hai.


3

TD; DR

  1. Bạn có thể giải thích mã của mình cho Nhà phát triển Junior sau 10 phút hoặc ít hơn không?
  2. Hai tháng kể từ bây giờ, bạn có thể hiểu mã của bạn?

Phân tích chi tiết

Rõ ràng và dễ đọc

Mã ban đầu rất ấn tượng rõ ràng và dễ hiểu cho bất kỳ cấp độ lập trình viên nào. Đó là một phong cách quen thuộc với mọi người .

Khả năng đọc phần lớn dựa trên sự quen thuộc, không phải là một số tính toán toán học của các mã thông báo . IMO, ở giai đoạn này, bạn có quá nhiều ES6 trong bản viết lại của mình. Có lẽ trong một vài năm nữa tôi sẽ thay đổi phần này trong câu trả lời của mình. :-) BTW, tôi cũng thích câu trả lời @ b0nyb0y như một sự thỏa hiệp hợp lý và rõ ràng.

Khả năng kiểm tra

if(apiData.pages.foo){
   pagesList.add('foo', apiData.pages.foo){
}

Giả sử PagesList.add () có các bài kiểm tra, điều này nên, đây hoàn toàn là mã đơn giản và không có lý do rõ ràng nào cho phần này cần thử nghiệm riêng biệt.

if (apiData.pages.arrayOfBars){
      let bars = apiData.pages.arrayOfBars;
      bars.forEach((bar) => {
         pagesList.add(bar.name, bar.data);
      })
   }

Một lần nữa, tôi thấy không cần ngay lập tức cho bất kỳ thử nghiệm riêng biệt đặc biệt nào của phần này. Trừ khi PagesList.add () có vấn đề bất thường với null hoặc trùng lặp hoặc các đầu vào khác.

if (apiData.pages.customBazes) {
      let bazes = apiData.pages.customBazes;
      bazes.forEach((baz) => {
         pagesList.add(customBazParser(baz)); 
      })
   } 

Mã này cũng rất đơn giản. Giả sử đã customBazParserđược thử nghiệm và không trả lại quá nhiều kết quả "đặc biệt". Vì vậy, một lần nữa, trừ khi có những tình huống khó khăn với `PagesList.add (), (có thể là tôi không quen thuộc với tên miền của bạn) Tôi không hiểu tại sao phần này cần thử nghiệm đặc biệt.

Nói chung, kiểm tra toàn bộ chức năng nên hoạt động tốt.

Tuyên bố miễn trừ trách nhiệm : Nếu có những lý do đặc biệt để kiểm tra tất cả 8 khả năng của ba if()tuyên bố, thì có, hãy chia nhỏ các bài kiểm tra. Hoặc, nếu PagesList.add()nhạy cảm, có, chia ra các bài kiểm tra.

Cấu trúc: Có đáng chia thành ba phần (như Gaul)

Ở đây bạn có lý lẽ tốt nhất. Cá nhân, tôi không nghĩ mã gốc là "quá dài" (Tôi không phải là người cuồng SRP). Nhưng, nếu có thêm một vài if (apiData.pages.blah)phần, thì SRP cho rằng cái đầu xấu xí của nó và nó sẽ có giá trị chia tách. Đặc biệt là nếu DRY áp dụng và các chức năng có thể được sử dụng ở những nơi khác của mã.

Một đề nghị của tôi

YMMV. Để lưu một dòng mã và một số logic, tôi có thể kết hợp iflet thành một dòng: vd

let bars = apiData.pages.arrayOfBars || [];
bars.forEach((bar) => {
   pagesList.add(bar.name, bar.data);
})

Điều này sẽ thất bại nếu apiData.pages.arrayOfBars là một Số hoặc Chuỗi, nhưng mã gốc cũng vậy. Và với tôi thì rõ ràng hơn (và một thành ngữ được sử dụng quá mức).

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.