Hàm một dòng chỉ được gọi một lần


120

Hãy xem xét một hàm không tham số ( chỉnh sửa: không nhất thiết) thực hiện một dòng mã duy nhất và chỉ được gọi một lần trong chương trình (mặc dù không thể không cần thiết lại trong tương lai).

Nó có thể thực hiện một truy vấn, kiểm tra một số giá trị, làm một cái gì đó liên quan đến regex ... bất cứ điều gì tối nghĩa hoặc "hacky".

Lý do đằng sau điều này sẽ là để tránh những đánh giá khó đọc:

if (getCondition()) {
    // do stuff
}

nơi getCondition()là hàm một dòng.

Câu hỏi của tôi chỉ đơn giản là: đây có phải là một thực hành tốt? Nó có vẻ ổn với tôi nhưng tôi không biết về lâu dài ...


9
Trừ khi đoạn mã của bạn là từ ngôn ngữ OOP với máy thu ẩn (ví dụ: điều này) chắc chắn sẽ là một thực tiễn tồi, vì getCondition () có lẽ phụ thuộc vào trạng thái toàn cầu ...
Ingo

2
Anh ta có thể ngụ ý rằng getCondition () có thể có đối số?
dùng606723

24
@Ingo - một số thứ thực sự có trạng thái Toàn cầu. Thời gian hiện tại, tên máy chủ, số cổng, v.v ... đều là "Quả cầu" hợp lệ. Lỗi thiết kế đang tạo ra một Toàn cầu từ một cái gì đó vốn không phải là toàn cầu.
James Anderson

1
Tại sao bạn không chỉ nội tuyến getCondition? Nếu nó nhỏ và được sử dụng không thường xuyên như bạn nói thì việc đặt tên cho nó là không hoàn thành bất cứ điều gì.
davidk01

12
davidk01: Mã dễ đọc.
wjl

Câu trả lời:


240

Phụ thuộc vào một dòng. Nếu dòng có thể đọc và súc tích bằng chính nó, chức năng có thể không cần thiết. Ví dụ đơn giản:

void printNewLine() {
  System.out.println();
}

OTOH, nếu hàm đặt tên tốt cho một dòng mã chứa ví dụ: một biểu thức phức tạp, khó đọc, thì nó hoàn toàn hợp lý (với tôi). Ví dụ có sẵn (được chia thành nhiều dòng để dễ đọc ở đây):

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isFemale() 
        && (taxPayer.getNumberOfChildren() > 2 
        || (taxPayer.getAge() > 50 && taxPayer.getEmployer().isNonProfit()));
}

99
+1. Từ ma thuật ở đây là "Mã tự ghi".
Konamiman

8
Một ví dụ điển hình về điều mà chú Bob sẽ gọi là "gói gọn các điều kiện".
Anthony Pegram

12
@Aditya: Không có gì nói rằng đó taxPayerlà toàn cầu trong kịch bản này. Có lẽ lớp này là TaxReturn, và taxPayerlà một thuộc tính.
Adam Robinson

2
Ngoài ra, nó cho phép bạn ghi lại chức năng theo cách có thể được chọn bằng ví dụ javadoc và hiển thị công khai.

2
@dallin, Clean Code của Bob Martin cho thấy rất nhiều ví dụ về mã bán thực. Nếu bạn có quá nhiều chức năng trong một lớp, có lẽ lớp của bạn quá lớn?
Péter Török

67

Vâng, điều này có thể được sử dụng để đáp ứng thực tiễn tốt nhất. Ví dụ, tốt hơn là có một hàm được đặt tên rõ ràng thực hiện một số công việc, ngay cả khi nó chỉ dài một dòng, hơn là để dòng mã đó trong một hàm lớn hơn và cần một nhận xét một dòng giải thích những gì nó làm. Ngoài ra, các dòng mã lân cận sẽ thực hiện các tác vụ ở cùng mức độ trừu tượng. Một ví dụ sẽ là một cái gì đó như

startIgnition();
petrolFlag |= 0x006A;
engageChoke();

Trong trường hợp này, chắc chắn tốt hơn là chuyển đường giữa thành một hàm có tên hợp lý.


15
Đó là 0x006A rất đáng yêu: DA nổi tiếng về nhiên liệu carbon hóa với các chất phụ gia a, b và c.
Coder

