Làm thế nào xấu xấu là mã không liên quan trong khối thử-bắt-cuối?


11

Đây là một Q có liên quan: Việc sử dụng mệnh đề cuối cùng để thực hiện công việc sau khi trả lại kiểu xấu / nguy hiểm?

Trong Q được tham chiếu, mã cuối cùng có liên quan đến cấu trúc được sử dụng và sự cần thiết của việc tìm nạp trước. Câu hỏi của tôi hơi khác một chút và tôi tin rằng nó phù hợp với đối tượng rộng lớn hơn. Ví dụ cụ thể của tôi là một ứng dụng winform C #, nhưng điều này cũng sẽ áp dụng cho việc sử dụng C ++ / Java.

Tôi nhận thấy khá nhiều khối thử bắt cuối cùng, nơi có rất nhiều mã không liên quan đến ngoại lệ và xử lý ngoại lệ / dọn dẹp được chôn trong khối. Và tôi sẽ thừa nhận sự thiên vị của mình đối với việc có các khối thử-bắt-cuối rất chặt chẽ với mã liên quan chặt chẽ đến ngoại lệ và xử lý. Dưới đây là một số ví dụ về những gì tôi đang thấy.

Các khối thử sẽ có rất nhiều cuộc gọi sơ bộ và các biến được đặt dẫn đến mã có thể ném. Thông tin đăng nhập sẽ được thiết lập và chạy trong khối thử.

Cuối cùng, các khối sẽ có các cuộc gọi định dạng biểu mẫu / mô-đun / điều khiển (mặc dù ứng dụng sắp kết thúc, như được thể hiện trong khối bắt), cũng như tạo các đối tượng mới như bảng điều khiển.

