Cách thanh lịch để xử lý nếu (nếu khác) khác


161

Đây là một vấn đề nhỏ, nhưng mỗi khi tôi phải viết mã như thế này, sự lặp lại làm phiền tôi, nhưng tôi không chắc rằng bất kỳ giải pháp nào không tệ hơn.

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
    }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}
  • Có một tên cho loại logic này?
  • Tôi có phải là một chút quá OCD?

Tôi mở các đề xuất mã xấu, nếu chỉ vì tò mò ...


8
@Emmad Kareem: hai DefaultActioncuộc gọi vi phạm nguyên tắc DRY
Abyx

Cảm ơn câu trả lời của bạn, nhưng tôi nghĩ rằng nó ổn, ngoại trừ việc không sử dụng thử / bắt vì có thể có lỗi không trả về kết quả và sẽ gây ra sự lạm dụng (tùy thuộc vào ngôn ngữ lập trình của bạn).
NoChance

20
Tôi nghĩ vấn đề chính ở đây là bạn đang làm việc ở mức độ trừu tượng không nhất quán . Mức độ trừu tượng cao hơn là : make sure I have valid data for DoSomething(), and then DoSomething() with it. Otherwise, take DefaultAction(). Các chi tiết khó chịu của nitty về việc đảm bảo bạn có dữ liệu cho DoS Something () ở mức độ trừu tượng thấp hơn, và do đó nên ở một chức năng khác. Hàm này sẽ có một tên ở mức độ trừu tượng cao hơn và việc thực hiện nó sẽ ở mức thấp. Các câu trả lời tốt dưới đây giải quyết vấn đề này.
Gilad Naor

6
Vui lòng chỉ định một ngôn ngữ. Các giải pháp khả thi, thành ngữ tiêu chuẩn và chuẩn mực văn hóa lâu đời là khác nhau đối với các ngôn ngữ khác nhau và sẽ dẫn đến các câu trả lời khác nhau cho Q. của bạn
Caleb

1
Bạn có thể tham khảo cuốn sách này "Tái cấu trúc: Cải thiện thiết kế mã hiện tại". Có một số phần về cấu trúc if-other, thực tế hữu ích.
Vacker

Câu trả lời:


96

Trích xuất nó để tách chức năng (phương thức) và sử dụng returncâu lệnh:

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
        return;
    }
}

DefaultAction();

Hoặc, có thể tốt hơn, tách biệt nội dung nhận và xử lý của nó:

contents_t get_contents(name_t file)
{
    if(!FileExists(file))
        return null;

    contents = OpenFile(file);
    if(!SomeTest(contents)) // like IsContentsValid
        return null;

    return contents;
}

...

contents = get_contents(file)
contents ? DoSomething(contents) : DefaultAction();

Cập nhật:

Tại sao không ngoại lệ, tại sao OpenFilekhông ném ngoại lệ IO:
Tôi nghĩ rằng đó thực sự là câu hỏi chung chung, thay vì câu hỏi về tệp IO. Các tên như FileExists, OpenFilecó thể gây nhầm lẫn, nhưng nếu thay thế chúng bằng Foo, Barv.v. - sẽ rõ ràng hơn DefaultActioncó thể được gọi thường xuyên DoSomething, vì vậy nó có thể là trường hợp không đặc biệt. Péter Török đã viết về điều này khi kết thúc câu trả lời của mình

Tại sao có toán tử điều kiện thứ ba trong biến thể thứ 2:
Nếu có thẻ [C ++], tôi đã viết iftuyên bố với phần khai báo contentstrong phần điều kiện của nó:

if(contents_t contents = get_contents(file))
    DoSomething(contents);
else
    DefaultAction();

Nhưng đối với các ngôn ngữ khác (giống như C), if(contents) ...; else ...;hoàn toàn giống như câu lệnh biểu thức với toán tử điều kiện ternary, nhưng dài hơn. Bởi vì phần chính của mã là get_contentschức năng, tôi chỉ sử dụng phiên bản ngắn hơn (và contentsloại bỏ qua ). Dù sao, nó vượt quá câu hỏi này.


93
+1 cho nhiều lợi nhuận - khi các phương thức được thực hiện đủ nhỏ , cách tiếp cận này hiệu quả nhất với tôi
gnat

Không phải là một fan hâm mộ lớn của nhiều lợi nhuận, mặc dù tôi thỉnh thoảng sử dụng nó. Nó khá hợp lý trên một cái gì đó đơn giản, nhưng không có quy mô tốt. Tiêu chuẩn của chúng tôi là tránh nó cho tất cả trừ các phương pháp đơn giản điên rồ vì các phương pháp có xu hướng tăng kích thước nhiều hơn so với thu nhỏ.
Brian Knoblauch

3
Nhiều đường dẫn trả về có thể có ý nghĩa hiệu suất tiêu cực trong các chương trình C ++, đánh bại các nỗ lực của trình tối ưu hóa để sử dụng RVO (cũng là NRVO, trừ khi mỗi đường dẫn trả về cùng một đối tượng).
Funcastic

Tôi khuyên bạn nên đảo ngược logic trên giải pháp thứ 2: {if (tệp tồn tại) {đặt nội dung; if (đôi khi) {trả lại nội dung; }} trả về null; } Nó đơn giản hóa dòng chảy và giảm số lượng dòng.
Nêm