2
+1 Tôi là tất cả cho mã tự viết tài liệu :) nhân tiện, tôi nghĩ rằng tôi đồng ý giữ mức độ trừu tượng không đổi thông qua các khối, nhưng tôi không thể giải thích tại sao. Bạn có muốn mở rộng điều đó?
vemv

3
Nếu bạn chỉ nói rằng petrolFlag |= 0x006A;không có bất kỳ loại quyết định nào, tốt hơn là chỉ cần nói petrolFlag |= A_B_C; mà không có chức năng bổ sung. Có lẽ, engageChoke()chỉ nên được gọi nếu petrolFlagđáp ứng một tiêu chí nhất định và điều đó sẽ nói rõ ràng 'Tôi cần một chức năng ở đây.' Chỉ là một nit nhỏ, câu trả lời này về cơ bản là tại chỗ :)
Tim Post

9
+1 để chỉ ra rằng mã trong một phương thức phải ở cùng mức trừu tượng. @vemv, đó là một thực hành tốt vì nó làm cho mã dễ hiểu hơn, bằng cách tránh sự cần thiết của bạn để nhảy lên xuống giữa các mức độ trừu tượng khác nhau trong khi đọc mã. Việc nhập mức độ trừu tượng chuyển sang các cuộc gọi / trả về phương thức (nghĩa là "nhảy" cấu trúc) là một cách hay để làm cho mã trôi chảy và sạch sẽ hơn.
Péter Török

21
Không, đó là một giá trị hoàn toàn. Nhưng thực tế mà bạn tự hỏi về nó chỉ nhấn mạnh điểm: nếu mã đã nói mixLeadedGasoline(), bạn sẽ không phải làm thế!
Kilian Foth

37

Tôi nghĩ rằng trong nhiều trường hợp, hàm như vậy là kiểu tốt, nhưng bạn có thể coi biến boolean cục bộ là thay thế trong trường hợp khi bạn không cần sử dụng điều kiện này ở đâu đó ở những nơi khác, ví dụ:

bool someConditionSatisfied = [complex expression];

Điều này sẽ đưa ra một gợi ý cho trình đọc mã và tiết kiệm từ việc giới thiệu một chức năng mới.


1
Bool đặc biệt có lợi nếu tên hàm điều kiện sẽ khó hoặc có thể gây hiểu nhầm (ví dụ IsEngineReadyUnlessItIsOffOrBusyOrOutOfService).
dbkk

2
Tôi đã trải nghiệm lời khuyên này một ý tưởng tồi: bool điều kiện đánh lạc hướng sự chú ý từ hoạt động kinh doanh cốt lõi của chức năng. Ngoài ra, một kiểu thích các biến cục bộ làm cho việc tái cấu trúc khó hơn.
Sói

1
@Wolf OTOH, tôi thích điều này hơn với một lệnh gọi hàm để giảm "độ sâu trừu tượng" của mã. IMO, nhảy vào một hàm là một chuyển đổi ngữ cảnh lớn hơn so với một vài dòng mã rõ ràng và trực tiếp, đặc biệt nếu nó chỉ trả về một boolean.
Kache

@Kache Tôi nghĩ nó phụ thuộc vào việc bạn mã hóa hướng đối tượng hay không, trong trường hợp OOP, việc sử dụng hàm thành viên giữ cho thiết kế linh hoạt hơn nhiều. Nó thực sự phụ thuộc vào bối cảnh ...
Wolf

23

Ngoài câu trả lời của Peter , nếu điều kiện đó có thể cần được cập nhật tại một thời điểm nào đó trong tương lai, bằng cách đóng gói theo cách bạn đề xuất, bạn sẽ chỉ có một điểm chỉnh sửa duy nhất.

Theo ví dụ của Peter, nếu điều này

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isFemale() 
        && (taxPayer.getNumberOfChildren() > 2 
        || (taxPayer.getAge() > 50 && taxPayer.getEmployer().isNonProfit()));
}

trở thành cái này

boolean isTaxPayerEligibleForTaxRefund() {
  return taxPayer.isMutant() 
        && (taxPayer.getNumberOfThumbs() > 2 
        || (taxPayer.getAge() > 123 && taxPayer.getEmployer().isXMan()));
}

