eiffelroom

blogTake advantage of Eiffel Exceptions as Objects

I was asked what the benefit of Exceptions as Objects (EAO) was in Eiffel. My short answer was flexibility of design. I knew that this answer was far from sufficient. Now I take the time my machine is busy running tests to think about it deeper and write it down. Hopefully people who are trying using it or starting to learn this area of Eiffel may better understand the mechanism and its benefits. What I am talking maybe far from sufficient too and don't blame me I know I am not a good English writer.

Before we go into the subject EAO, I would talk about some basics of exception handling and the previous way of exception handling in Eiffel.

Real applications normally have the situations they run into exceptional context in which they don't have enough information to deal with such exceptional cases. Hence they have to do something to flag the rest of the world possibly with the interesting information of exceptional contexts. Informing in applications implies handling, though exception handling could be nothing. Without good exception handling mechanism, coding to handle various exceptional cases is a nightmare and the code is almost unreadable. A good exception handling mechanism perfectly decouples the code for normal application processes and that for exception handling. Thank Eiffel for its born readability. The built-in rescue-retry exception handing mechanism has been good enough to achieve the decoupling. Codes are well arranged and of really nice readability. Consider the following piece of code: Example1

class
    APP

create
    make

feature {NONE} -- Initialize

    make
        do
            normal_process
        rescue
            handle_my_exception
            retry
        end

feature {NONE} -- Normal logic of the program
   
    normal_process
        do
            if not is_initialized then
                exceptions.raise ("Not initialized.")
            else
                -- do something else.
            end
        end

    is_initialized: BOOLEAN

feature {NONE} -- Exception handling

    handle_my_exception
            -- Code to handle exceptions
        do
            is_initialized := True
        end

feature {NONE}

    exceptions: EXCEPTIONS
        once
            create Result
        end

end

In Example1, an exception is raised in `normal_process' with the tag of "Not initialized." (It is call "message" in EAO), when not `is_initialized'. For `normal_process', this is an exceptional context, it doesn't know how to fix this, so raises it to higher level that knows better the context and how to handle the not `is_initialized' exception. Now rescue of `make' handles the exception. `is_initialized' is simply set with True. One may argue that `normal_process' here knows enough information to handle the case not `is_initialized'. But the reality is much more complicated, where `is_initialized' may not be a field or the `normal_process' is a client of a library in which exception is raised.

In Eiffel, once an exception is raised, the calling routine aborts, the runtime backtracks the call stack to find the nearest rescue and step into the rescue. If no rescue is found, backtracking reaches the bottom of the call stack where a execution vector with a hidden rescue was pushed. The hidden rescue of course is executed to do necessary clean-ups and also to print the exception call stack. That 's what people usually see in console if an Eiffel application crashes.

Exceptions as Objects (EAO)

Now EAO is something new, but keep in mind the rescue-retry mechanism is not obsolete. The way of rescuing and internally backtracking don't change at all. The ultimate change is the ability of information encapsulation from which we will benefit a lot. I will talk about it later. The new mechanism, as the name implies, is based on objects. In this article, http://dev.eiffel.com/Exceptions_as_Objects , one can see the exception hierarchy, interfaces and some other topics. EXCEPTION is the top most class in the hierarchy, which means all exceptions derive from it. {EXCEPTION}.raise raises the exception object. In previous way of EXCEPTIONS class, take the Example1 as an instance, the exception raised was only a code `developer_exception' defined in the class EXCEP_CONST and the tag, a string of "Not initialized.". Raising exceptions in Eiffel was almost fully governed by the class EXCEPTIONS. Apart from this class, nothing exception related had to do with objects, it was code based exception handling.

Example2:

class
    APP

create
    make

feature {NONE} -- Initialize

    make
        do
            normal_process
        rescue
            handle_my_exception ((create {EXCEPTION_MANAGER}).last_exception)
            retry
        end

feature {NONE} -- Normal logic of the program
   
    normal_process
        local
            l_exception: NON_INITIALIZED_EXCEPTION
        do
            if not is_initialized then
                create l_exception
                l_exception.set_message ("Not initialized.")
                l_exception.raise
            else
                -- do something else.
            end
        end

    is_initialized: BOOLEAN

feature {NONE} -- Exception handling

    handle_my_exception (e: EXCEPTION)
            -- Code to handle exceptions
        do
            is_initialized := True
        end

end
class NON_INITIALIZED_EXCEPTION

inherit
    DEVELOPER_EXCEPTION

feature

end
In Example2, a developer exception is defined as class NON_INITIALIZED_EXCEPTION. In the exceptional context not `is_initialized' in `normal_process', the developer defined exception is created by developer, filled with exceptional information "Not initialized" and raised by calling `raise'. Once `raise' is call, the following code, if any, in that routine wouldn't be called. The nearest rescue is hit in `make'. The exception object, containing exceptional information, can be accessed by calling {EXCEPTION_MANAGER}.last_exception. With the exception object, `handle_my_exception', who knows better how to handle the exceptional context, is able to do more if needed.

