blogTake advantage of Eiffel Exceptions as Objects

ted_eiffel's picture

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.

Syndicate content