Thực hành tốt nhất - Gói nếu xung quanh chức năng gọi vs Thêm thoát sớm nếu bảo vệ trong chức năng


9

Tôi biết điều này có thể rất cụ thể trong trường hợp sử dụng, nhưng tôi thấy mình tự hỏi điều này quá thường xuyên. Có một cú pháp thường được ưa thích.

Tôi không hỏi cách tiếp cận tốt nhất là gì khi trong một chức năng, tôi đang hỏi tôi nên thoát sớm hay tôi không nên gọi chức năng đó.

Bao bọc nếu xung quanh chức năng gọi


if (shouldThisRun) {
  runFunction();
}

nếu ( bảo vệ ) trong chức năng

runFunction() {
  if (!shouldThisRun) return;
}

Tùy chọn thứ hai rõ ràng có khả năng giảm trùng lặp mã nếu hàm này được gọi nhiều lần, nhưng đôi khi cảm thấy sai khi thêm nó vào đây vì khi đó bạn có thể mất đi trách nhiệm đơn lẻ của hàm.


Đây là một ví dụ

Nếu tôi có một hàm updateStatus () chỉ đơn giản là cập nhật trạng thái của một cái gì đó. Tôi chỉ muốn trạng thái được cập nhật nếu trạng thái đã thay đổi. Tôi biết các địa điểm trong mã của mình nơi trạng thái có khả năng thay đổi và tôi biết các địa điểm khác nơi nó đã thay đổi một cách thách thức.

Tôi không chắc đó có phải là tôi không nhưng tôi cảm thấy hơi bẩn khi kiểm tra chức năng bên trong này vì tôi muốn giữ chức năng này thuần nhất có thể - nếu tôi gọi nó, tôi hy vọng trạng thái sẽ được cập nhật. Nhưng tôi không thể biết liệu có tốt hơn để kết thúc cuộc gọi trong một vài nơi mà tôi biết nó có khả năng không thay đổi hay không.



3
@gnat Không, câu hỏi đó thực chất là 'Cú pháp ưa thích trong lối ra sớm' trong khi của tôi là 'Tôi có nên thoát sớm hay không nên gọi hàm'
Matthew Mullin

4
bạn không thể tin tưởng các nhà phát triển, ngay cả chính bạn , để kiểm tra chính xác các điều kiện trước của chức năng ở mọi nơi mà nó được gọi. vì lý do này, tôi đề nghị rằng hàm xác nhận bên trong bất kỳ điều kiện cần thiết nào nếu nó có khả năng thực hiện điều đó.
TZHX

"Tôi chỉ muốn trạng thái được cập nhật nếu trạng thái đã thay đổi" - bạn muốn trạng thái được cập nhật (= đã thay đổi) nếu trạng thái tương tự đã thay đổi? Âm thanh khá tròn. Bạn có thể vui lòng làm rõ ý của bạn về điều này không, vì vậy tôi có thể thêm một ví dụ có ý nghĩa vào câu trả lời của tôi về điều này?
Doc Brown

Ví dụ @DocBrown Cho phép tôi muốn giữ đồng bộ hai thuộc tính trạng thái đối tượng khác nhau. Khi một trong hai đối tượng thay đổi, tôi gọi syncStatuses () - nhưng điều này có thể được kích hoạt cho nhiều thay đổi trường khác nhau (không chỉ trường trạng thái).
Matthew Mullin

Câu trả lời:


15

Bao bọc một if xung quanh một lệnh gọi hàm:
Điều này là để quyết định xem hàm này có nên được gọi hay không và là một phần của quá trình ra quyết định của chương trình của bạn.

Mệnh đề bảo vệ trong hàm (trả về sớm):
Đây là để bảo vệ khỏi bị gọi với các tham số không hợp lệ

Mệnh đề bảo vệ được sử dụng theo cách này giữ cho hàm "thuần" (thuật ngữ của bạn). Nó chỉ tồn tại để đảm bảo rằng chức năng không bị hỏng với dữ liệu đầu vào sai.