Bạn thực hiện một chỉnh sửa duy nhất và nó được cập nhật toàn cầu. Bảo trì khôn ngoan, đây là một lợi thế.

Hiệu suất khôn ngoan, hầu hết các trình biên dịch tối ưu hóa sẽ loại bỏ lệnh gọi hàm và nội tuyến khối mã nhỏ. Tối ưu hóa một cái gì đó như thế này thực sự có thể thu nhỏ kích thước khối (bằng cách xóa các hướng dẫn cần thiết cho chức năng gọi, trả lại, v.v.) để thông thường an toàn ngay cả trong các điều kiện có thể ngăn chặn nội tuyến.


2
Tôi đã nhận thức được lợi ích của việc giữ một điểm chỉnh sửa duy nhất - đó là lý do tại sao câu hỏi nói "... được gọi một lần". Tuy nhiên, thật tuyệt khi biết về những tối ưu hóa trình biên dịch đó, tôi luôn nghĩ rằng họ tuân theo loại hướng dẫn này theo nghĩa đen.
vemv

Việc tách ra một phương thức để kiểm tra một điều kiện đơn giản có xu hướng ngụ ý rằng một điều kiện có thể được thay đổi bằng cách thay đổi phương thức. Hàm ý như vậy rất hữu ích khi nó đúng, nhưng có thể nguy hiểm khi không. Ví dụ, giả sử rằng mã cần phân chia mọi thứ thành các vật nhẹ, vật nặng màu xanh lá cây và vật không nặng màu xanh lá cây. Các đối tượng có thuộc tính "dường như" nhanh chóng, đáng tin cậy cho các vật nặng, nhưng có thể trả về dương tính giả với các vật nhẹ; chúng cũng có chức năng "đoSpectralResponse" chậm, nhưng sẽ hoạt động đáng tin cậy cho tất cả các đối tượng.
supercat

Thực tế seemsGreensẽ không đáng tin cậy với các đối tượng nhẹ sẽ không liên quan nếu mã không bao giờ quan tâm liệu các đối tượng nhẹ có màu xanh lá cây hay không. Tuy nhiên, nếu định nghĩa về "nhẹ" thay đổi, do đó, một số vật thể không có màu xanh lá cây trở lại đúng với giá trị seemsGreenkhông phải là "nhẹ", thì việc thay đổi định nghĩa "nhẹ" có thể phá vỡ mã kiểm tra các đối tượng là màu xanh lá cây". Trong một số trường hợp, việc kiểm tra tính chất xanh và trọng lượng cùng nhau trong mã có thể làm cho mối quan hệ giữa các thử nghiệm rõ ràng hơn nếu chúng là các phương pháp riêng biệt.
supercat

5

Ngoài khả năng đọc (hoặc bổ sung cho nó), điều này cho phép viết các hàm ở mức độ trừu tượng thích hợp.


1
Tôi sợ rằng tôi không hiểu những gì bạn có nghĩa là ...
vemv

1
@vemv: Tôi nghĩ rằng anh ta có nghĩa là "mức độ trừu tượng thích hợp" mà bạn không kết thúc với mã có mức độ trừu tượng khác nhau (nghĩa là những gì Kilian Foth đã nói). Bạn không muốn mã của mình xử lý các chi tiết dường như không đáng kể (ví dụ: sự hình thành của tất cả các trái phiếu trong nhiên liệu) khi mã xung quanh đang xem xét một cái nhìn cấp cao hơn về tình huống trong tay (ví dụ như chạy một động cơ). Cái trước làm mờ cái sau và làm cho mã của bạn không dễ đọc và duy trì vì bạn sẽ phải xem xét tất cả các mức độ trừu tượng cùng một lúc bất cứ lúc nào.
Egon

4

Nó phụ thuộc. Đôi khi, tốt hơn là gói gọn một biểu thức trong một hàm / phương thức, ngay cả khi đó chỉ là một dòng. Nếu nó phức tạp để đọc hoặc bạn cần nó ở nhiều nơi, thì tôi coi đó là một thực hành tốt. Về lâu dài, việc bảo trì sẽ dễ dàng hơn, vì bạn đã giới thiệu một điểm thay đổi duy nhất và khả năng đọc tốt hơn .

Tuy nhiên, đôi khi nó chỉ là thứ bạn không cần. Khi biểu thức dễ đọc dù sao và / hoặc chỉ xuất hiện ở một nơi, thì đừng bao bọc nó.