From the whole system perspective, what is the EAO doing? The answer is simple. It provides a universal way to store and access exceptional context information wrapped as objects, and the scope is from deep into the runtime to very top of the application. Example2 demonstrates how developer exceptions are raised and get handled. We can think about what it is the situation if NON_INITIALIZED_EXCEPTION, `is_initialized' and the call `normal_process' is defined in a library. We find that it is the same thing, but better explains how one benefits doing the library when he knows nothing about how the situation should be handled by its client application. Responsibility is brought in here and very clear that who is responsible to collect exceptional information and who is responsible to handle it with that information. Maybe one is more interested in system raised exceptions. From my point of view, there is nothing really special compared with those raised by developers. The difference is that one particular Eiffel compiler with its runtime might have different implementation, and those system raised exceptions are created and raised by the runtime or by code generated from the compiler. System raised exceptions could be raised anywhere in the code, in most cases, they implies bugs. Finally, about system raised exceptions, some of them, such as operating system failures, are actually a handler in the runtime who does mappings between exceptions raised from the OS and the language exceptions.

As the idea of exception handling is introduced simply, hopefully not too simple, I can go ahead the benefits of EAO as I can see.

Benefits

Encapsulation

As stated earlier, compared with the previous implementation of exception handling in Eiffel, the ultimate enhancement of EAO is information encapsulation. People benefit from object-oriented way of this good manner of information encapsulation. Code is clear, when all exceptional information is encapsulated in the exception object which is directly accessible later through EAO mechanism when handling. Let's have a look at complexer example Example3:

class
    MY_APP

feature
    read_data
        local
            l_reader: DB_READER
        do
            create l_reader.make (new_connection_string)
            l_reader.read
        rescue
            if (ex: !CONNECTION_FAILURE)exception_manager.last_exception then
                print ("ex.message" + "%N")
                print ("Name: " + ex.name + "%N")
                print ("Domain: " + ex.domain + "%N")
                print ("password: " + ex.passwd + "%N")
            end
            retry
        end

    new_connection_string: STRING
        do
            -- Code to get new connection string.
        end

    exception_manager: EXCEPTION_MANAGER
        once
            create Result
        end

end
In library "database":
class
    DB_READER

create
    make

feature {NONE} -- Initialization

    make (a_str: like connection_string)
        do
            connection_string := a_str
        end

feature -- Element change

    set_connection_string (a_str: like connection_string)
        do
            connection_string := a_str
        end

feature -- Actions

    read
        do
            try_connect
            -- Read action ommitted.
        end

feature {NONE}

    try_connect is
        local
            l_domain, l_name, l_passwd: STRING
            l_exception: CONNECTION_FAILURE
        do
            l_name = extract_name (connection_string)
            l_domain = extract_domain (connection_string)
            l_passwd = extract_passwd (connection_string)
            connect (l_domain, l_name, l_passwd)
            if not is_connected then
                create l_exception.make (l_name, l_domain, l_passwd, "Connection failed!")
                l_exception.raise
            end
        end

    connect (domain, name, passwd: STRING): BOOLEAN is
        do
            -- Connect
        end

    is_connected: BOOLEAN
   
    connection_string: STRING

end
class CONNECTION_FAILURE

inherit
    DEVELOPER_EXCEPTION

create
    make

feature {NONE} -- Initializaton

    make (a_domain, a_name, a_passwd, a_message: STRING)
        do
            doamin := a_domain
            name := a_name
            passwd := a_passwd
            set_message (a_message)
        end

feature -- Access
   
    domain, name, passwd: STRING

end

Of course Example3 is code from a real system, there are a lot more things we need to do within real systems. And I don't add any contract in this piece of code, since I want to focus more on the exception handling. In this example, we see domain, name and passwd as exceptional context which is saved in the exception object CONNECTION_FAILURE. In this exceptional context of the library "database", there is no information how it should proceed, so the exception is raised to upper level. We can think about how we did with old implementation of exception handling. What we supposed to do was simply call `raise ("Connection failed!")'. At the client side in rescue, only information of the exception code and message was available. People could have their own way work around. He could put information like domain, name and passwd in the DB_READER as queries or had his own class to wrap this information and make it available somewhere accessible in the library. But as you see, those ways working around were awkard, since for clients, it was difficult to know where the useful information was. EAO now does the good job, it is a standard way for the library developers to encapsulate exceptional information and for the clients to get the information.

Extendibility

This is what we benefit from object-oriented method. Inheritance, polymorphism and so on. The previous way of exception handling only have one code for developer exceptions. That's far from enough. Now one can use the type system to define it own exceptions as many as he may want, as long as it inherits from DEVELOPER_EXCEPTION. Still use Example3, in the "database" library, there can be many kind of connection failures like DB2_CONNECTION_FAILURE, ORACLE_CONNECTION_FAILURE and so on. They all inherit from CONNECTION_FAILURE. At client side, there is no particular change should be done if details are not that important. One may also notice that with this extension, there is no need for the client to know each kind of exceptions. But in previous way, one may have to write code like this:

foo
do
    ...
rescue
    if db2_connection_failure then
    elseif oracle_connection_failure then
    end
end
Or somewhere has a query like this. This is an example of better extendibility people benefit from inheritance. I believe one can easily have his own example of better extendibility from polymorphism.

Maintenance

With object-oriented method, it has been proved that code are can be easier maintained. I don't think I need to talk about this, there are a bunch of books about it.

Better Debugging

ISE debugger has supported EAO, which means that it will be easier to debug when with the caught exception object in the debugger. Since exception objects are proper places to collect informative exception context. With a single object view, one can get useful information like the trace and, more important, the information filled by the coder who intended to expose it.

I am stopping here. I am too lazy to explore more. But I will extend it if I find more.

about - contact