Logic về việc có nên gọi hàm này hay không là ở mức độ trừu tượng cao hơn, ngay cả khi chỉ. Nó nên tồn tại bên ngoài chức năng chính nó. Như DocBrown nói, bạn có thể có một hàm trung gian thực hiện kiểm tra này, để đơn giản hóa mã.

Đây là một câu hỏi hay để hỏi và nằm trong nhóm các câu hỏi dẫn đến việc nhận ra mức độ trừu tượng. Mỗi chức năng nên hoạt động ở một mức độ duy nhất của sự trừu tượng, và có cả các chương trình logic và logic chức năng trong hàm cảm thấy sai lầm khi bạn - điều này là bởi vì họ đang ở mức trừu tượng khác nhau. Các chức năng chính nó là một cấp thấp hơn.

Bằng cách giữ các phần riêng biệt này, bạn đảm bảo rằng mã của bạn sẽ dễ viết, đọc và bảo trì hơn.


Câu trả lời tuyệt vời. Tôi thích thực tế nó cho tôi một cách rõ ràng để suy nghĩ về điều này. Nếu nên ở bên ngoài vì nó là 'một phần của quá trình ra quyết định' về việc liệu hàm này có được gọi ở vị trí đầu tiên hay không. Và vốn dĩ không liên quan gì đến chức năng. Cảm thấy kỳ lạ khi đánh dấu một câu trả lời ý kiến ​​là chính xác, nhưng tôi sẽ xem lại sau vài giờ và làm như vậy.
Matthew Mullin

Nếu nó hữu ích, tôi không coi đây là một câu trả lời "ý kiến". Tôi lưu ý rằng nó "cảm thấy" sai, nhưng đó là vì mức độ trừu tượng khác nhau không tách rời. Điều tôi nhận được từ câu hỏi của bạn là bạn có thể thấy rằng điều đó không 'đúng' nhưng vì bạn không nghĩ về mức độ trừu tượng nên rất khó để định lượng, do đó, đó là điều bạn đấu tranh để diễn đạt.
Baldrickk

7

Bạn có thể có cả hai - một chức năng không kiểm tra các tham số và một chức năng khác, như thế này (có thể trả về một số thông tin về nếu cuộc gọi được thực hiện):

bool tryRunFunction(...)
{
    bool shouldThisRun = /* some logic using data not available inside "runFunction"*/;
    if (shouldThisRun)
        runFunction();
    return shouldThisRun;
}

Bằng cách đó, bạn có thể tránh logic trùng lặp bằng cách cung cấp chức năng có thể sử dụng lại tryRunFunctionvà vẫn có chức năng ban đầu (có thể thuần túy) mà không thực hiện kiểm tra bên trong.

Lưu ý rằng đôi khi bạn sẽ cần một chức năng như tryRunFunctionvới một kiểm tra tích hợp, vì vậy bạn có thể tích hợp kiểm tra vào runFunction. Hoặc bạn không có nhu cầu sử dụng lại kiểm tra ở bất cứ đâu trong chương trình của bạn, trong trường hợp đó bạn có thể để nó ở lại chức năng gọi điện.

Tuy nhiên, hãy cố gắng làm cho nó trong suốt cho người gọi những gì xảy ra bằng cách đặt tên đúng cho các chức năng của bạn. Vì vậy, người gọi không phải đoán hoặc xem xét việc thực hiện nếu họ phải tự kiểm tra hoặc nếu chức năng được gọi đã thực hiện việc đó cho họ. Một tiền tố đơn giản như trythường có thể đủ cho việc này.


1
Phải thừa nhận, thành ngữ "tryXXX ()" luôn có vẻ hơi sai và không phù hợp ở đây. Bạn không cố gắng làm điều gì đó có thể xảy ra lỗi. Bạn đang cập nhật nếu nó bẩn.
dùng949300

@ user949300: chọn một tên hay sơ đồ đặt tên tùy thuộc vào trường hợp sử dụng thực, tên hàm thực chứ không phải một số tên giả như thế runFunction. Một chức năng như updateStatus()có thể được đi kèm với một chức năng khác như updateIfStatusHasChanged(). Nhưng đây là trường hợp phụ thuộc 100%, không có giải pháp "một cỡ vừa cho tất cả" cho vấn đề này, vì vậy, tôi đồng ý, thành ngữ "thử" không phải lúc nào cũng là một lựa chọn tốt.
Doc Brown

