Saturday, May 14, 2016

Exception Driven Development

Exception driven development

Exception driven development

Programming credo I lived by, is that: the line of code is the most elegant, has by far the least bugs, is the easiest to write, is the easiest to someone else to understand your code, that line of code you have never write. -- Paul Hegarty

0. Motivation

I think every programmer asks himself sooner or later when to catch exception, when to throw exception and when return null and when return exception. After more then two years in JavaEE development I have gained some good practices which I'd like to share with others.

I will base my example on a simple WebService which responsible for getting book by provided id of type int from database. What our requirements from this WebService?

  1. If book found it will return HTTP code 200 and Json representation of the book
  2. If it book isn't exists it will return 404 error. Why? See explanation here: When to use HTTP status code 404 in an API
  3. If it didn't due to error (database error, id contains wrong characters, etc) it will return an appropriate error message

So we have requirements now, lets try to implement this with less lines of code while trying to keep it simple, readable, maintainable and expandable.

1. Skeleton

We should start from something, sometimes it's most difficult part of a process, so let make it simple. The method below receives parameter, prints it and returns HTTP code 200, everything ok.

@Path("book")
public class BookAPI {
    
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response getBookById(@QueryParam(value = "id") String bookIdString) {
        
        System.out.println("The book id is: " + bookIdString);
        
        return Response.ok().build();
    }
}

2. Parsing String to Integer

We will use the following line of code to parse string to integer:

int bookId = Integer.valueOf(bookIdString);

But what will happen if we get unparsable string for example something like 123a? In such situation static Integer valueOf(String s) will throw an NumberFormatException which we need to catch and return meaningful HTTP response.

Actually the gold rule is following: no exception should be ignored. The following lines of code are inappropriate:

try{
 ...
 //code which can throw an exception
 ...
}catch{Exception e}{
   //who cares?
}

So we will handle it as follow:

@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getBookById(@QueryParam(value = "id") String bookIdString) {
    System.out.println("The book id is: " + bookIdString);
    try {
        int bookId = Integer.valueOf(bookIdString);
    } catch (NumberFormatException e) {
        e.printStackTrace();
        return Response.status(Status.BAD_REQUEST).build();
    }
    return Response.ok().build();
}

Now if NumberFormatException will be thrown we will print error to IDE console and return HTTP error code 400 which meaning:

The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repeat the request without modifications.

If everything is ok our WebService will return HTTP code 200 which meaning:

The request has succeeded. The information returned with the response is dependent on the method used in the request.

3. Method to retrieve book by id from database

Lets write code which will do actual work, return book from database.

public class DbBookReader {
    public static Book getBookById(int id) throws SQLException, BookNotFoundException{
        Book book = null;
        
        Connection con = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        
        
        String sql = "SELECT * FROM BOOKS WHERE ID = ?";
        
        try {
            //ConnectionProvider is helper class to get Connections
            con = ConnectionProvider.getConnection(); 
            
            ps = con.prepareStatement(sql);
            ps.setInt(1, id);
            rs = ps.executeQuery();
            
            while(rs.next()){
                book = new Book();
                book.setId(id);
                book.setTitle(rs.getString("TITLE"));
                book.setAuthorName(rs.getString("AUTHOR_NAME"));
            }
            
            if(book == null){
                throw new BookNotFoundException(id);
            }
            
        } catch (SQLException e) {
            e.printStackTrace();   //pay attention that we can have multiple nested SQLExceptions here
            throw e;
        }
        
        return book;
    }
}

What do happen here? First of all we don't return null if book with such id doesn't exist, but throwing BookNotFoundException

public class BookNotFoundException extends Exception {
    private static final long serialVersionUID = 1L;

    public BookNotFoundException(int id){
        super("Book with id: " + id + " wasn't found");
    }
}

Why throwing an exception and not returning null? Actually it's matter of convention, in this method I specify an id of book which I expect does exist, if it doesn't it's error and BookNotFoundException should be thrown. If we just perform search and return for example Set of books which can contain multiple books or can be empty we won't be throwing exception, but returning empty Set, for example like this:

public static List<Book> getBooksById(int id) throws SQLException{
        ...
} 

Another reason is behavior consistency across different part of our program. If my object doesn't exists our WebService will return 404 not 200 and empty object, so we'll expect that method which used by WebService will return exception too, not null object.

Next interesting thing is catching SQLException and then immediately throwing it back, why do we need it? We catching exception ASAP to make our debugging life easier, you won't want to move back and force across your code to see which exception were thrown and why was it thrown, for more details see this answer: Exceptions: Why throw early? Why catch late?. And we rethrowing it to let our WebService know that something went wrong, you'll see how it work in final implementation of our WebService.

4.WebService - Final Implementation

And this is our result:

@Path("book")
public class BookAPI {
    
    private static Gson gson = new GsonBuilder()    //Our serializer based on Google Gson library
                                .setPrettyPrinting()
                                .create();
    
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response getBookById(@QueryParam(value = "id") String bookIdString) {
        
        System.out.println("The book id is: " + bookIdString);
        
        try {
            int bookId = Integer.valueOf(bookIdString);
            Book book = DbBookReader.getBookById(bookId);
            String json = gson.toJson(book);
            return Response.ok().entity(json).build(); 
        } catch (NumberFormatException e) {
            e.printStackTrace();
            return Response.status(Status.BAD_REQUEST).entity(gson.toJson(e)).build();
        } catch (SQLException e) {
            e.printStackTrace();
            return Response.status(Status.INTERNAL_SERVER_ERROR).entity(gson.toJson(e)).build();
        } catch (BookNotFoundException e) {
            e.printStackTrace();
            return Response.status(Status.NOT_FOUND).entity(gson.toJson(e)).build();
        } 
        
    }
}

We have added Gson object which serialize our Book object to Json String:

private static Gson gson = new GsonBuilder()    
                                .setPrettyPrinting()
                                .create();

In try block we're trying to perform our job:

int bookId = Integer.valueOf(bookIdString);
Book book = DbBookReader.getBookById(bookId);
String json = gson.toJson(book);
return Response.ok().entity(json).build();

If everything will work as expected we will get Book object, convert it to Json and then return response 200 with our json as entity, and our client we will see:

{
  "id": 5,
  "title": "Atlas Shrugged",
  "authorName": "Ayn Rand"
}

If something went wrong one of our catch clauses will be activated and will handle it, returning one of the following HTTP errors codes: 400, 404 or 500.

5. Conclusion

  • Our Webservice easy to read and/or maintain, it has linear workflow without multiple if/else.
  • It's expandable. If we need to handle another error we just add catch clause, without changing general workflow.
  • We not only returning appropriate HTTP codes we also serialize and return Exception's message. It can help debugging process, make application more user friendly or just do nothing it's wholly up to client's implementation.

Source Code for this example at GitHub

No comments:

Post a Comment