1
Xin chào Abyx, tôi nhận thấy bạn kết hợp một số phản hồi từ các ý kiến ​​ở đây: cảm ơn vì đã làm điều đó. Tôi đã dọn sạch mọi thứ được giải quyết trong câu trả lời của bạn và các câu trả lời khác.

56

Nếu ngôn ngữ lập trình bạn đang sử dụng (0) so sánh nhị phân ngắn mạch (nghĩa là nếu không gọi SomeTestnếu FileExiststrả về sai) và (1) phép gán trả về một giá trị (kết quả OpenFileđược gán cho contentsvà sau đó giá trị đó được truyền dưới dạng đối số đến SomeTest), bạn có thể sử dụng một cái gì đó như sau, nhưng bạn vẫn nên khuyên bạn nhận xét mã lưu ý rằng đơn =là cố ý.

if( FileExists(file) && SomeTest(contents = OpenFile(file)) )
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}

Tùy thuộc vào mức độ phức tạp của if, có thể tốt hơn khi có một biến cờ (phân tách kiểm tra các điều kiện thành công / thất bại với mã xử lý lỗi DefaultActiontrong trường hợp này)


Đây là cách tôi sẽ làm điều đó.
Anthony

13
Theo tôi, khá thô để đặt quá nhiều mã trong một iftuyên bố.
moteutsch

15
Trái lại, tôi thích kiểu tuyên bố "nếu có gì đó tồn tại và đáp ứng điều kiện này". +1
Gorpik

Tôi cũng làm như vậy! Cá nhân tôi không thích cách mọi người sử dụng nhiều trả lại một số mặt bằng không được đáp ứng. Tại sao bạn không đảo ngược các if đó và thực thi mã của mình nếu chúng được đáp ứng?
klaar

"Nếu một cái gì đó tồn tại và đáp ứng điều kiện này" là tốt. "Nếu một cái gì đó tồn tại và làm một cái gì đó liên quan tiếp tuyến ở đây và đáp ứng điều kiện này", OTOH, là khó hiểu. Nói cách khác, tôi không thích tác dụng phụ trong một điều kiện.
Piskvor

26

Nghiêm trọng hơn sự lặp lại của lệnh gọi DefaultAction là kiểu chính vì mã được viết không trực giao (xem câu trả lời này để biết lý do chính đáng để viết trực giao).

Để chỉ ra lý do tại sao mã không trực giao là xấu, hãy xem xét ví dụ ban đầu, khi một yêu cầu mới mà chúng ta không nên mở tệp nếu nó được lưu trữ trên đĩa mạng được đưa ra. Chà, sau đó chúng tôi chỉ có thể cập nhật mã như sau:

if(FileExists(file))
{
    if(! OnNetworkDisk(file))
    {
        contents = OpenFile(file); // <-- prevents inclusion in if
        if(SomeTest(contents))
        {
            DoSomething(contents);
        }
        else
        {
            DefaultAction();
        }
    }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}

Nhưng sau đó cũng có một yêu cầu là chúng ta không nên mở các tệp lớn trên 2Gb. Vâng, chúng tôi chỉ cập nhật lại:

if(FileExists(file))
{
    if(LessThan2Gb(file))
    {
        if(! OnNetworkDisk(file))
        {
            contents = OpenFile(file); // <-- prevents inclusion in if
            if(SomeTest(contents))
            {
                DoSomething(contents);
            }
            else
            {
                DefaultAction();
            }
        }
        else
        {
            DefaultAction();
        }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}

Cần phải rất rõ ràng rằng phong cách mã như vậy sẽ là một nỗi đau bảo trì rất lớn.

Trong số các câu trả lời ở đây được viết đúng trực giao là ví dụ thứ hai của Abyxcâu trả lời của Jan Hudec , vì vậy tôi sẽ không nhắc lại, chỉ cần thêm rằng hai yêu cầu trong các câu trả lời đó sẽ chỉ là

if(! LessThan2Gb(file))
    return null;

if(OnNetworkDisk(file))
    return null;

(hoặc goto notexists;thay vì return null;), không ảnh hưởng đến bất kỳ mã nào khác ngoài những dòng được thêm vào . Ví dụ: trực giao.

Khi kiểm tra, quy tắc chung nên là kiểm tra các trường hợp ngoại lệ, không phải trường hợp bình thường .


8
+1 cho tôi. Trả về sớm giúp tránh mô hình chống đầu mũi tên. Xem codinghorror.com/blog/2006/01/flattening-arrow-code.htmllostechies.com/chrismissal/2009/05/27/... Trước khi đọc về mô hình này, tôi luôn đăng ký với 1 entry / exit mỗi chức năng lý thuyết do những gì tôi đã được dạy cách đây 15 năm. Tôi cảm thấy điều này chỉ làm cho mã dễ đọc hơn rất nhiều và khi bạn đề cập đến việc duy trì nhiều hơn.
Ông Moose

3
@MrMoose: đề cập đến câu trả lời chống mẫu của đầu mũi tên Câu hỏi rõ ràng của Stewol: "Có tên nào cho loại logic này không?" Gửi nó như một câu trả lời và bạn đã nhận được phiếu bầu của tôi.
outis

Đây là một câu trả lời tuyệt vời, cảm ơn bạn. Và @MrMoose: "mô hình chống đầu mũi tên" có thể trả lời viên đạn đầu tiên của tôi, vì vậy, hãy đăng nó. Tôi không thể hứa rằng tôi sẽ chấp nhận nó, nhưng nó xứng đáng được bình chọn!
Stewol

@outis. Cảm ơn. Tôi đã thêm câu trả lời. Mẫu chống đầu mũi tên chắc chắn có liên quan trong bài của hlovdal và các mệnh đề bảo vệ của anh ta hoạt động tốt trong việc đi xung quanh chúng. Tôi không biết làm thế nào bạn có thể trả lời điểm đạn thứ hai cho vấn đề này. Tôi không đủ điều kiện để chẩn đoán điều đó :)
Mr Moose