3

Tôi nghĩ rằng nếu bạn chỉ có một vài trong số họ thì không sao nhưng vấn đề xuất hiện khi có rất nhiều trong số họ trong mã của bạn. Và khi trình biên dịch chạy hoặc bộ điều khiển (tùy thuộc vào ngôn ngữ bạn sử dụng) Nó sẽ chuyển đến chức năng đó trong bộ nhớ. Vì vậy, giả sử bạn có 3 người trong số họ Tôi không nghĩ máy tính sẽ nhận thấy nhưng nếu bạn bắt đầu có 100 trong số những điều nhỏ nhặt đó thì hệ thống phải đăng ký các chức năng trong bộ nhớ chỉ được gọi một lần và sau đó không bị phá hủy.


Theo câu trả lời của Stephen, điều này có thể không phải lúc nào cũng xảy ra (mặc dù tin tưởng một cách mù quáng vào phép thuật của trình biên dịch dù sao cũng không thể tốt hơn)
vemv

1
vâng, nó sẽ bị xóa tùy thuộc vào rất nhiều sự thật. Nếu đó là ngôn ngữ được xen kẽ, hệ thống sẽ rơi vào các chức năng dòng đơn mọi lúc trừ khi bạn cài đặt một cái gì đó cho bộ đệm mà vẫn không thể thực hiện được. Bây giờ khi nói đến trình biên dịch, nó chỉ là vấn đề vào ngày yếu và sự xoa dịu của các máy bay nếu trình biên dịch sẽ là bạn của bạn và dọn dẹp mớ hỗn độn nhỏ của bạn hoặc nếu nó sẽ nghĩ bạn thực sự cần nó. Tôi xin lưu ý rằng nếu bạn biết chính xác số lần lặp thì vòng lặp sẽ luôn chạy tốt hơn một vài lần để sao chép và dán nó nhiều lần sau đó lặp lại.
WojonsTech

+1 để căn chỉnh các hành tinh :) nhưng câu cuối cùng của bạn nghe có vẻ hoàn toàn khó hiểu với tôi, bạn có thực sự làm điều đó không?
vemv

Nó thực sự phụ thuộc Hầu hết thời gian không tôi không trừ khi tôi nhận được số tiền thanh toán chính xác để kiểm tra xem nó có làm giảm tốc độ và những thứ khác như thế không. Nhưng trong các trình biên dịch cũ, tốt hơn là sao chép và dán nó sau đó rời khỏi trình biên dịch để tìm ra 9/10.
WojonsTech

3

Gần đây tôi đã thực hiện điều này chính xác trong một ứng dụng mà tôi đã tái cấu trúc, để làm rõ ý nghĩa thực sự của mã nhận xét sans:

protected void PaymentButton_Click(object sender, EventArgs e)
    Func<bool> HaveError = () => lblCreditCardError.Text == string.Empty && lblDisclaimer.Text == string.Empty;

    CheckInputs();

    if(HaveError())
        return;

    ...
}

Chỉ tò mò, ngôn ngữ đó là gì?
vemv

5
@vemv: Hình như C # với tôi.
Scott Mitchell

Tôi cũng thích nhận dạng thêm hơn bình luận. Nhưng điều này thực sự khác với việc giới thiệu một biến cục bộ để giữ ifngắn? Cách tiếp cận cục bộ (lambda) này làm cho PaymentButton_Clickhàm (nói chung) khó đọc hơn. Trong lblCreditCardErrorví dụ của bạn dường như là một thành viên, do đó, cũng HaveErrorlà một vị từ (riêng tư) có giá trị cho đối tượng. Tôi có xu hướng hạ thấp điều này, nhưng tôi không phải là lập trình viên C #, vì vậy tôi chống lại.
Sói

@Wolf Này anh bạn, ừ. Tôi đã viết điều này khá lâu trước đây :) Tôi chắc chắn làm mọi thứ khá khác ngày nay. Trên thực tế, nhìn vào nội dung nhãn để xem có lỗi nào khiến tôi co rúm lại không ... Tại sao tôi không trả lại một bool từ CheckInputs()???
joshperry

0

