Xử lý việc không biết tên tham số của hàm khi bạn gọi nó


13

Đây là một vấn đề về lập trình / ngôn ngữ mà tôi muốn nghe suy nghĩ của bạn.

Chúng tôi đã phát triển các quy ước mà hầu hết các lập trình viên (nên) tuân theo đó không phải là một phần của cú pháp ngôn ngữ nhưng phục vụ để làm cho mã dễ đọc hơn. Tất nhiên đây luôn là vấn đề tranh luận nhưng ít nhất có một số khái niệm cốt lõi mà hầu hết các lập trình viên đều thấy dễ chịu. Đặt tên cho các biến của bạn một cách thích hợp, đặt tên nói chung, làm cho các dòng của bạn không quá dài, tránh các hàm dài, đóng gói, những thứ đó.

Tuy nhiên, có một vấn đề mà tôi vẫn chưa tìm thấy ai bình luận và đó có thể là vấn đề lớn nhất. Đó là vấn đề của các đối số là ẩn danh khi bạn gọi một hàm.

Các hàm xuất phát từ toán học trong đó f (x) có ý nghĩa rõ ràng vì một hàm có định nghĩa chặt chẽ hơn nhiều mà nó thường làm trong lập trình. Các hàm thuần túy trong toán học có thể làm ít hơn rất nhiều so với lập trình và chúng là một công cụ tao nhã hơn nhiều, chúng thường chỉ lấy một đối số (thường là một số) và chúng luôn trả về một giá trị (cũng thường là một số). Nếu một hàm có nhiều đối số, thì hầu như chúng luôn chỉ là các chiều bổ sung của miền của hàm. Nói cách khác, một đối số không quan trọng hơn các đối số khác. Chúng được sắp xếp rõ ràng, chắc chắn, nhưng ngoài điều đó, chúng không có thứ tự ngữ nghĩa.

Tuy nhiên, trong lập trình, chúng ta có nhiều chức năng xác định tự do hơn và trong trường hợp này tôi cho rằng đó không phải là một điều tốt. Một tình huống phổ biến, bạn có một hàm được định nghĩa như thế này

func DrawRectangleClipped (rectToDraw, fillColor, clippingRect) {}

Nhìn vào định nghĩa, nếu hàm được viết chính xác, nó hoàn toàn rõ ràng những gì. Khi gọi hàm, bạn thậm chí có thể có một số phép thuật hoàn thành mã / intellisense đang diễn ra trong IDE / trình soạn thảo của bạn sẽ cho bạn biết đối số tiếp theo sẽ là gì. Nhưng chờ đã. Nếu tôi cần điều đó khi tôi thực sự viết cuộc gọi, có phải chúng tôi đang thiếu thứ gì ở đây không? Người đọc mã không có lợi ích của IDE và trừ khi họ chuyển sang định nghĩa, họ không biết cái nào trong hai hình chữ nhật được truyền làm đối số được sử dụng cho mục đích gì.

Vấn đề còn đi xa hơn thế. Nếu các đối số của chúng tôi đến từ một số biến cục bộ, có thể có những tình huống mà chúng tôi thậm chí không biết đối số thứ hai là gì vì chúng tôi chỉ nhìn thấy tên biến. Lấy ví dụ dòng mã này

DrawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

Điều này được giảm bớt thành nhiều mức độ khác nhau trong các ngôn ngữ khác nhau nhưng ngay cả trong các ngôn ngữ được nhập chính xác và ngay cả khi bạn đặt tên cho các biến của mình một cách hợp lý, bạn thậm chí không đề cập đến loại biến khi bạn chuyển nó vào hàm.

Vì nó thường là với lập trình, có rất nhiều giải pháp tiềm năng cho vấn đề này. Nhiều người đã thực hiện bằng các ngôn ngữ phổ biến. Các tham số được đặt tên trong C # chẳng hạn. Tuy nhiên, tất cả những gì tôi biết đều có nhược điểm đáng kể. Đặt tên cho mọi tham số trên mỗi lệnh gọi hàm không thể dẫn đến mã có thể đọc được. Nó gần như cảm thấy có thể chúng ta vượt xa các khả năng mà lập trình văn bản đơn giản mang lại cho chúng ta. Chúng tôi đã chuyển từ văn bản CHỈ trong hầu hết các lĩnh vực, nhưng chúng tôi vẫn mã giống nhau. Cần thêm thông tin để được hiển thị trong mã? Thêm văn bản. Dù sao, điều này đang có một chút tiếp tuyến vì vậy tôi sẽ dừng ở đây.