4
+1 cho "trường hợp ngoại lệ kiểm tra, không phải trường hợp bình thường."
Roy Tinker

25

Hiển nhiên, rõ ràng:

Whatever(Arguments)
{
    if(!FileExists(file))
        goto notexists;
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(!SomeTest(contents))
        goto notexists;
    DoSomething(contents);
    return;
notexists:
    DefaultAction();
}

Bạn nói rằng bạn thậm chí còn cởi mở với các giải pháp độc ác, vì vậy sử dụng số lượng goto ác, không?

Trong thực tế tùy thuộc vào bối cảnh, giải pháp này có thể ít tệ hơn so với cái ác thực hiện hành động hai lần hoặc biến phụ xấu. Tôi gói nó trong một hàm, bởi vì nó chắc chắn sẽ không ổn ở giữa hàm dài (nhất là do sự trở lại ở giữa). Nhưng hơn chức năng dài là không ổn, thời gian.

Khi bạn có ngoại lệ, chúng sẽ dễ đọc hơn, đặc biệt là nếu bạn có thể có OpenFile và DoS Something chỉ cần ném ngoại lệ nếu các điều kiện không được thỏa mãn, do đó bạn không cần kiểm tra rõ ràng. Mặt khác, trong C ++, Java và C # ném một ngoại lệ là một hoạt động chậm, vì vậy từ điểm hiệu suất, goto vẫn thích hợp hơn.


Lưu ý về "cái ác": C ++ FAQ 6.15 định nghĩa "cái ác" là:

Nó có nghĩa là như vậy và đó là điều bạn nên tránh hầu hết thời gian, nhưng không phải là điều bạn nên tránh mọi lúc. Ví dụ, cuối cùng bạn sẽ sử dụng những thứ "xấu xa" này bất cứ khi nào chúng là "thứ xấu xa nhất trong số những thứ thay thế xấu xa".

Và điều đó áp dụng cho gototrong bối cảnh này. Các cấu trúc điều khiển luồng cấu trúc hầu hết thời gian tốt hơn, nhưng khi bạn gặp phải trường hợp chúng tích lũy quá nhiều tệ nạn của riêng mình, như gán trong điều kiện, lồng sâu hơn khoảng 3 cấp, sao chép mã hoặc điều kiện dài, gotocó thể chỉ đơn giản là kết thúc trở nên ít ác hơn.


11
Con trỏ của tôi đang di chuột qua nút chấp nhận ... chỉ để thu hút tất cả những người theo chủ nghĩa thuần túy. Oooohh sự cám dỗ: D
Stewol

2
Vâng vâng! Đây là cách hoàn toàn "đúng" để viết mã. Cấu trúc của mã bây giờ là "Nếu lỗi, xử lý lỗi. Hành động bình thường. Nếu lỗi, xử lý lỗi. Hành động bình thường" giống hệt như vậy. Tất cả các mã "bình thường" được viết chỉ bằng một lần thụt cấp duy nhất trong khi tất cả các mã liên quan đến lỗi có hai mức thụt. Vì vậy, mã bình thường VÀ QUAN TRỌNG NHẤT có được vị trí trực quan nổi bật nhất và có thể rất dễ đọc và dễ dàng đọc dòng chảy xuống theo thứ tự. Bằng mọi cách chấp nhận câu trả lời này.
hlovdal

2
Và một khía cạnh khác là mã được viết theo cách này là trực giao. Chẳng hạn, hai dòng "if (! FileExists (file)) \ n \ tgoto notexists;" Bây giờ CHỈ liên quan đến việc xử lý khía cạnh lỗi đơn này (KISS) và quan trọng nhất là nó không ảnh hưởng đến bất kỳ dòng nào khác . Câu trả lời này stackoverflow.com/a/3272062/23118 liệt kê một số lý do tốt để giữ mã trực giao.
hlovdal

5
Nói về các giải pháp độc ác: Tôi có thể có giải pháp của bạn mà không cần goto:for(;;) { if(!FileExists(file)) break; contents = OpenFile(file); if(!SomeTest(contents)) break; DoSomething(contents); return; } /* broken out */ DefaultAction();
herby