Không có cái gì tên là "DryRun"? Ít nhiều trở thành một thực thi thường xuyên mà không có tác dụng phụ. Làm thế nào để vô hiệu hóa các tác dụng phụ là một câu chuyện khác
Laiv

3

Đối với ai quyết định có nên chạy hay không, câu trả lời là từ GRASP , ai là "chuyên gia thông tin" biết.

Một khi bạn đã quyết định điều đó, hãy xem xét đổi tên hàm cho rõ ràng.

Một cái gì đó như thế này, nếu chức năng quyết định:

 ensureUpdated()
 updateIfDirty()

Hoặc, nếu người gọi có nghĩa vụ quyết định:

 writeStatus()

2

Tôi muốn mở rộng câu trả lời của @ Baldrickk.

Không có câu trả lời chung cho câu hỏi của bạn. Nó phụ thuộc vào ý nghĩa (hợp đồng) của chức năng được gọi và bản chất của điều kiện.

Vì vậy, hãy thảo luận về nó trong bối cảnh cuộc gọi ví dụ của bạn updateStatus(). Hợp đồng của nó có lẽ là để cập nhật một số trạng thái vì một cái gì đó có ảnh hưởng đến trạng thái đã xảy ra. Tôi mong muốn các cuộc gọi đến phương thức đó được cho phép ngay cả khi không có thay đổi trạng thái thực sự và là cần thiết nếu có thay đổi thực sự.

Vì vậy, một trang web gọi có thể bỏ qua một cuộc gọi đến updateStatus()nếu nó biết rằng (bên trong chân trời tên miền của nó) không có gì thay đổi liên quan. Đó là tình huống mà cuộc gọi nên được bao quanh bởi một ifcấu trúc thích hợp .

Bên trong updateStatus()hàm, có thể có các tình huống mà hàm này phát hiện (từ dữ liệu bên trong chân trời miền của nó) mà không có gì để làm và đó là nơi nó sẽ quay lại sớm.

Vì vậy, các câu hỏi là:

  • Từ góc nhìn bên ngoài, khi nào được phép / yêu cầu gọi hàm, có tính đến hợp đồng của hàm không?
  • Bên trong chức năng, có những tình huống mà nó có thể trở lại sớm mà không có công việc thực sự?
  • Điều kiện có nên gọi hàm / trả về sớm có thuộc về miền bên trong của hàm hay bên ngoài không?

Với một updateStatus()chức năng, tôi hy vọng sẽ thấy cả hai, gọi các trang web không biết gì đã thay đổi, bỏ qua cuộc gọi và kiểm tra thực hiện sớm các tình huống "không có gì thay đổi", ngay cả khi cách này cùng một điều kiện đôi khi được kiểm tra hai lần, cả hai bên trong và bên ngoài.


2

Có nhiều lời giải thích tốt. Nhưng tôi muốn nhìn theo cách khác thường: Giả sử rằng bạn sử dụng cách này:

if (shouldThisRun) {
   runFunction();
}

runFunction() {
   if (!shouldThisRun) return;
}

Và bạn cần gọi một hàm khác trong runFunctionphương thức như thế này:

runFunction() {
   if (!shouldThisRun) return;
   someOtherfunction();
}

Bạn sẽ làm gì? Bạn có sao chép tất cả các xác nhận từ trên xuống dưới?

someOtherfunction() {
   if (!shouldThisRun) return;
}

Tôi không nghĩ vậy. Vì vậy, tôi thường làm cách tiếp cận tương tự: Xác thực các đầu vào và kiểm tra các điều kiện trong publicphương thức. Các phương thức công khai nên tự xác nhận và kiểm tra các điều kiện bắt buộc ngay cả những người gọi. Nhưng hãy để phương pháp riêng chỉ làm kinh doanh riêng của mình . Một số chức năng khác có thể gọi runFunctionmà không thực hiện bất kỳ xác nhận hoặc kiểm tra bất kỳ điều kiện.

public someFunction() {
   if (shouldThisRun) {
      runFunction();
   }
}

private runFunction() {
 // do your business.
}
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.