Di chuyển một dòng đó thành một phương thức có tên tốt giúp mã của bạn dễ đọc hơn. Nhiều người khác đã đề cập rằng ("mã tự ghi"). Ưu điểm khác của việc chuyển nó thành một phương thức là nó giúp cho việc kiểm tra đơn vị dễ dàng hơn. Khi nó được phân lập thành phương thức riêng và đơn vị được kiểm tra, bạn có thể chắc chắn rằng nếu / khi phát hiện ra lỗi, nó sẽ không nằm trong phương thức này.


0

Đã có rất nhiều câu trả lời hay, nhưng có một trường hợp đặc biệt đáng nói .

Nếu câu lệnh một dòng của bạn cần một nhận xét và bạn có thể xác định rõ ràng (có nghĩa là: tên) mục đích của nó, hãy xem xét trích xuất một hàm trong khi nâng cao nhận xét vào tài liệu API. Bằng cách này bạn thực hiện chức năng gọi dễ dàng nhanh hơn và dễ hiểu hơn.

Thật thú vị, điều tương tự có thể được thực hiện nếu hiện tại không có gì để làm, nhưng một nhận xét nhắc nhở về việc mở rộng cần thiết (trong tương lai rất gần 1) ), vì vậy,

def sophisticatedHello():
    # todo set up
    say("hello")
    # todo tear down

cũng có thể thay đổi thành này

def sophisticatedHello():
    setUp()
    say("hello")
    tearDown()

1) bạn nên thực sự chắc chắn về điều này (xem nguyên tắc YAGNI )


0

Nếu ngôn ngữ hỗ trợ nó, tôi thường sử dụng các hàm ẩn danh có nhãn để thực hiện việc này.

someCondition = lambda p: True if [complicated expression involving p] else False
#I explicitly write the function with a ternary to make it clear this is a a predicate
if (someCondition(p)):
    #do stuff...

IMHO đây là một sự thỏa hiệp tốt vì nó mang lại cho bạn lợi ích dễ đọc khi không có biểu thức phức tạp làm lộn xộn ifđiều kiện trong khi tránh làm lộn xộn không gian tên toàn cầu / gói với các nhãn nhỏ. Nó có thêm lợi ích là chức năng "định nghĩa" là đúng nơi nó được sử dụng giúp dễ dàng sửa đổi và đọc định nghĩa.

Nó không phải chỉ là chức năng vị ngữ. Tôi cũng thích đặt các nồi hơi lặp đi lặp lại trong các chức năng nhỏ như thế này (Nó hoạt động đặc biệt tốt cho việc tạo danh sách pythonic mà không làm lộn xộn cú pháp ngoặc). Ví dụ, ví dụ đơn giản hóa sau đây khi làm việc với PIL trong python

#goal - I have a list of PIL Image objects and I want them all as grayscale (uint8) numpy arrays
im_2_arr = lambda im: array(im.convert('L')) 
arr_list = [im_2_arr(image) for image in image_list]

Tại sao "lambda p: Đúng nếu [biểu thức phức tạp liên quan đến p] khác Sai" thay vì "lambda p: [biểu thức phức tạp liên quan đến p]"? 8-)
Hans-Peter Störr

@hstoerr của nó trong bình luận ngay bên dưới dòng đó. Tôi muốn làm cho nó được biết rõ rằng chúng tôi someCondition là một vị ngữ. Mặc dù nó hoàn toàn không cần thiết, tôi viết rất nhiều kịch bản khoa học được đọc bởi những người không viết mã nhiều như vậy, cá nhân tôi nghĩ rằng nó phù hợp hơn để có thêm sự căng thẳng hơn là khiến các đồng nghiệp của tôi bối rối vì họ không biết điều đó [] == Falsehoặc một số khác tương đương pythonic tương tự mà không 'luôn luôn' trực quan. Về cơ bản, đây là một cách để gắn cờ rằng someCondition trên thực tế là một vị ngữ.
crasic

Chỉ để xóa lỗi sai rõ ràng của tôi [] != Falsenhưng []là Sai khi được chuyển sang bool
crasic

@crasic: khi tôi không mong độc giả của mình biết rằng [] đánh giá là Sai, tôi thích làm len([complicated expression producing a list]) == 0hơn là sử dụng True if [blah] else Falsemà vẫn đòi hỏi người đọc phải biết rằng [] đánh giá là Sai.
Lie Ryan
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.