4
@herby: Giải pháp của bạn còn tệ hơn cả goto, bởi vì bạn đang lạm dụng breaktheo cách mà không ai ngờ nó bị lạm dụng, vì vậy mọi người đọc mã sẽ gặp nhiều vấn đề hơn khi thấy sự phá vỡ dẫn họ đến đâu so với goto nói rõ ràng. Bên cạnh đó, bạn đang sử dụng một vòng lặp vô hạn sẽ chỉ chạy một lần, điều này sẽ khá khó hiểu. Thật không may do { ... } while(0)là cũng không thể đọc chính xác, bởi vì bạn chỉ thấy nó chỉ là một khối vui nhộn khi bạn đi đến cuối và C không hỗ trợ phá vỡ từ các khối khác (không giống như perl).
Jan Hudec

12
function FileContentsExists(file) {
    return FileExists(file) ? OpenFile(file) : null;
}

...

contents = FileContentExists(file);
if(contents && SomeTest(contents))
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}

hoặc đi thêm nam và tạo một phương thức
FileExistsAndConditionMet

@herby SomeTestcó thể có cùng ngữ nghĩa với sự tồn tại của tệp nếu SomeTestkiểm tra loại tệp, ví dụ: kiểm tra xem .gif có thực sự là tệp GIF không.
Abyx

1
Đúng. Phụ thuộc. @Benjol hiểu rõ hơn.
Herby

3
... tất nhiên ý tôi là "đi thêm một lần nữa" ... :)
ChúZeiv

2
Đó đang ravioli đến tứ thậm chí tôi không đi (và tôi cực đoan trong việc này) ... Tôi nghĩ rằng bây giờ nó là độc đáo có thể đọc được xem xét contents && f(contents). Hai chức năng để cứu người khác?!
Herby

12

Một khả năng:

boolean handled = false;

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
        handled = true;
    }
}
if (!handled)
{
    DefaultAction();
}

Tất nhiên, điều này làm cho mã phức tạp hơn một chút theo một cách khác. Vì vậy, nó phần lớn là một câu hỏi phong cách.

Một cách tiếp cận khác sẽ sử dụng các ngoại lệ, ví dụ:

try
{
    contents = OpenFile(file); // throws IO exception if file not found
    DoSomething(contents); // calls SomeTest() and throws exception on failure
}
catch(Exception e)
{
    DefaultAction();
    // and the exception should be at least logged...
}

Điều này có vẻ đơn giản hơn, tuy nhiên nó chỉ được áp dụng nếu

  • chúng tôi biết chính xác loại ngoại lệ nào sẽ xảy ra và DefaultAction()phù hợp với từng loại
  • chúng tôi hy vọng việc xử lý tệp thành công và một tệp bị thiếu hoặc SomeTest()bị lỗi rõ ràng là một điều kiện sai lầm, do đó phù hợp để đưa ra một ngoại lệ đối với nó.

19
Không ~! Không phải là một biến cờ, nó chắc chắn sai cách, bởi vì nó dẫn đến sự phức tạp, khó hiểu (nơi mà cờ đó trở thành đúng) và khó để cấu trúc lại mã.
Abyx

Không, nếu bạn giới hạn nó ở phạm vi địa phương nhất có thể. (function () { ... })()trong Javascript, { flag = false; ... }trong C-like, vv
Herby

+1 cho logic ngoại lệ, rất có thể là giải pháp phù hợp nhất tùy thuộc vào kịch bản.
Steven Jeuris

4
+1 Điều này lẫn nhau 'Nooooo!' thật buồn cười Tôi nghĩ rằng biến trạng thái và trả lại sớm đều hợp lý trong một số trường hợp nhất định. Trong các thói quen phức tạp hơn, tôi sẽ chọn biến trạng thái bởi vì, thay vì thêm độ phức tạp, những gì nó thực sự làm là logic rõ ràng. Không có gì sai với điều đó.
Grossvogel

1
Đây là định dạng ưa thích của chúng tôi, nơi tôi làm việc. Hai tùy chọn có thể sử dụng chính dường như là "nhiều lợi nhuận" và "biến cờ". Cả hai dường như không có bất kỳ loại lợi thế thực sự nào, nhưng cả hai đều phù hợp với hoàn cảnh nhất định tốt hơn những trường hợp khác. Phải đi với trường hợp điển hình của bạn. Chỉ là một cuộc chiến tôn giáo "Emacs" so với "Vi". :-)
Brian Knoblauch

11

Đây là mức độ trừu tượng cao hơn:

if (WeCanDoSomething(file))
{
   DoSomething(contents);
}
else
{
   DefaultAction();
} 

Và điều này điền vào các chi tiết.

boolean WeCanDoSomething(file)
{
    if FileExists(file)
    {
        contents = OpenFile(file);
        return (SomeTest(contents));
    }
    else
    {
        return FALSE;
    }
}

11

Chức năng nên làm một việc. Họ nên làm điều đó tốt. Họ chỉ nên làm điều đó.
- Robert Martin trong mã sạch

Một số người thấy rằng cách tiếp cận hơi cực đoan, nhưng nó cũng rất sạch sẽ. Cho phép tôi minh họa bằng Python:

def processFile(self):
    if self.fileMeetsTest():
        self.doSomething()
    else:
        self.defaultAction()

def fileMeetsTest(self):
    return os.path.exists(self.path) and self.contentsTest()

def contentsTest(self):
    with open(self.path) as file:
        line = file.readline()
        return self.firstLineTest(line)