Một câu trả lời tôi nhận được với đoạn mã thứ hai là trước tiên bạn có thể giải nén mảng cho một số biến được đặt tên và sau đó sử dụng các biến đó nhưng tên của biến có thể có nghĩa là nhiều thứ và cách nó được gọi không nhất thiết phải nói cho bạn biết cách nó được sử dụng được giải thích trong ngữ cảnh của hàm được gọi. Trong phạm vi cục bộ, bạn có thể có hai hình chữ nhật có tên trái Hình chữ nhật và Hình chữ nhật bên phải bởi vì đó là những gì chúng đại diện về mặt ngữ nghĩa, nhưng nó không cần phải mở rộng cho những gì chúng thể hiện khi được cung cấp cho một hàm.

Trong thực tế, nếu các biến của bạn được đặt tên theo ngữ cảnh của hàm được gọi hơn là bạn giới thiệu ít thông tin hơn khả năng bạn có thể gọi với hàm đó và ở một mức độ nào đó nếu dẫn đến mã kém hơn. Nếu bạn có một quy trình dẫn đến hình chữ nhật mà bạn lưu trữ trong orthForClipping và sau đó một quy trình khác cung cấp orthForDrawing, thì cuộc gọi thực tế đến DrawRonymousClipped chỉ là nghi thức. Một dòng có nghĩa là không có gì mới và chỉ có ở đó để máy tính biết chính xác những gì bạn muốn mặc dù bạn đã giải thích nó với cách đặt tên của bạn. Đây không phải là một điều tốt.

Tôi thực sự thích nghe những quan điểm mới mẻ về điều này. Tôi chắc chắn tôi không phải là người đầu tiên coi đây là một vấn đề, vậy nó được giải quyết như thế nào?


2
Tôi bối rối về vấn đề chính xác là gì ... dường như có một vài ý tưởng ở đây, không chắc cái nào là điểm chính của bạn.
Thất vọngWithFormsDesigner

1
Các tài liệu cho hàm sẽ cho bạn biết các đối số làm gì. Bạn có thể phản đối rằng ai đó đang đọc mã có thể không có tài liệu, nhưng sau đó không thực sự biết mã đó làm gì và ý nghĩa mà họ trích ra từ việc đọc nó là một phỏng đoán có giáo dục. Trong bất kỳ bối cảnh nào mà người đọc cần biết rằng mã là chính xác, anh ta sẽ cần tài liệu.
Doval

3
@Darwin Trong lập trình chức năng, tất cả các chức năng vẫn chỉ có 1 đối số. Nếu bạn cần truyền "nhiều đối số", tham số thường là một tuple (nếu bạn muốn chúng được đặt hàng) hoặc một bản ghi (nếu bạn không muốn chúng). Ngoài ra, việc tạo các phiên bản chuyên biệt của chức năng bất cứ lúc nào là rất nhỏ, do đó bạn có thể giảm số lượng đối số cần thiết. Vì hầu như mọi ngôn ngữ chức năng đều cung cấp cú pháp cho các bộ dữ liệu và bản ghi, nên việc đóng gói các giá trị là không gây đau đớn và bạn có được thành phần miễn phí (bạn có thể xâu chuỗi các hàm trả về các bộ dữ liệu với các bộ dữ liệu.)
Doval

1
@Bergi Mọi người có xu hướng khái quát hóa nhiều hơn trong FP thuần túy, vì vậy tôi nghĩ rằng các chức năng thường nhỏ hơn và nhiều hơn. Tôi có thể được cách mặc dù. Tôi không có nhiều kinh nghiệm làm việc trong các dự án thực tế với Haskell và băng đảng.
Darwin

4
Tôi nghĩ câu trả lời là "Đừng đặt tên cho biến của bạn là 'deserializedArray'"?
whatsisname

Câu trả lời:


10

