Các thực hành tốt nhất để bắt và ném lại ngoại lệ là gì?


156

Các ngoại lệ bị bắt nên được ném lại trực tiếp, hay chúng nên được bọc xung quanh một ngoại lệ mới?

Đó là, tôi nên làm điều này:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw $e;
}

hoặc này:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw new Exception("Exception Message", 1, $e);
}

Nếu câu trả lời của bạn là ném trực tiếp, vui lòng đề nghị sử dụng chuỗi ngoại lệ , tôi không thể hiểu được kịch bản trong thế giới thực nơi chúng ta sử dụng chuỗi ngoại lệ.

Câu trả lời:


287

Bạn không nên bắt ngoại lệ trừ khi bạn có ý định làm điều gì đó có ý nghĩa .

"Một cái gì đó có ý nghĩa" có thể là một trong những điều sau:

Xử lý ngoại lệ

Hành động có ý nghĩa rõ ràng nhất là xử lý ngoại lệ, ví dụ: bằng cách hiển thị thông báo lỗi và hủy bỏ thao tác:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    echo "Error while connecting to database!";
    die;
}

Ghi nhật ký hoặc dọn dẹp một phần

Đôi khi bạn không biết cách xử lý đúng một ngoại lệ trong một bối cảnh cụ thể; có lẽ bạn thiếu thông tin về "bức tranh lớn", nhưng bạn muốn ghi lại sự thất bại càng gần đến điểm xảy ra càng tốt. Trong trường hợp này, bạn có thể muốn bắt, đăng nhập và ném lại:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    logException($e); // does something
    throw $e;
}

Một kịch bản liên quan là nơi bạn đang ở đúng nơi để thực hiện một số dọn dẹp cho hoạt động thất bại, nhưng không quyết định cách xử lý lỗi ở cấp cao nhất. Trong các phiên bản PHP trước, điều này sẽ được thực hiện như

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
catch (Exception $e) {
    $connect->disconnect(); // we don't want to keep the connection open anymore
    throw $e; // but we also don't know how to respond to the failure
}

PHP 5.5 đã giới thiệu finallytừ khóa, vì vậy đối với các kịch bản dọn dẹp, bây giờ có một cách khác để tiếp cận điều này. Nếu mã dọn dẹp cần chạy bất kể điều gì đã xảy ra (nghĩa là cả lỗi và thành công), giờ đây bạn có thể làm điều này trong khi cho phép minh bạch mọi ngoại lệ bị ném để tuyên truyền:

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
finally {
    $connect->disconnect(); // no matter what
}

Lỗi trừu tượng (có chuỗi ngoại lệ)

Trường hợp thứ ba là nơi bạn muốn nhóm một cách hợp lý nhiều thất bại có thể xảy ra dưới một chiếc ô lớn hơn. Một ví dụ cho nhóm hợp lý:

class ComponentInitException extends Exception {
    // public constructors etc as in Exception
}

class Component {
    public function __construct() {
        try {
            $connect = new CONNECT($db, $user, $password, $driver, $host);
        }
        catch (Exception $e) {
            throw new ComponentInitException($e->getMessage(), $e->getCode(), $e);
        }
    }
}

Trong trường hợp này, bạn không muốn người dùng Componentbiết rằng nó được triển khai bằng kết nối cơ sở dữ liệu (có thể bạn muốn giữ các tùy chọn của mình mở và sử dụng lưu trữ dựa trên tệp trong tương lai). Vì vậy, đặc điểm kỹ thuật của bạn Componentsẽ nói rằng "trong trường hợp lỗi khởi tạo, ComponentInitExceptionsẽ bị ném". Điều này cho phép người tiêu dùng Componentnắm bắt các ngoại lệ của loại dự kiến đồng thời cho phép mã gỡ lỗi truy cập tất cả các chi tiết (phụ thuộc vào triển khai) .

Cung cấp bối cảnh phong phú hơn (với chuỗi ngoại lệ)

Cuối cùng, có những trường hợp bạn có thể muốn cung cấp thêm ngữ cảnh cho ngoại lệ. Trong trường hợp này, sẽ rất hợp lý khi bọc ngoại lệ trong một trường hợp khác chứa nhiều thông tin hơn về những gì bạn đã cố gắng thực hiện khi xảy ra lỗi. Ví dụ:

class FileOperation {
    public static function copyFiles() {
        try {
            $copier = new FileCopier(); // the constructor may throw

            // this may throw if the files do no not exist
            $copier->ensureSourceFilesExist();

            // this may throw if the directory cannot be created
            $copier->createTargetDirectory();

            // this may throw if copying a file fails
            $copier->performCopy();
        }
        catch (Exception $e) {
            throw new Exception("Could not perform copy operation.", 0, $e);
        }
    }
}

Trường hợp này tương tự như ở trên (và ví dụ có lẽ không phải là trường hợp tốt nhất có thể xảy ra), nhưng nó minh họa điểm cung cấp thêm ngữ cảnh: nếu một ngoại lệ được ném, nó cho chúng ta biết rằng sao chép tệp thất bại. Nhưng tại sao nó lại thất bại? Thông tin này được cung cấp trong các trường hợp ngoại lệ được bao bọc (trong đó có thể có nhiều hơn một cấp nếu ví dụ phức tạp hơn nhiều).

