when do I write my own exception class?

后端 未结 4 1193
感情败类
感情败类 2021-02-02 12:00

I have been wondering since I stepped into the murky waters of OOP and have written a couple or so of distributed libraries when it is necessary to write my own extension of the

4条回答
  •  离开以前
    2021-02-02 13:03

    You should extend the Exception class with your own Exception types when you need to differentiate between different types of errors. Throwing an Exception just means something went wrong. You have no idea what went wrong though. Should you abort everything? Is this an expected error? Throwing a UserIsNotAllowedToDoThisException instead means something much more specific. The importance is to differentiate what code can handle what kind of error:

    try {
        new Foo($bar);
    } catch (UserIsNotAllowedToDoThisException $e) {
        echo "Sorry, you're not allowed to do this.";
    }
    

    This code handles the simple case when something is not allowed. If Foo would throw some other exception, like TheDatabaseJustCrashedAndIsBurningException, you don't want to know about this here, you want some global error handler to handle it. By differentiating what went wrong, it allows you to handle problems appropriately.


    OK, here a little more complete example:

    First, if you use proper OOP, you need Exceptions to fail object constructions. Without being able to fail object constructions, you're ignoring a large part of OOP: type safety and therefore data integrity. See for example:

    class User {
    
        private $name = null;
        private $db = null;
    
        public function __construct($name, PDO $db) {
            if (strlen($name) < 3) {
                throw new InvalidArgumentException('Username too short');
            }
            $this->name = $name;
            $this->db = $db;
            $this->db->save($this->name);  // very fictional DB call, mind you
        }
    
    }
    

    In this example, we see a lot of things:

    • My User objects have to have a name. Failing to pass a $name argument to the constructor will make PHP fail the whole program.
      • The username needs to be at least 3 characters long. If it is not, the object cannot be constructed (because an Exception is thrown).
    • My User objects have to have a valid and working database connection.
      • Failing to pass the $db argument will make PHP fail the whole program.
      • Failing to pass a valid PDO instance will make PHP fail the whole program.
        • I can't pass just anything as the second argument, it needs to be a valid PDO object.
        • This means if the construction of a PDO instance succeeded, I have a valid database connection. I do not need to worry about or check the validity of my database connection henceforth. That's the same reason I'm constructing a User object; if the construction succeeds, I have a valid user (valid meaning his name is at least 3 characters long). I do not need to check this again. Ever. I only need to type hint for User objects, PHP takes care of the rest.

    So, you see the power that OOP + Exceptions gives you. If you have an instance of an object of a certain type, you can be 100% assured that its data is valid. That's a huge step up from passing data arrays around in any halfway complex application.

    Now, the above __construct may fail due to two problems: The username being too short, or the database is for whatever reason not working. The PDO object is valid, so the connection was working at the time the object was constructed, but maybe it's gone down in the meantime. In that case, the call to $db->save will throw its own PDOException or a subtype thereof.

    try {
        $user = new User($_POST['username'], $db);
    } catch (InvalidArgumentException $e) {
        echo $e->getMessage();
    }
    

    So I'd use the above code to construct a User object. I do not check beforehand whether the username is at least 3 characters long, because this would violate the DRY principle. Instead, I'll just let the constructor worry about it. If the construction fails with an InvalidArgumentException, I know the username was incorrect, so I'll let the user know about that.

    What if the database is down though? Then I cannot continue to do anything in my current app. In that case I want to halt my application completely, displaying an HTTP 500 Internal Server Error page. Here's one way to do it:

    try {
        $user = new User($_POST['username'], $db);
    } catch (InvalidArgumentException $e) {
        echo $e->getMessage();
    } catch (PDOException $e) {
        abortEverythingAndShowError500();
    }
    

    But this is a bad way. The database may fail at any time anywhere in the application. I do not want to do this check at every point I'm passing a database connection to anything. What I'll do instead is I let the exception bubble up. In fact, it has already bubbled up. The exception was not thrown by new User, it was thrown in a nested function call to $db->save. The Exception has already traveled up at least two layers. So I'll just let it travel up even further, because I have set up my global error handler to deal with PDOExceptions (it's logging the error and displays a nice error page). I do not want to worry about this particular error here. So, here it comes:

    Using different types of Exceptions allows me to ignore certain types of errors at certain points in my code and let other parts of my code handle them. If I have an object of a certain type, I do not ever have to question or check or worry about its validity. If it wasn't valid, I wouldn't have an instance of it in the first place. And, if it ever fails to be valid (like a suddenly failing database connection), the object can signal by itself that an error occurred. All I need to do is catch the Exception at the right point (which can be very high up), I do not need to check whether something succeeded or not at every single point in my program. The upshot is less, more robust, better structured code. In this very simple example, I'm only using a generic InvalidArgumentException. In somewhat more complex code with objects that accept many arguments, you'd probably want to differentiate between different types of invalid arguments. Hence you'd make your own Exception subclasses.

    Try to replicate this by using only one type of Exception. Try to replicate this using only function calls and return false. You need a lot more code to do so every time you need to make that check. Writing custom exceptions and custom objects is a little more code and apparent complexity upfront, but it saves you a ton of code later and makes things much simpler in the long run. Because anything that shouldn't be (like a user with a too short username) is guaranteed to cause an error of some sort. You don't need to check every time. On the contrary, you only need to worry about at which layer you want to contain the error, not whether you'll find it at all.

    And it's really no effort to "write your own Exceptions":

    class UserStoppedLovingUsException extends Exception { }
    

    There, you have created your own Exception subclass. You can now throw and catch it at the appropriate points in your code. You don't need to do any more than that. In fact, you have now a formal declaration of the types of things that may go wrong in your app. Doesn't that beat a lot of informal documentation and ifs and elses and return falses?

提交回复
热议问题