Tôi đồng ý rằng cách các hàm thường được sử dụng có thể là một phần khó hiểu trong việc viết mã và đặc biệt là đọc mã.

Câu trả lời cho vấn đề này một phần phụ thuộc vào ngôn ngữ. Như bạn đã đề cập, C # có tên tham số. Giải pháp của Objective-C cho vấn đề này liên quan đến các tên phương thức mô tả nhiều hơn. Ví dụ, stringByReplacingOccurrencesOfString:withString:là một phương thức với các tham số rõ ràng.

Trong Groovy, một số hàm lấy bản đồ, cho phép cú pháp như sau:

restClient.post(path: 'path/to/somewhere',
            body: requestBody,
            requestContentType: 'application/json')

Nói chung, bạn có thể giải quyết vấn đề này bằng cách giới hạn số lượng tham số bạn truyền cho một hàm. Tôi nghĩ 2-3 là một giới hạn tốt. Nếu có vẻ như một hàm cần nhiều tham số hơn, nó khiến tôi nghĩ lại thiết kế. Nhưng, điều này có thể khó trả lời nói chung. Đôi khi bạn đang cố gắng làm quá nhiều trong một chức năng. Đôi khi nó có ý nghĩa để xem xét một lớp để lưu trữ các tham số của bạn. Ngoài ra, trong thực tế, tôi thường thấy rằng các hàm có số lượng lớn các tham số thường có nhiều trong số chúng là tùy chọn.

Ngay cả trong một ngôn ngữ như Objective-C, việc giới hạn số lượng tham số là hợp lý. Một lý do là nhiều tham số là tùy chọn. Để biết ví dụ, hãy xem RangeOfString: và các biến thể của nó trong NSString .

Một mẫu mà tôi thường sử dụng trong Java là sử dụng một lớp kiểu thông thạo làm tham số. Ví dụ:

something.draw(new Box().withHeight(5).withWidth(20))

Điều này sử dụng một lớp làm tham số và với một lớp kiểu thông thạo, làm cho mã dễ đọc.

Đoạn mã Java ở trên cũng giúp việc sắp xếp thứ tự các tham số có thể không rõ ràng. Chúng ta thường giả sử với tọa độ rằng X xuất hiện trước Y. Và tôi thường thấy chiều cao trước chiều rộng là quy ước, nhưng điều đó vẫn không rõ ràng ( something.draw(5, 20)).

Tôi cũng đã thấy một số chức năng như drawWithHeightAndWidth(5, 20)nhưng ngay cả những chức năng này không thể có quá nhiều tham số hoặc bạn sẽ bắt đầu mất khả năng đọc.


2
Thứ tự, nếu bạn tiếp tục trên ví dụ Java, thực sự có thể rất khó khăn. Ví dụ, so sánh các hàm tạo sau từ awt: Dimension(int width, int height)GridLayout(int rows, int cols)(số lượng hàng là chiều cao, nghĩa là GridLayoutcó chiều cao trước và Dimensionchiều rộng).
Pierre Arlaud

1
Sự không nhất quán như vậy cũng đã bị chỉ trích rất nhiều với PHP ( eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design ), ví dụ: array_filter($input, $callback)so với array_map($callback, $input), strpos($haystack, $needle)so vớiarray_search($needle, $haystack)
Pierre Arlaud

12

Chủ yếu là nó được giải quyết bằng cách đặt tên tốt cho các hàm, tham số và đối số. Bạn đã khám phá điều đó và thấy nó có thiếu sót, tuy nhiên. Hầu hết các thiếu sót đó được giảm thiểu bằng cách giữ các hàm nhỏ, với một số lượng nhỏ các tham số, cả trong ngữ cảnh gọi và ngữ cảnh được gọi. Ví dụ cụ thể của bạn có vấn đề vì chức năng bạn đang gọi đang cố gắng thực hiện một số việc cùng một lúc: chỉ định hình chữ nhật cơ sở, chỉ định vùng cắt, vẽ và tô màu bằng một màu cụ thể.

Điều này giống như cố gắng viết một câu chỉ sử dụng các tính từ. Đặt thêm động từ (lời gọi hàm) vào đó, tạo chủ ngữ (đối tượng) cho câu của bạn và dễ đọc hơn:

rect.clip(clipRect).fill(color)

Ngay cả khi clipRectcolorcó những cái tên khủng khiếp (và chúng không nên), bạn vẫn có thể phân biệt các loại của chúng khỏi bối cảnh.

Ví dụ giải trừ của bạn là có vấn đề bởi vì bối cảnh gọi đang cố gắng làm quá nhiều cùng một lúc: khử lưu huỳnh và vẽ một cái gì đó. Bạn cần chỉ định tên có ý nghĩa và phân tách rõ ràng hai trách nhiệm. Ở mức tối thiểu, một cái gì đó như thế này:

(rect, clipRect, color) = deserializeClippedRect()
rect.clip(clipRect).fill(color)

Rất nhiều vấn đề về khả năng đọc được gây ra bằng cách cố gắng quá ngắn gọn, bỏ qua các giai đoạn trung gian mà con người yêu cầu để phân biệt bối cảnh và ngữ nghĩa.


1
Tôi thích ý tưởng xâu chuỗi nhiều lời gọi hàm để làm rõ nghĩa, nhưng đó không phải chỉ là vấn đề xoay quanh vấn đề sao? Về cơ bản, đó là "Tôi muốn viết một câu, nhưng ngôn ngữ tôi đang kiện sẽ không cho phép tôi nên tôi chỉ có thể sử dụng từ tương đương gần nhất"
Darwin

@Darwin IMHO không như thế này có thể được cải thiện bằng cách làm cho ngôn ngữ lập trình giống ngôn ngữ tự nhiên hơn. Ngôn ngữ tự nhiên rất mơ hồ và chúng ta chỉ có thể hiểu chúng trong ngữ cảnh và thực sự không bao giờ có thể chắc chắn. Các cuộc gọi hàm chuỗi là cách tốt hơn vì mọi thuật ngữ (lý tưởng) có tài liệu và các nguồn có sẵn và chúng tôi có dấu ngoặc đơn và dấu chấm làm cho cấu trúc rõ ràng.
maaartinus

3

Trong thực tế, nó được giải quyết bằng thiết kế tốt hơn. Điều đặc biệt là các hàm được viết tốt sẽ có nhiều hơn 2 đầu vào và khi nó xảy ra, sẽ rất hiếm khi nhiều đầu vào đó không thể được tổng hợp thành một số gói gắn kết. Điều này giúp dễ dàng chia nhỏ các hàm hoặc tổng hợp các tham số để bạn không làm cho hàm quá nhiều. Một nó có hai đầu vào, nó trở nên dễ đặt tên và rõ ràng hơn nhiều về đầu vào nào.

Ngôn ngữ đồ chơi của tôi có khái niệm cụm từ để giải quyết vấn đề này và các ngôn ngữ lập trình tập trung vào ngôn ngữ tự nhiên khác đã có những cách tiếp cận khác để đối phó với nó, nhưng tất cả chúng đều có xu hướng có những nhược điểm khác. Thêm vào đó, các cụm từ thậm chí còn ít hơn một cú pháp hay xung quanh việc làm cho các hàm có tên tốt hơn. Sẽ luôn khó để tạo ra một tên hàm tốt khi cần một loạt các đầu vào.


Các cụm từ thực sự có vẻ như một bước tiến. Tôi biết một số ngôn ngữ có khả năng tương tự nhưng đó là FAR từ phổ biến. Chưa kể, với tất cả sự căm ghét vĩ mô đến từ những người theo chủ nghĩa thuần túy C (++) không bao giờ sử dụng macro được thực hiện đúng, chúng ta có thể không bao giờ có các tính năng như thế này trong các ngôn ngữ phổ biến.
Darwin

Chào mừng bạn đến với chủ đề chung về các ngôn ngữ dành riêng cho tên miền , một điều tôi thực sự mong muốn nhiều người sẽ hiểu được lợi thế của ... (+1)
Izkata

2

Ví dụ, trong Javascript (hoặc ECMAScript ), nhiều lập trình viên đã quen với

truyền tham số dưới dạng tập hợp các thuộc tính đối tượng được đặt tên trong một đối tượng ẩn danh.