Giá trị của việc này được minh họa nếu bạn nghĩ về một kịch bản, ví dụ như việc tạo một UserProfileđối tượng khiến các tệp bị sao chép vì hồ sơ người dùng được lưu trữ trong các tệp và nó hỗ trợ ngữ nghĩa giao dịch: bạn có thể "hoàn tác" các thay đổi vì chúng chỉ được thực hiện trên một bản sao của hồ sơ cho đến khi bạn cam kết.

Trong trường hợp này, nếu bạn đã làm

try {
    $profile = UserProfile::getInstance();
}

và kết quả là một lỗi ngoại lệ "Không thể tạo thư mục đích", bạn sẽ có quyền bị nhầm lẫn. Việc gói ngoại lệ "lõi" này trong các lớp của các ngoại lệ khác cung cấp ngữ cảnh sẽ giúp xử lý lỗi dễ dàng hơn nhiều ("Tạo bản sao hồ sơ không thành công" -> "Không thể tạo thư mục tệp" -> "Không thể tạo thư mục đích").


Tôi chỉ đồng ý với 2 lý do cuối cùng: 1 / xử lý ngoại lệ: bạn không nên làm điều đó ở cấp độ này, 2 / ghi nhật ký hoặc dọn dẹp: cuối cùng sử dụng và đăng nhập ngoại lệ trên trình dữ liệu của bạn
remi bourgarel

1
@remi: ngoại trừ việc PHP không hỗ trợ finallycấu trúc (ít nhất là chưa) ... Vì vậy, điều đó có nghĩa là chúng ta phải dùng đến những thứ bẩn thỉu như thế này ...
ircmaxell

@remibourgarel: 1: Đó chỉ là một ví dụ. Tất nhiên bạn không nên làm điều đó ở cấp độ này, nhưng câu trả lời là đủ dài. 2: Như @ircmaxell nói, không có finallyPHP.
Jon

3
Cuối cùng, PHP 5.5 thực hiện cuối cùng.
OCDev

12
Có một lý do tôi nghĩ rằng bạn đã bỏ lỡ trong danh sách của mình ở đây - bạn có thể không biết liệu bạn có thể xử lý một ngoại lệ cho đến khi bạn bắt được nó và có cơ hội kiểm tra nó không. Ví dụ: trình bao bọc cho API cấp thấp hơn sử dụng mã lỗi (và có hàng trăm mã) có thể có một lớp ngoại lệ duy nhất mà nó đưa ra một trường hợp cho bất kỳ lỗi nào, với một thuộc error_codetính có thể được kiểm tra để nhận lỗi cơ bản mã. Nếu bạn chỉ có thể xử lý một cách có ý nghĩa một số lỗi đó, thì có lẽ bạn muốn nắm bắt, kiểm tra và nếu bạn không thể xử lý lỗi - hãy thử lại.
Đánh dấu Amery

37

Vâng, đó là tất cả về việc duy trì sự trừu tượng. Vì vậy, tôi đề nghị sử dụng chuỗi ngoại lệ để ném trực tiếp. Theo như tại sao, hãy để tôi giải thích khái niệm trừu tượng bị rò rỉ

Giả sử bạn đang xây dựng một mô hình. Mô hình được cho là trừu tượng hóa tất cả sự tồn tại và xác nhận dữ liệu từ phần còn lại của ứng dụng. Vậy bây giờ điều gì xảy ra khi bạn gặp lỗi cơ sở dữ liệu? Nếu bạn suy nghĩ lại DatabaseQueryException, bạn đang rò rỉ sự trừu tượng. Để hiểu tại sao, hãy nghĩ về sự trừu tượng trong một giây. Bạn không quan tâm làm thế nào mô hình lưu trữ dữ liệu, chỉ có vậy. Tương tự như vậy, bạn không quan tâm chính xác những gì đã sai trong các hệ thống cơ bản của mô hình, chỉ là bạn biết rằng đã xảy ra sự cố và xấp xỉ những gì đã sai.

Vì vậy, bằng cách điều chỉnh lại DatabaseQueryException, bạn đang rò rỉ sự trừu tượng hóa và yêu cầu mã gọi để hiểu ngữ nghĩa của những gì đang diễn ra trong mô hình. Thay vào đó, hãy tạo một cái chungModelStorageException và bọc DatabaseQueryExceptionbên trong cái đó. Theo cách đó, mã cuộc gọi của bạn vẫn có thể cố gắng xử lý lỗi về mặt ngữ nghĩa, nhưng nó không thành vấn đề với công nghệ cơ bản của Mô hình do bạn chỉ phơi bày các lỗi từ lớp trừu tượng đó. Thậm chí tốt hơn, vì bạn đã bao bọc ngoại lệ, nếu nó nổi bong bóng lên và cần phải đăng nhập, bạn có thể theo dõi ngoại lệ gốc được ném (đi theo chuỗi) để bạn vẫn có tất cả thông tin gỡ lỗi mà bạn cần!