Khi anh ta nói các chức năng nên làm một việc, anh ta có nghĩa là một điều. processFile()chọn một hành động dựa trên kết quả của một bài kiểm tra, và đó là tất cả những gì nó làm. fileMeetsTest()kết hợp tất cả các điều kiện của bài kiểm tra, và đó là tất cả những gì nó làm. contentsTest()chuyển dòng đầu tiên sang firstLineTest(), và đó là tất cả những gì nó làm.

Có vẻ như rất nhiều chức năng, nhưng nó thực tế đọc giống như tiếng Anh thẳng:

Để xử lý tệp, kiểm tra xem nó có đáp ứng thử nghiệm không. Nếu có, sau đó làm một cái gì đó. Nếu không, hãy thực hiện hành động mặc định. Các tập tin đáp ứng kiểm tra nếu nó tồn tại và vượt qua kiểm tra nội dung. Để kiểm tra nội dung, mở tệp và kiểm tra dòng đầu tiên. Bài kiểm tra cho dòng đầu tiên ...

Cấp, đó là một chút dài dòng, nhưng lưu ý rằng nếu một người bảo trì không quan tâm đến các chi tiết, anh ta có thể ngừng đọc chỉ sau 4 dòng mã processFile(), và anh ta sẽ vẫn có kiến ​​thức tốt về chức năng.


5
+1 Đó là lời khuyên tốt, nhưng điều gì tạo nên "một điều" phụ thuộc vào lớp trừu tượng hiện tại. processFile () là "một thứ", nhưng có hai thứ: fileMeetsTest () và doS Something () hoặc defaultAction (). Tôi lo sợ rằng "một điều" khía cạnh có thể nhầm lẫn người mới bắt đầu người không một tiên nghiệm hiểu khái niệm.
Caleb

1
Đó là một mục tiêu tốt ... Đó là tất cả những gì tôi phải nói về điều đó ... ;-)
Brian Knoblauch

1
Tôi không thích ngầm truyền các đối số như các biến thể hiện như thế. Bạn có đầy đủ các biến đối tượng "vô dụng" và có nhiều cách để cải thiện trạng thái của bạn và phá vỡ các bất biến.
hugomg

@Caleb, ProcessFile () thực sự đang làm một việc. Như Karl nói trong bài đăng của mình, nó đang sử dụng một bài kiểm tra để quyết định nên thực hiện hành động nào và trì hoãn việc thực hiện thực tế các khả năng hành động đối với các phương pháp khác. Nếu bạn đã thêm nhiều hành động thay thế khác, các tiêu chí mục đích duy nhất cho phương thức vẫn sẽ được đáp ứng miễn là không có sự lồng ghép logic nào xảy ra trong phương thức ngay lập tức.
S.Robins

6

Liên quan đến cái này được gọi là gì, nó có thể dễ dàng phát triển thành mẫu chống đầu mũi tên khi mã của bạn phát triển để xử lý nhiều yêu cầu hơn (như được hiển thị bởi câu trả lời được cung cấp tại https://softwareengineering.stackexchange.com/a/122625/33922 ) và sau đó rơi vào cái bẫy có những đoạn mã lớn với các câu điều kiện lồng nhau giống như một mũi tên.

Xem các Liên kết như;

http://codinghorror.com/blog/2006/01/flattening-arrow-code.html

http://lostechies.com/chrismissal/2009/05/27/anti-potypes-and-worst-practices-the-arrowhead-anti-potype/

Có nhiều hơn nữa về điều này và các mẫu chống khác được tìm thấy trên Google.

Một số lời khuyên tuyệt vời Jeff cung cấp trên blog của mình về điều này là;

1) Thay thế các điều kiện bằng các mệnh đề bảo vệ.

2) Phân tách các khối có điều kiện thành các hàm riêng biệt.

3) Chuyển đổi kiểm tra tiêu cực thành kiểm tra tích cực

4) Luôn luôn quay trở lại cơ hội càng sớm càng tốt từ chức năng.

Xem một số ý kiến ​​trên blog của Jeff liên quan đến đề xuất của Steve McConnells về lợi nhuận sớm;

"Sử dụng trả về khi nó tăng cường khả năng đọc: Trong một số thói quen nhất định, khi bạn biết câu trả lời, bạn muốn trả lại ngay cho thói quen gọi điện. Nếu thói quen được xác định theo cách mà nó không yêu cầu dọn dẹp thêm một lần nữa phát hiện lỗi, không quay lại ngay có nghĩa là bạn phải viết thêm mã. "

...

"Giảm thiểu số lần trả về trong mỗi thói quen: Khó hiểu hơn một thói quen khi đọc nó ở phía dưới, bạn không biết khả năng nó sẽ quay trở lại ở đâu đó. Vì lý do đó, hãy sử dụng trả về một cách thận trọng - chỉ khi chúng cải thiện khả năng đọc. "

Tôi luôn đăng ký 1 mục nhập / thoát cho mỗi lý thuyết chức năng do những gì tôi đã được dạy cách đây 15 năm hoặc lâu hơn. Tôi cảm thấy điều này chỉ làm cho mã dễ đọc hơn rất nhiều và như bạn đề cập đến dễ bảo trì hơn


6

Điều này phù hợp với quy tắc DRY, không goto và không trả về nhiều lần, theo ý kiến ​​của tôi có thể mở rộng và có thể đọc được:

success = FileExists(file);
if (success)
{
    contents = OpenFile(file);
    success = SomeTest(contents);
}
if (success)
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}

1
Tuân thủ các tiêu chuẩn không nhất thiết phải bằng mã tốt mặc dù. Tôi hiện chưa quyết định về đoạn mã này.
Brian Knoblauch

cái này chỉ thay thế 2 defaultAction (); với 2 điều kiện giống hệt nhau và thêm một biến cờ mà imo tệ hơn nhiều.
Ryathal

3
Lợi ích của việc sử dụng một cấu trúc như thế này là khi số lượng thử nghiệm tăng lên, mã không bắt đầu lồng nhiều ifs vào bên trong các ifs khác . Ngoài ra, mã để xử lý trường hợp không thành công ( DefaultAction()) chỉ ở một nơi và với mục đích gỡ lỗi, mã không nhảy xung quanh các hàm trợ giúp và thêm các điểm dừng vào các dòng mà successbiến được thay đổi có thể nhanh chóng hiển thị các thử nghiệm đã vượt qua (phía trên kích hoạt điểm dừng) và những điểm chưa được thử nghiệm (bên dưới).
Frozenkoi

1
Yeeaah, tôi rất thích nó, nhưng tôi nghĩ tôi đã đổi tên successthành ok_so_far:)
Stewol

Điều này rất giống với những gì tôi làm khi (1) quá trình này rất tuyến tính khi mọi thứ đều ổn và (2) nếu không bạn sẽ có mũi tên chống mẫu. Tuy nhiên, tôi cố gắng tránh thêm một biến phụ, điều này thường dễ nếu bạn nghĩ theo các điều kiện tiên quyết cho bước tiếp theo (khác biệt một cách tinh tế so với hỏi nếu bước trước đó thất bại). Nếu tệp tồn tại, mở tệp. Nếu tập tin được mở, hãy đọc nội dung. Nếu tôi có nội dung, xử lý chúng, khác làm hành động mặc định.
Adrian McCarthy

3

Tôi sẽ trích xuất nó sang một phương thức riêng biệt và sau đó:

if(!FileExists(file))
{
    DefaultAction();
    return;
}

contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return;
}

DoSomething(contents);

cũng cho phép

if(!FileExists(file))
{
    DefaultAction();
    return Result.FileNotFound;
}

contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return Result.TestFailed;
}

DoSomething(contents);
return Result.Success;            

sau đó có thể bạn có thể loại bỏ các DefaultActioncuộc gọi và thực hiện việc thực hiện DefaultActioncho người gọi:

Result OurMethod(file)
{
    if(!FileExists(file))
    {
        return Result.FileNotFound;
    }

    contents = OpenFile(file);
    if(!SomeTest(contents))
    {
        return Result.TestFailed;
    }

    DoSomething(contents);
    return Result.Success;            
}

void Caller()
{
    // something, something...

    var result = OurMethod(file);
    // if (result == Result.FileNotFound || result == Result.TestFailed), or just
    if (result != Result.Success)        
    {
        DefaultAction();
    }
}

Tôi cũng thích cách tiếp cận của Jeanne Pindar .


3

Đối với trường hợp cụ thể này, câu trả lời là đủ dễ dàng ...

Có một điều kiện cuộc đua giữa FileExistsOpenFile: điều gì xảy ra nếu tệp bị xóa?

Cách duy nhất để đối phó với trường hợp cụ thể này là bỏ qua FileExists:

contents = OpenFile(file);
if (!contents) // open failed
    DefaultAction();
else (SomeTest(contents))
    DoSomething(contents);

Điều này giải quyết gọn gàng vấn đề này làm cho mã sạch hơn.

Nói chung: Cố gắng suy nghĩ lại vấn đề và đưa ra một giải pháp khác để tránh vấn đề hoàn toàn.


2

Một khả năng khác nếu bạn không muốn thấy quá nhiều người khác là bỏ hoàn toàn việc sử dụng người khác và đưa ra một tuyên bố hoàn trả thêm. Khác là loại không cần thiết trừ khi bạn yêu cầu logic phức tạp hơn để xác định xem có nhiều hơn chỉ có hai khả năng hành động hay không.

Do đó, ví dụ của bạn có thể trở thành:

void DoABunchOfStuff()
{
    if(FileExists(file))
    {
        DoSomethingWithFileContent(file);
        return;
    }

    DefaultAction();
}

void DoSomethingWithFileContent(file)
{        
    var contents = GetFileContents(file)

    if(SomeTest(contents))
    {
        DoSomething(contents);
        return;
    }

    DefaultAction();
}

AReturnType GetFileContents(file)
{
    return OpenFile(file);
}

Cá nhân tôi không ngại sử dụng mệnh đề khác vì nó nói rõ ràng logic được cho là hoạt động như thế nào và do đó cải thiện khả năng đọc mã của bạn. Tuy nhiên một số công cụ đang làm đẹp thích để đơn giản hóa đến một đơn nếu tuyên bố để ngăn cản luận làm tổ.


2

