|
|
||
David Walend's BlogPatterns ArchivesDesign For ExceptionsPosted by dwalend on October 06, 2003 at 04:57 AM | Permalink | Comments (25)I read Bill Venner's interview with James Gosling, "Failure and Exceptions," and with Anders Hejlsberg, "The Trouble with Checked Exceptions," and was a little surprised. I thought exceptions would be in .Net since .Net has taken so many other features from Java. I've never found checked exception clauses to be much of a burden. It's one of my favorite features of Java. Anyway, given all the talk about exceptions and exception handling, I thought I'd take a minute to describe what I do and ask how others use them. I think the problems that Dr. Hejlsberg describes, versionability and scalability, are easy to contain if the development team decides how they're going to handle trouble before they get too deep into their work. Here's what I usually do:
When I start a package, I create a handful of support classes. These classes are simple, but having them there encourages developers to use them instead of creating custom solutions. One of these classes is an abstract exception, a parent for all the concrete exceptions in that package. When developers discover they need a new exception, they extend this exception. The API for the package only throws subclasses of this exception. The abstract exception from JDigraph's net.walend.digraph package looks like this:
If I can't handle a checked exception, I like wrapping the exception with my own subclass of my package's abstract exception, then throw the wrapping exception. My API still only throws subclasses of my abstract exception. The overhead is just constructing the new exception. Wrapping exceptions bounds the scaling problem Dr. Hejlsberg describes by limiting what can appear in the throws clause. Here's the part that's worth discussing: I like keeping my methods' throws list very specific, limited to the concrete exceptions actually thrown. This choice means that people writing code that calls these methods will be cognizant of each exception. If they like, they can still catch the common parent exception. The alternative is having all methods declare that they throw only the same abstract exception. If you believe your public API can never be changed, this is the way to go. This approach solves the versioning problem Dr. Hejlsberg describes by insulating code from changes in the exceptions actually thrown, but does not give great hints for how to handle the problem. For most of my work, the versioning problem is overstated. Most of the source code developers create stays small in scope. I understand that after some point in the development life cycle of code reused across many projects the throws clause in a method can't be changed anymore. If a project becomes popular enough, these detailed throws clauses will break down and having the parent exception in the throws clause is a better decision. I think that popularity is pretty rare: Interfaces from a JSR, or public methods in a popular apache project, or from a core software team at a big company should declare that they throw a general parent exception from day one. However, most code we write has a fairly small and well-defined group of developers. Changing the throws clause doesn't wag many lines of code. That code may need to be changed to handle the new exception in a thoughtful way. If the library is becoming more popular while it is still being developed, adding the parent exception to the end of the throws clauses is easy and well-received. If that last paragraph doesn't generate some interesting talk back, the next two will. When I've tried to use the standard exceptions available in java or javax packages, I've discovered that very few of these exceptions have constructors that take nested exceptions. Generally, developers will use constructors that take an exception, but will not use the initCause() method on their own. I usually have to subclass them to either call the initCause() method in the constructor or override getCause() (to stay compatible with JDK 1.3. See Felipe Leme's latest blog.) I avoid throwing my own RuntimeExceptions except at fairly high levels when it's time to stop or reset the application without cluttering the API. And I'll sometimes use them to mark states I think are impossible. If I control the main event loop, my code catches these exceptions and tries to bow out gracefully, whatever graceful means in the context. I've also used RuntimeExceptions when I was trying to make quick fixes to an existing code base. But I've regretted this shortcut in the past.
Do other people do anything radically different? Is it ever OK to use throws Exception in a declaration? Or, like .Net, to use all RuntimeExceptions no matter what?
Design for Reuse: Source Directory Structures for Java ProjectsPosted by dwalend on September 25, 2003 at 04:56 AM | Permalink | Comments (12)I've found I want to reuse code from almost every project I've ever worked on. Plus other people treat my code as library code years longer than I thought possible. So when I create Java code, I produce reusable .jars of code. Structuring the project correctly at the beginning to help reuse seems to be important, but isn't without cost. I have settled on one way, but am not convinced it's the best. (The examples are from JDigraph, a library for representing and working with directed graphs.) I pack a few complete packages into each .jar I produce. Although it's possible to split a package between multiple .jar files, .jar files work best when one .jar file holds all the .class files from specific packages. The manifest file contains version and security information based on specific packages. Signing a package in a .jar file prevents others from adding code to that package, so splitting a package between .jar files can result in security exceptions. More importantly, if someone asks me about a ClassNotFoundException, I can quickly figure out which .jar file is missing from their classpath. I have a simple rule for figuring out which packages go in which .jar files: If two packages depend on each other -- java.lang and java.io, for example -- then they should go in the same .jar file. If not, they should be in different ones. Making this decision early seems to be important for making smaller pieces reusable. I know two ways to structure source code in Java projects: All-In-One and Many-Subprojects. Both allow me to build the .jars I want to. Neither is perfect. I think Many-Subprojects is best in most cases, but if there's a third, I'd love to hear about it. All-In-One: The most common way I've seen Java projects set up is with all the source code under a common source root:
The great feature of All-In-One is that everyone can find things in the source code. The approach grows organically as the project grows; there aren't any choices beyond deciding when and where to create a new package. People expect to find this sort of structure. It's a simple structure that works well for small projects with few people working on them. The All-In-One approach makes it hard to safely break the kit into smaller reusable .jars. Because the project grows organically, nothing enforces dependency directions between packages. If a developer adds a dependency in the wrong direction, the packages have to be in the same .jar file to safely use either package. If someone adds a dependency, javac will ignore the rules I lay down in your build.xml and reach across the directory structure to compile source code that is supposed to be independent; the build.xml looses control of the source path. The All-In-One approach doesn't scale well. It's fine for three good programmers for about six months, but for twenty developers or for three good developers for two years, it becomes brittle. The build file has to hold the complexity of creating the two separate .jar files. I use ant build.xmls for builds. Ant build.xml files are easy to hack under the pressure of the moment, which makes them brittle over time. Most projects that I've worked on have had increasingly complex build.xml files that get very difficult to maintain and follow. Further, as the project grows larger, it gets harder to use a test-first style and harder to refactor. If I want to try out a change in API, I have to wag all my code to match the new API before I can run my tests. Many-Subprojects: An alternative is Many-Subprojects. A project is made of several subprojects, each with its own build file that builds a single .jar file of library code. Each subproject contains only the source code needed for its root. The uber subproject orchestrates the other build files and creates the final product. It looks like this:
The best thing about the Many-Subprojects approach is that it organizes things into smaller reusable pieces from the start. I can even snap off a subproject and convert it to a new project. There's no shared common source root, so javac can enforce the dependency rules I've laid down. And the build.xml keeps tyrannical control of the classpath. This approach scales well; as I start new packages, I add new subprojects. The build.xml files for the subprojects are generally identical. Some subprojects will need an odd target or two, but the changes stay isolated. The approach encourages refactoring and a test-first approach; I can try out a change in one subproject's test code before propagating the change beyond that subproject. The worst thing about Many-Subprojects is that the directory structure is less intuitive. Few developers have seen the style before. When I start a new package, I have to decide if the new package is independent enough to rate its own .jar file and subproject immediately. Some people find that task difficult. Setting up the structure initially takes more work. All-In-One seems fine for projects with few people and a short life time. Although I usually only work with a few developers, my projects tend to last for years, grow, and create new projects. Converting an All-In-One project to Many-Subprojects is a thankless task that uncovers ugly cross-dependencies, but converting Many-Subprojects to All-In-One is trivial. I use the Many-Subprojects approach from the start.
Does anyone have a third structure that will give me the best of both worlds?
| ||
|
|