Và như một thực hành lập trình, nó đã nhận được từ các lập trình viên đến thư viện của họ và từ đó đến các lập trình viên khác, những người đã phát triển thích nó và sử dụng nó và viết thêm một số thư viện, v.v.

Thí dụ

Thay vì gọi

function drawRectangleClipped (rectToDraw, fillColor, clippingRect)

như thế này:

drawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

, đó là một phong cách hợp lệ và chính xác, bạn gọi

function drawRectangleClipped (params)

như thế này:

drawRectangleClipped({
    rectToDraw: deserializedArray[0], 
    fillColor: deserializedArray[1], 
    clippingRect: deserializedArray[2]
})

, đó là hợp lệ và chính xác tốt đẹp liên quan đến câu hỏi của bạn.

Dĩ nhiên, phải có điều kiện phù hợp cho việc này - trong Javascript, điều này khả thi hơn nhiều so với trong C. Trong javascript, điều này thậm chí đã sinh ra ký hiệu cấu trúc được sử dụng rộng rãi ngày càng phổ biến như một đối trọng nhẹ hơn với XML. Nó được gọi là JSON (bạn có thể đã nghe về nó).


Tôi không biết đủ về ngôn ngữ đó để xác minh cú pháp, nhưng, tôi rất thích bài viết này. Có vẻ khá thanh lịch. +1
CNTT Alex

Rất thường xuyên, điều này được kết hợp với các đối số bình thường, nghĩa là có 1-3 đối số theo sau params(thường chứa các đối số tùy chọn và thường là tùy chọn), ví dụ như hàm này . Điều này làm cho các hàm có nhiều đối số khá dễ nắm bắt (trong ví dụ của tôi có 2 đối số bắt buộc và 6 đối số tùy chọn).
maaartinus

0

Bạn nên sử dụng object-C, đây là một định nghĩa hàm:

- (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject

Và ở đây nó được sử dụng:

[someObject performSelector:someSelector withObject:someObject2 withObject:someObject3];

Tôi nghĩ rằng ruby ​​có cấu trúc tương tự và bạn có thể mô phỏng chúng bằng các ngôn ngữ khác với danh sách khóa-giá trị.

Đối với các hàm phức tạp trong Java, tôi muốn định nghĩa các biến giả trong từ ngữ hàm. Ví dụ bên trái của bạn:

Rectangle referenceRectangle = leftRectangle;
Rectangle targetRectangle = rightRectangle;
doSomeWeirdStuffWithRectangles(referenceRectangle, targetRectangle);

Có vẻ như mã hóa nhiều hơn, nhưng ví dụ bạn có thể sử dụng leftRonymous và sau đó cấu trúc lại mã sau với "Trích xuất biến cục bộ" nếu bạn nghĩ rằng nó sẽ không thể hiểu được đối với người duy trì mã trong tương lai, có thể không phải là bạn.


Về ví dụ Java đó, tôi đã viết trong câu hỏi tại sao tôi nghĩ rằng nó không phải là một giải pháp tốt. Bạn nghĩ gì về điều này?
Darwin

0

Cách tiếp cận của tôi là tạo các biến cục bộ tạm thời - nhưng không chỉ gọi chúng LeftRectangeRightRectangle. Thay vào đó, tôi sử dụng tên dài hơn một chút để truyền đạt ý nghĩa hơn. Tôi thường cố gắng phân biệt tên càng nhiều càng tốt, ví dụ như không gọi cả hai something_rectangle, nếu vai trò của chúng không đối xứng nhau.

Ví dụ (C ++):

auto& connector_source = deserializedArray[0]; 
auto& connector_target = deserializedArray[1]; 
auto& bounding_box = deserializedArray[2]; 
DoWeirdThing(connector_source, connector_target, bounding_box)

và tôi thậm chí có thể viết một hàm hoặc mẫu trình bao bọc một lớp lót:

template <typename T1, typename T2, typename T3>
draw_bounded_connector(
    T1& connector_source, T2& connector_target,const T3& bounding_box) 
{
    DoWeirdThing(connector_source, connector_target, bounding_box)
}

(bỏ qua ký hiệu nếu bạn không biết C ++).

Nếu hàm thực hiện một số điều kỳ lạ không có mô tả hay - thì có lẽ nó cần phải được cấu trúc lại!

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.