Trường hợp được hiển thị trong mã mẫu thường có thể được giảm xuống thành một ifcâu lệnh. Trên nhiều hệ thống, chức năng mở tệp sẽ trả về giá trị không hợp lệ nếu tệp không tồn tại. Đôi khi đây là hành vi mặc định; lần khác, nó phải được chỉ định thông qua một đối số. Điều này có nghĩa là FileExistsbài kiểm tra có thể bị hủy, điều này cũng có thể giúp với các điều kiện cuộc đua do việc xóa tệp giữa kiểm tra tồn tại và mở tệp.

file = OpenFile(path);
if(isValidFileHandle(file) && SomeTest(file)) {
    DoSomething(file);
} else {
    DefaultAction();
}

Điều này không trực tiếp giải quyết vấn đề trộn mức độ trừu tượng vì nó vượt qua vấn đề của nhiều thử nghiệm không thể giải quyết được, mặc dù việc loại bỏ kiểm tra tồn tại tệp không tương thích với các mức độ trừu tượng hóa. Giả sử rằng các tệp xử lý tệp không hợp lệ tương đương với "false" và các tệp xử lý đóng khi chúng vượt quá phạm vi:

OpenFileIfSomething(path:String) : FileHandle {
    file = OpenFile(path);
    if (file && SomeTest(file)) {
        return file;
    }
    return null;
}

...

if ((file = OpenFileIfSomething(path))) {
    DoSomething(file);
} else {
    DefaultAction();
}

2

Tôi đồng ý với Frozenkoi, tuy nhiên, đối với C # dù sao, tôi nghĩ rằng nó sẽ giúp theo cú pháp của các phương thức TryPude.

if(FileExists(file) && TryOpenFile(file, out contents))
    DoSomething(contents);
else
    DefaultAction();
bool TryOpenFile(object file, out object contents)
{
    try{
        contents = OpenFile(file);
    }
    catch{
        //something bad happened, computer probably exploded
        return false;
    }
    return true;
}

1

Mã của bạn xấu vì bạn đang làm quá nhiều trong một chức năng. Bạn muốn xử lý tệp hoặc thực hiện hành động mặc định, vì vậy hãy bắt đầu bằng cách nói rằng:

if (!ProcessFile(file)) { 
  DefaultAction(); 
}

Lập trình viên Perl và Ruby viết processFile(file) || defaultAction()

Bây giờ hãy viết ProcessFile:

if (FileExists(file)) { 
  contents = OpenFile(file);
  if (SomeTest(contents)) {
    processContents(contents);
    return true;
  }
}
return false;

1

Tất nhiên bạn chỉ có thể đi xa trong các tình huống như thế này, nhưng đây là một cách để đi:

interface File<T> {
    function isOK():Bool;
    function getData():T;
}

var appleFile:File<Apple> = appleStorage.get(fileURI);
if (appleFile.isOK())
    eat(file.getData());
else
    cry();

Bạn có thể muốn các bộ lọc bổ sung. Sau đó, làm điều này:

var appleFile = appleStorage.get(fileURI, isEdible);
//isEdible is of type Apple->Bool and will be used internally to answer to the isOK call
if (appleFile.isOK())
    eat(file.getData());
else
    cry();

Mặc dù điều này cũng có thể có ý nghĩa:

function eat(apple:Apple) {
     if (isEdible(apple)) 
         digest(apple);
     else
         die();
}
var appleFile = appleStorage.get(fileURI);
if (appleFile.isOK())
    eat(appleFile.getData());
else
    cry();

Cái nào tốt nhất? Điều đó phụ thuộc vào vấn đề thực tế mà bạn gặp phải.
Nhưng điều cần làm là: bạn có thể làm được rất nhiều với thành phần và tính đa hình.


1

Có gì sai với điều hiển nhiên

if(!FileExists(file)) {
    DefaultAction();
    return;
}
contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return;
}        
DoSomething(contents);

Nó có vẻ khá chuẩn với tôi? Đối với loại thủ tục lớn đó, nơi có rất nhiều điều nhỏ phải xảy ra, thất bại của bất kỳ điều nào trong số đó sẽ ngăn cản điều sau. Các ngoại lệ làm cho nó sạch hơn một chút nếu đó là một tùy chọn.


0

Tôi nhận ra rằng đây là một câu hỏi cũ, nhưng tôi nhận thấy một mô hình chưa được đề cập; chủ yếu, thiết lập một biến để sau này xác định phương thức bạn muốn gọi (bên ngoài if ... other ...).

Đây chỉ là một góc nhìn khác để làm cho mã dễ làm việc hơn. Nó cũng cho phép khi bạn có thể muốn thêm một phương thức khác để được gọi hoặc thay đổi phương thức thích hợp cần được gọi trong các tình huống nhất định.

Thay vì phải thay thế tất cả các đề cập của phương thức (và có thể thiếu một số tình huống), tất cả chúng đều được liệt kê ở cuối khối if ... khác ... và đơn giản hơn để đọc và thay đổi. Tôi có xu hướng sử dụng điều này khi ví dụ, một số phương thức có thể được gọi, nhưng trong phạm vi lồng nhau nếu ... khác ... một phương thức có thể được gọi trong một số kết quả khớp.

Nếu bạn đặt một biến xác định trạng thái, bạn có thể có nhiều tùy chọn lồng nhau sâu sắc và cập nhật trạng thái khi có một cái gì đó được (hoặc không được) thực hiện.