Roughly:

    tên phương thức (...)
    {
        thử
        {
            // Rất nhiều mã cho phương thức ...
            // mã có thể ném ...
            // Nhiều mã hơn cho phương thức và trả về ...
        }
        bắt (cái gì đó)
        {// xử lý ngoại lệ}
        cuối cùng
        {
            // một số dọn dẹp do ngoại lệ, đóng cửa mọi thứ
            // thêm mã cho nội dung đã được tạo (bỏ qua mọi trường hợp ngoại lệ có thể đã ném) ...
            // có thể tạo thêm một số đối tượng
        }
    }

Mã này hoạt động, vì vậy có một số giá trị cho nó. Nó không được gói gọn và logic hơi phức tạp. Tôi (đau đớn) quen thuộc với các rủi ro trong việc chuyển mã xung quanh cũng như tái cấu trúc, vì vậy câu hỏi của tôi tập trung vào việc muốn biết kinh nghiệm của người khác với mã có cấu trúc tương tự.

Liệu phong cách xấu có biện minh cho việc thay đổi? Có ai bị bỏng nặng từ một tình huống tương tự? Bạn có muốn chia sẻ chi tiết về trải nghiệm tồi tệ đó không? Rời khỏi đó là vì tôi phản ứng quá mức và nó không tệ về phong cách? Có được lợi ích bảo trì của việc dọn dẹp mọi thứ?


Thẻ "C ++" không thuộc về nơi này, vì C ++ không có (và không cần) finally. Tất cả các sử dụng tốt được bảo hiểm theo RAII / RRID / SBRM (bất kỳ từ viết tắt nào bạn thích).
David Thornley

2
@DavidThornley: Ngoài ví dụ sử dụng từ khóa 'cuối cùng', phần còn lại của câu hỏi áp dụng hoàn toàn tốt cho C ++. Tôi là nhà phát triển C ++ và từ khóa đó không thực sự làm tôi bối rối. Và xem xét tôi có các cuộc trò chuyện tương tự với nhóm của mình trên cơ sở bán định kỳ, câu hỏi này rất phù hợp với những gì chúng tôi làm với C ++
DXM

@DXM: Tôi gặp khó khăn khi hình dung điều này (và tôi biết những gì finallytrong C #). C ++ tương đương là gì? Những gì tôi nghĩ là mã sau catch, và điều đó áp dụng tương tự cho mã sau C # finally.
David Thornley

@DavidThornley: mã cuối cùng là mã được thực thi bất kể là gì .
orlp

1
@nightcracker, điều đó không thực sự chính xác. Nó sẽ không được thực thi nếu bạn làm Environment.FailFast(); nó có thể không được thực thi nếu bạn có một ngoại lệ chưa được lưu. Và nó thậm chí còn phức tạp hơn nếu bạn có một khối lặp finallymà bạn lặp lại bằng tay.
Svick

Câu trả lời:


10

Tôi đã trải qua một tình huống rất giống nhau khi tôi phải đối phó với một mã Windows Forms di sản khủng khiếp được viết bởi các nhà phát triển rõ ràng không biết họ đang làm gì.

Trước hết, bạn không hoạt động quá mức. Đây là mã xấu. Giống như bạn đã nói, khối bắt phải về việc hủy bỏ và chuẩn bị dừng lại. Chưa đến lúc tạo đối tượng (Bảng đặc biệt). Tôi thậm chí không thể bắt đầu giải thích tại sao điều này là xấu.

Điều đó đang được nói ...

Lời khuyên đầu tiên của tôi là: nếu nó không bị hỏng, đừng chạm vào nó!

Nếu công việc của bạn là duy trì mã, bạn phải cố gắng hết sức để không phá vỡ nó. Tôi biết điều đó thật đau đớn (tôi đã từng ở đó) nhưng bạn phải cố gắng hết sức để không phá vỡ những gì đã hoạt động.

Lời khuyên thứ hai của tôi là: nếu bạn phải thêm nhiều tính năng hơn, hãy thử giữ cấu trúc mã hiện có càng nhiều càng tốt để bạn không phá vỡ mã.

Ví dụ: nếu có một tuyên bố trường hợp chuyển đổi gớm ghiếc mà bạn cảm thấy có thể được thay thế bằng quyền thừa kế phù hợp, bạn phải cẩn thận và suy nghĩ kỹ trước khi quyết định bắt đầu di chuyển mọi thứ xung quanh.

Bạn chắc chắn sẽ tìm thấy các tình huống trong đó tái cấu trúc là cách tiếp cận phù hợp nhưng hãy cẩn thận: mã tái cấu trúc có nhiều khả năng đưa ra các lỗi . Bạn phải đưa ra quyết định đó từ góc độ chủ sở hữu ứng dụng, không phải từ góc độ nhà phát triển. Vì vậy, bạn phải suy nghĩ nếu nỗ lực (tiền) cần thiết để khắc phục vấn đề có đáng để tái cấu trúc hay không. Tôi đã thấy nhiều lần một nhà phát triển dành vài ngày để sửa một cái gì đó không thực sự bị hỏng chỉ vì anh ta nghĩ rằng "mã là xấu".

Lời khuyên thứ ba của tôi là: bạn sẽ bị đốt cháy nếu bạn phá mã, không thành vấn đề nếu đó là lỗi của bạn hay không.

Nếu bạn được thuê để bảo trì, điều đó thực sự không quan trọng nếu ứng dụng bị hỏng do ai đó đưa ra quyết định tồi. Từ góc độ người dùng, nó đã hoạt động trước đây và bây giờ bạn đã phá vỡ nó. Bạn đã phá vỡ nó!

Joel đưa rất tốt vào bài viết của mình giải thích một số lý do tại sao bạn không nên viết lại mã kế thừa.

http://www.joelonsoftware.com/articles/fog0000000069.html

Vì vậy, bạn nên cảm thấy thực sự tồi tệ về loại mã đó (và bạn không bao giờ nên viết bất cứ điều gì như vậy) nhưng duy trì nó là một con quái vật hoàn toàn khác.

Về kinh nghiệm của tôi: Tôi đã phải duy trì mã trong khoảng 1 năm và cuối cùng tôi đã có thể viết lại từ đầu nhưng không phải tất cả cùng một lúc.

Điều đã xảy ra là mã quá tệ đến nỗi các tính năng mới không thể thực hiện được. Các ứng dụng hiện có có vấn đề nghiêm trọng về hiệu suất và khả năng sử dụng. Cuối cùng, tôi được yêu cầu thực hiện một thay đổi sẽ khiến tôi mất 3-4 tháng (chủ yếu là vì làm việc với mã đó khiến tôi mất nhiều thời gian hơn bình thường). Tôi nghĩ rằng tôi có thể viết lại toàn bộ tác phẩm đó (bao gồm triển khai tính năng mới mong muốn) trong khoảng 5-6 tháng. Tôi đã đưa ra đề xuất này cho các bên liên quan và họ đồng ý viết lại nó (may mắn cho tôi).

Sau khi tôi viết lại tác phẩm này, họ hiểu rằng tôi có thể cung cấp những thứ tốt hơn nhiều so với những gì họ đã có. Vì vậy, tôi đã có thể viết lại toàn bộ ứng dụng.

Cách tiếp cận của tôi là viết lại nó từng mảnh. Đầu tiên tôi thay thế toàn bộ UI (Windows Forms), sau đó tôi bắt đầu thay thế lớp giao tiếp (các cuộc gọi dịch vụ Web) và cuối cùng tôi đã thay thế toàn bộ triển khai Máy chủ (nó là một loại ứng dụng máy khách / máy chủ dày).

Một vài năm sau và ứng dụng này đã biến thành một công cụ quan trọng được sử dụng bởi toàn bộ công ty. Tôi chắc chắn 100% rằng sẽ không bao giờ có thể xảy ra nếu tôi không viết lại toàn bộ.

Mặc dù tôi đã có thể làm điều đó nhưng phần quan trọng là các bên liên quan đã phê duyệt và tôi đã có thể thuyết phục họ rằng nó đáng đồng tiền bát gạo. Vì vậy, trong khi bạn phải duy trì ứng dụng hiện tại, hãy cố gắng hết sức để không phá vỡ bất cứ điều gì và nếu bạn có thể thuyết phục chủ sở hữu về lợi ích của việc viết lại thì hãy thử làm như Jack the Ripper: bằng các mảnh.


1
+1. Nhưng câu cuối cùng đề cập đến một kẻ giết người hàng loạt. Điều đó có thực sự cần thiết?
MarkJ

Tôi thích một phần của điều này nhưng đồng thời để lại các thiết kế bị hỏng và thêm các công cụ thường dẫn đến một thiết kế tồi tệ hơn. Tốt hơn hết là tìm ra cách khắc phục và sửa chữa nó, mặc dù theo cách tiếp cận đo lường tất nhiên.
Ricky Clarkson

@RickyClarkson Tôi đồng ý. Thật khó để biết khi nào bạn làm quá sức (tái cấu trúc là không thực sự cần thiết) và khi bạn thực sự làm tổn thương ứng dụng bằng cách thêm các thay đổi.
Alex

@RickyClarkson Tôi đồng ý với điều đó đến mức tôi thậm chí có thể viết lại dự án mà tôi đã tham gia. Nhưng trong một thời gian dài, tôi đã phải cẩn thận thêm các thứ cố gắng gây ra thiệt hại tối thiểu có thể. Trừ khi yêu cầu là khắc phục điều gì đó mà nguyên nhân chính là quyết định thiết kế tồi (thường không phải là trường hợp), tôi muốn nói rằng thiết kế của ứng dụng không nên chạm vào.
Alex

@RickyClarkson Đó là một phần kinh nghiệm của cộng đồng mà tôi muốn tham gia. Tuyên bố tất cả các mã di sản là xấu là một mô hình chống xấu không kém. Tuy nhiên, có những điều trong mã này tôi đang làm việc mà thực sự không nên thực hiện như là một phần của khối cuối cùng. Phản hồi và phản hồi của Alex là những gì tôi đang tìm đọc.

2

Nếu không cần phải thay đổi mã, hãy để nguyên như vậy. Nhưng nếu bạn phải thay đổi mã vì bạn phải sửa một số lỗi hoặc thay đổi một số chức năng, bạn sẽ phải thay đổi nó, nếu bạn muốn hay không. IMHO thường an toàn hơn và ít bị lỗi hơn, nếu trước tiên bạn tái cấu trúc nó thành các phần nhỏ hơn, dễ hiểu hơn và sau đó thêm chức năng. Khi bạn có các công cụ tái cấu trúc tự động trong tay, việc này có thể được thực hiện tương đối an toàn, chỉ với một rủi ro rất nhỏ là đưa ra các lỗi mới. Và tôi khuyên bạn nên lấy một bản sao của cuốn sách Michael Feathers

http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

sẽ cung cấp cho bạn các gợi ý có giá trị về cách làm cho mã dễ kiểm tra hơn, thêm kiểm tra đơn vị và tránh phá mã khi thêm các tính năng mới.

Nếu bạn làm đúng, phương pháp của bạn hy vọng sẽ trở nên đủ đơn giản, lần thử cuối cùng sẽ không chứa bất kỳ mã không liên quan nào ở các vị trí sai nữa.


2

Giả sử bạn có sáu phương thức để gọi, bốn trong số đó chỉ nên được gọi nếu phương thức đầu tiên hoàn thành không có lỗi. Hãy xem xét hai lựa chọn thay thế:

try {
     method1();  // throws
     method2();
     method3();
     method4();
     method5();
 } catch(e) {
     // handle error
 }
 method6();

đấu với

 try {
      method1();  // throws
 }
 catch(e) {
      // handle error
 }
 if(! error ) {
     method2();
     method3();
     method4();
     method5();
 }
 method6();

Trong mã thứ hai, bạn đang xử lý các ngoại lệ như mã trả về. Về mặt khái niệm, điều này giống hệt với:

rc = method1();
if( rc != error ) {
     method2();
     method3();
     method4();
     method5();
 }
 method6();

Nếu chúng ta sẽ bao bọc mọi phương thức có thể đưa ra một ngoại lệ trong khối thử / bắt, thì điểm nào có ngoại lệ trong một tính năng ngôn ngữ? Không có lợi ích và có nhiều đánh máy hơn.

Lý do mà chúng tôi có ngoại lệ trong các ngôn ngữ ngay từ đầu là vì trong những ngày cũ của mã trả về, chúng tôi thường có các tình huống ở trên, nơi chúng tôi có nhiều phương thức có thể trả về lỗi và mỗi lỗi có nghĩa là hủy bỏ hoàn toàn tất cả các phương thức. Khi bắt đầu sự nghiệp, vào những ngày C cũ, tôi đã thấy mã như thế này ở khắp mọi nơi:

rc = method1();
if( rc != error ) {
     rc = method2();
     if( rc != error ) {
         rc = method3();
         if( rc != error ) {
             rc = method4();
             if(rc != error ) {
                 method5();
             }
         }
     }
 }
 method6();

Đây là một mô hình cực kỳ phổ biến và một trường hợp ngoại lệ được giải quyết rất gọn gàng bằng cách cho phép bạn quay lại ví dụ đầu tiên ở trên. Một quy tắc kiểu nói rằng mỗi phương thức có khối ngoại lệ riêng của nó hoàn toàn ném cái này ra khỏi cửa sổ. Tại sao thậm chí bận tâm, sau đó?

Bạn phải luôn nỗ lực để làm cho khối thử vượt qua bộ mã về mặt khái niệm là một đơn vị. Nó sẽ bao quanh mã mà nếu có bất kỳ lỗi nào trong đơn vị mã, thì sẽ không có điểm nào trong việc chạy bất kỳ mã nào khác trong đơn vị mã.

Quay trở lại mục đích ban đầu của các trường hợp ngoại lệ: hãy nhớ rằng chúng được tạo vì thường mã không thể dễ dàng xử lý lỗi ngay khi chúng thực sự xảy ra. Điểm của các trường hợp ngoại lệ là cho phép bạn xử lý các lỗi đáng kể sau này trong mã hoặc tiếp tục lên ngăn xếp. Mọi người quên điều đó và trong nhiều trường hợp, hãy bắt chúng tại địa phương khi họ tốt hơn là chỉ cần ghi lại rằng phương pháp được đề cập có thể đưa ra ngoại lệ này. Có quá nhiều mã trên thế giới trông như thế này:

void method0() : throws MyNewException 
{
    try {
        method1();  // throws MyOtherException
    }
    catch(e) {
        if(e == MyOtherException)
            throw MyNewException();
    }
    method2();
}

Ở đây, bạn chỉ cần thêm các lớp và các lớp thêm nhầm lẫn. Chỉ cần làm điều này:

void method0() : throws MyOtherException
{
    method1();
    method2();
}

Ngoài ra, cảm giác của tôi là nếu bạn tuân theo quy tắc đó và thấy mình có nhiều hơn một vài khối thử / bắt trong phương thức, bạn nên đặt câu hỏi liệu phương thức đó có quá phức tạp không và cần được chia thành nhiều phương thức .

Takeaway: một khối thử nên chứa tập hợp mã cần được hủy bỏ nếu xảy ra lỗi ở bất kỳ đâu trong khối. Cho dù bộ mã này là một dòng hay một nghìn dòng không thành vấn đề. (Mặc dù nếu bạn có một nghìn dòng trong một phương thức, bạn đã gặp các vấn đề khác.)


Steven - cảm ơn bạn đã đưa ra phản hồi tốt, và tôi hoàn toàn đồng ý với suy nghĩ của bạn. Tôi không gặp vấn đề với mã loại liên tục hoặc liên quan hợp lý sống trong một khối thử duy nhất. Nếu tôi có thể đăng một đoạn mã thực tế, nó sẽ hiển thị 5 - 10 cuộc gọi hoàn toàn không liên quan trước cuộc gọi có thể ném đầu tiên. Một khối đặc biệt cuối cùng đã tệ hơn nhiều - một số chủ đề mới đã bị loại bỏ và các thói quen bổ sung, không dọn dẹp được gọi. Và đối với vấn đề đó, tất cả các cuộc gọi trong khối cuối cùng không liên quan đến bất cứ thứ gì có thể ném.

Một vấn đề lớn với cách tiếp cận này là nó không phân biệt giữa các trường hợp method0có thể kỳ vọng rằng một ngoại lệ có thể bị ném và những trường hợp không xảy ra. Ví dụ, giả sử method1đôi khi có thể ném một cái InvalidArgumentException, nhưng nếu không, nó sẽ đưa một đối tượng vào trạng thái tạm thời không hợp lệ, điều này sẽ được sửa chữa method2, dự kiến ​​sẽ luôn đưa đối tượng trở lại trạng thái hợp lệ. Nếu phương thức 2 ném ra InvalidArgumentException, mã dự kiến ​​sẽ bắt ngoại lệ như vậy method1sẽ bắt được nó, mặc dù ...
supercat

... trạng thái hệ thống sẽ phù hợp với mong đợi của mã. Nếu một ngoại lệ được ném từ phương thức 1 phải được xử lý khác với một ngoại lệ được ném bởi phương thức 2, làm thế nào điều đó có thể được thực hiện một cách hợp lý mà không cần bọc chúng?
supercat

Lý tưởng nhất là trường hợp ngoại lệ của bạn đủ chi tiết rằng đây là một tình huống hiếm gặp.
Gort Robot
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.