Đừng chỉ đơn giản là bắt và lấy lại ngoại lệ tương tự trừ khi bạn cần thực hiện một số xử lý hậu kỳ. Nhưng một khối như } catch (Exception $e) { throw $e; }là vô nghĩa. Nhưng bạn có thể bọc lại các ngoại lệ cho một số mức tăng trừu tượng đáng kể.


2
Câu trả lời chính xác. Có vẻ như khá nhiều người xung quanh Stack Overflow (dựa trên câu trả lời, v.v.) đang sử dụng sai.
James

8

IMHO, bắt một Ngoại lệ để chỉ nghĩ lại nó là vô ích . Trong trường hợp này, chỉ cần không bắt nó và để các phương thức được gọi trước đó xử lý nó (còn gọi là các phương thức 'trên' trong ngăn xếp cuộc gọi) .

Nếu bạn nghĩ lại, xâu chuỗi ngoại lệ bị bắt vào cái mới bạn sẽ ném chắc chắn là một cách thực hành tốt, vì nó sẽ giữ thông tin mà ngoại lệ bị bắt có. Tuy nhiên, việc xem xét lại nó chỉ hữu ích nếu bạn thêm một số thông tin hoặc xử lý một cái gì đó vào ngoại lệ bị bắt, có thể đó là một số bối cảnh, giá trị, ghi nhật ký, giải phóng tài nguyên, bất cứ điều gì.

Một cách để thêm một số thông tin để mở rộng Exceptionlớp, có trường hợp ngoại lệ như NullParameterException, DatabaseExceptionvv Hơn nữa, điều này cho phép các developper để chỉ bắt một số trường hợp ngoại lệ mà ông có thể xử lý. Ví dụ, người ta chỉ có thể bắt DatabaseExceptionvà cố gắng giải quyết nguyên nhân gây ra Exception, như kết nối lại với cơ sở dữ liệu.


2
Nó không phải là vô dụng, có những lúc bạn cần phải làm một điều gì đó ngoại lệ, hãy nói trong chức năng ném nó và sau đó ném nó lại để cho một người bắt cao hơn làm việc khác. Trong một trong những dự án tôi đang thực hiện, đôi khi chúng tôi bắt gặp một ngoại lệ trong một phương thức hành động, hiển thị một thông báo thân thiện cho người dùng và sau đó ném lại để một khối bắt thử trong mã có thể bắt lại để ghi lại lỗi một bản ghi.
MitMaro

1
Vì vậy, như tôi đã nói, bạn thêm một số thông tin vào ngoại lệ (hiển thị thông báo, ghi nhật ký). Bạn không chỉ nghĩ lại như trong ví dụ của OP.
Clement Herreman

2
Chà, bạn chỉ có thể lấy lại nó nếu bạn cần đóng tài nguyên, nhưng không có thêm thông tin để thêm. Tôi đồng ý rằng đó không phải là thứ sạch nhất trên thế giới, nhưng nó không khủng khiếp
ircmaxell

2
@ircmaxell Đồng ý, được chỉnh sửa để phản ánh rằng nó chỉ vô dụng nếu bạn không làm gì ngoại trừ việc lấy lại nó
Clement Herreman

1
Điều quan trọng là bạn mất tập tin và / hoặc thông tin dòng về nơi ngoại lệ ban đầu được ném bằng cách ném lại. Vì vậy, tốt hơn hết là nên trow một cái mới và chuyển cái cũ theo, như trong ví dụ thứ 2 của câu hỏi. Nếu không, nó sẽ chỉ vào khối bắt, để bạn đoán vấn đề thực sự là gì.
DanMan

2

Bạn phải xem qua các thực tiễn tốt nhất về ngoại lệ trong PHP 5.3

Xử lý ngoại lệ trong PHP không phải là một tính năng mới. Trong liên kết sau, bạn sẽ thấy hai tính năng mới trong PHP 5.3 dựa trên các ngoại lệ. Đầu tiên là các ngoại lệ lồng nhau và thứ hai là một tập hợp các loại ngoại lệ mới được cung cấp bởi phần mở rộng SPL (hiện là phần mở rộng cốt lõi của thời gian chạy PHP). Cả hai tính năng mới này đã tìm thấy đường vào cuốn sách thực hành tốt nhất và xứng đáng được xem xét chi tiết.

http://ralphschindler.com/2010/09/15/exception-best-practices-in-php-5-3


1

Bạn thường nghĩ về nó theo cách này.

Một lớp có thể đưa ra nhiều loại ngoại lệ không phù hợp. Vì vậy, bạn tạo một lớp ngoại lệ cho lớp hoặc loại lớp đó và ném nó.

Vì vậy, mã sử dụng lớp chỉ phải bắt một loại ngoại lệ.


1
Hey bạn có thể cung cấp thêm một số chi tiết hoặc một liên kết nơi tôi có thể đọc thêm về phương pháp này.
Rahul Prasad
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.