Điều này có thể được sử dụng như trong ví dụ được hỏi trong câu hỏi mà chúng tôi đang kiểm tra xem 'DoS Something' có xảy ra hay không và nếu không, hãy thực hiện hành động mặc định. Hoặc bạn có thể có trạng thái cho từng phương thức bạn có thể muốn gọi, đặt khi áp dụng, sau đó gọi phương thức áp dụng bên ngoài nếu ... khác ...

Vào cuối câu lệnh lồng nhau nếu ... khác ..., bạn kiểm tra trạng thái và hành động tương ứng. Điều này có nghĩa là bạn chỉ cần một đề cập duy nhất về một phương thức thay vì tất cả các vị trí cần áp dụng.

bool ActionDone = false;

if (Method_1(object_A)) // Test 1
{
    result_A = Method_2(object_A); // Result 1

    if (Method_3(result_A)) // Test 2
    {
        Method_4(result_A); // Action 1
        ActionDone = true;
    }
}

if (!ActionDone)
{
    Method_5(); // Default Action
}

0

Để giảm IF lồng nhau:

1 / hoàn trả sớm;

2 / biểu thức hợp chất (nhận biết ngắn mạch)

Vì vậy, ví dụ của bạn có thể được tái cấu trúc như thế này:

if( FileExists(file) && SomeTest(contents = OpenFile(file)) )
{
    DoSomething(contents);
    return;
}
DefaultAction();

0

Tôi đã thấy rất nhiều ví dụ với "return" mà tôi cũng sử dụng nhưng đôi khi tôi muốn tránh tạo các hàm mới và sử dụng một vòng lặp thay thế:

while (1) {
    if (FileExists(file)) {
        contents = OpenFile(file);
        if (SomeTest(contents)) {
           DoSomething(contents);
           break;
        } 
    }
    DefaultAction();
    break;
}

Nếu bạn muốn viết ít dòng hơn hoặc bạn ghét các vòng lặp vô hạn như tôi, bạn có thể thay đổi loại vòng lặp thành "làm ... trong khi (0)" và tránh "ngắt" cuối cùng.


0

Làm thế nào về giải pháp này:

content = NULL; //I presume OpenFile returns a pointer 
if(FileExists(file))
    contents = OpenFile(file);
if(content != NULL && SomeTest(contents))
    DoSomething(contents);
else
    DefaultAction();

Tôi đã đưa ra giả định rằng OpenFile trả về một con trỏ, nhưng điều này cũng có thể hoạt động với trả về loại giá trị bằng cách chỉ định một số giá trị mặc định không thể trả về (mã lỗi hoặc đại loại như thế).

Tất nhiên tôi không mong đợi một số hành động có thể xảy ra thông qua phương thức someTest trên con trỏ NULL (nhưng bạn không bao giờ biết), vì vậy đây cũng có thể được xem như là một kiểm tra bổ sung cho con trỏ NULL cho lệnh gọi someTest (nội dung).


0

Rõ ràng, giải pháp thanh lịch và súc tích nhất là sử dụng macro tiền xử lý.

#define DOUBLE_ELSE(CODE) else { CODE } } else { CODE }

Cho phép bạn viết mã đẹp như thế này:

if(FileExists(file))
{
    contents = OpenFile(file);
    if(SomeTest(contents))
    {
        DoSomething(contents);
    }
    DOUBLE_ELSE(DefaultAction();)

Có thể khó dựa vào định dạng tự động nếu bạn sử dụng kỹ thuật này thường xuyên và một số IDE có thể mắng bạn một chút về những gì nó cho là sai lầm. Và như người ta vẫn nói, mọi thứ đều là sự đánh đổi, nhưng tôi cho rằng đó không phải là một cái giá quá tệ phải trả để tránh những tệ nạn của mã lặp đi lặp lại.


Đối với một số người và trong một số ngôn ngữ, macro tiền xử lý mã xấu :)
Stewol

@Benjol Bạn nói rằng bạn đã cởi mở với những lời đề nghị độc ác, phải không? ;)
Peter Olson

vâng, hoàn toàn, đó chỉ là viết "tránh tệ nạn" của bạn :)
Stewol

4
Điều này thật kinh khủng, tôi vừa phải nâng cấp nó: D
back2dos

Shirley, bạn không nghiêm túc !!!!!!
Jim ở Texas

-1

Vì bạn đã hỏi vì tò mò và câu hỏi của bạn không được gắn thẻ với một ngôn ngữ cụ thể (mặc dù rõ ràng bạn có ngôn ngữ bắt buộc trong tâm trí), có thể đáng để thêm rằng các ngôn ngữ hỗ trợ đánh giá lười biếng cho phép một cách tiếp cận hoàn toàn khác. Trong các ngôn ngữ đó, các biểu thức chỉ được đánh giá khi cần thiết, vì vậy bạn có thể xác định "biến" và chỉ sử dụng chúng khi có ý nghĩa để làm như vậy. Ví dụ: trong một ngôn ngữ hư cấu với sự lười biếng let/ incấu trúc, bạn quên mất việc điều khiển luồng và viết:

let
  contents = ReadFile(file)
in
  if FileExists(file) && SomeTest(contents) 
    DoSomething(contents)
  else 
    DefaultAction()
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.