Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

{toc}

...

Summary

Good exception design can be useful in communicating the semantics of an operation and improve method factoring. This requires looking beyond the mechanical compilation and to what each method means to its consumer.

Table of Contents
excludeSummary

Introduction

OSIDs strictly define the permissible checked an unchecked exceptions which may pass. They are straightforward in the most basic of method implementations. However, some semantic analysis is necessary when creating a chain of methods to help convey what may have gone wrong to your consumer or ultimately your end user.

Basic Examples

The Decorator

In a decorator pattern (same holds true for adapter patterns across instantiated OSID Providers), the same method is used in both layers. The method signatures line up and more importantly the semantics of the method are identical so the exceptions can just bubble through.

Code Block
languagejava
class AssetLookupLoggingDecorator
    implements org.osid.repository.AssetLookupSession {
    
    private org.osid.repository.AssetLookupSession next;
    
    @OSID
    public org.osid.repository.Asset getAsset(org.osid.id.Id assetId)
        throws org.osid.NotFoundException,
               org.osid.OperationFailedException,
               org.osid.PermissionDeniedException {
           
       log(getEffectiveAgent() + " looking up " + assetId);
       return (this.next.getAsset(assetId));
    }
...     

However, there may be a reason to catch an exception or two.

Jumping In The Way

The previous example logged retrievals even when it didn’t work. This example more accurately distinguishes successes from failures.

Code Block
languagejava
class AssetLookupLoggingDecorator
    implements org.osid.repository.AssetLookupSession {
    
    private org.osid.repository.AssetLookupSession next;
    
    @OSID
    public org.osid.repository.Asset getAsset(org.osid.id.Id assetId)
        throws org.osid.NotFoundException,
               org.osid.OperationFailedException,
               org.osid.PermissionDeniedException {
           
        try {
            return (this.next.getAsset(assetId));
        } catch (org.osid.OsidException oe) {
            log(getEffectiveAgent() + " could not get " + assetId + " because of " + oe.getMessage());
            throw oe;
        } finally {
            log(getEffectiveAgent() + " looking up " + assetId);
        }
    }
...    

This method adds nothing to the process of retrieving an asset so we don’t want to interfere with the exception chain. Re-throwing the same exception instead of creating a new one is the right choice when just needing a sneak peek.

This could have caught each of the NotFoundException, OperationFailedException, and PermissionDeniedException explicitly, but OsidException was a bit easier. All org.osid.OsidExceptions are checked exceptions declared in the method signatures. We cannot get any other kind of checked exception from getAsset() so the broader net is fine in this case (note that org.osid.OsidRuntimeException is like java.lang.RuntimeException but org.osid.OsidException is not like java.lang.Exception because it does not include org.osid.OsidRuntimeExceptions).

What about unchecked exceptions? The specification also explicitly permits org.osid.NullArgumentException and the runtime may throw a variety of org.osid.ProviderContractExceptions and org.osid.ConsumerContractExceptions. All of these exceptions result not from the operation of the code but from some kind of issue with the code itself. There’s no point in handling errors in code that is broken. In a running application, you may wish to catch them at your last net and open a jira.

In most cases, there is no reason to catch org.osid.OsidRuntimeException of any of its subclasses.

Calling Other OSIDs

Exception Misalignment

Sometimes exceptions don’t align and need some attention.

Code Block
languagejava
public class Activity
    implements org.osid.learning.Activity {
    
    private org.osid.id.Id objectiveId;
    private org.osid.learning.ObjectiveLookupSession objectives;
    ...
    @OSID
    public org.osid.id.Id getObjectiveId() {
        return (this.objectiveId);
    }
    
    @OSID 
    public org.osid.learning.Objective getObjective()
        throws org.osid.OperationFailedException {
        
        try {
            return (this.objectives.getObjective(getObjectiveId()));
        } catch (org.osid.NotFoundException nfe) {
            throw new org.osid.OperationFailedException("for some strange reason, there is no Objective for this Activity. Maybe we're talking to the wrong provider?", nfe)
        } catch (org.osid.PermissionDeniedException pde) {
            throw new org.osid.OperationFailedException("for some inexplicable reason you cannot see the Objective for your Activity. Authorization setup is fakakta. ", pde);
        } 
    }
    ...

In the above example, the getObjective() call is implemented using an ObjectiveLookupSession. ObjectiveLookupSession.getObjective defines NotFoundtException and PermissionDeniedException not present in Activity.getObjective() (ignoring the unchecked exceptions which imply a programming/integration problem which should not be handled here).

The Activity says that it has an Objective. Therefore, the Objective must exist. To say that Activity.getObjective() should also throw a NotFoundException ignores this tenet. If for whatever reason, the provider cannot come up with one should be considered an error due to the result of a breakage in connectivity, data integrity, authorization, configuration, or something which should not occur in normal operations. Semantics like this is what generally causes exception misalignments across method calls.

Do we worry only when exceptions don’t line up?

Exception Alignment

In this case we decided to override the Objective in an orchestration layer.

Code Block
languagejava
public class ObjectiveFetchingActivityLookupSession 
    implements org.osid.learning.ActivityLookupSession {
    
    private org.osid.learning.ActivityLookupSession activityLookupSession;
    private org.osid.learning.ObjectiveLookupSession objectiveLookupSession;
    ...
    @OSID 
    public org.osid.learning.Activity getActivity(org.osid.id.Id activityId) 
        throws org.osid.NotFoundException,
               org.osid.OperationFailedException,
               org.osid.PermsissionDeniedException {
               
        org.osid.learning.Activity activity = this.activityLookupSession.getActivity(activityId);
        org.osid.learning.Objective objective;
        try {
            objective = this.objectiveLookupSession.getObjective(activity.getObjectiveId());
        } catch (org.osid.NotFoundException nfe) {
            throw new org.osid.OperationFailedException("cannot resolve Objective for " + activityId, nfe);
        }
        return (new ObjectiveOverlayActvity(activity, objective);
    }
    ...
        

The first thing this method does is call getActivity() from some underlying provider. It’s similar to the basic decorator/adapter case in the first section. The semantics align so the exceptions align on this call.

The exceptions from getObjective() also align with getActivity(). But the semantics do not. The OSID Consumer is asking for an Activity and to be told the Activity does not exist when the Id reference of the Objective was not found is a problem. The NotFoundException in getActivity() describes the Activity, not the Objective nor any other call this method implementation may use. The fact that we decided to implement this method in such a way to cause this error condition means our service is broken when getObjective() tosses a NotFoundException at us. It should be caught and not allowed to pass through.

When acting as an OSID Consumer within an OSID Provider, look at the defined exceptions and to what they pertain in order to determine what to catch. Muddling through by adding try/catch blocks only when the compiler complains misses these semantics.

Method Factoring

Kicking The Can

Here’s an example of an attempt to replace a GradeSystem for a Course using the kick-the-can method:

Code Block
public class CourseLookupSession
    implements org.osid.course.CourseLookupSession {
    
    private org.osid.course.CourseLookupSession courseLookupSession;
    private org.osid.grading.GradeSystemLookupSession gradeSystemLookupSession;
    private org.osid.relationship.RelationshipLookupSession relationshipLookupSession;
    
    @OSID
    public org.osid.course.CourseList getCourses()
        throws org.osid.OperationFailedException,
               org.osid.PermissionDeniedException {
              
        return (processCourses(this.courseLookupSession.getCourses()));
    }
    
    private org.osid.course.CourseList processCourses(org.osid.course.CourseList courses) 
        throws org.osid.OperationFailedException,
               org.osid.PermissionDeniedException {
              
        MutableCourseList ret = new MutableCourseList();
        while (courses.hasNext()) {
            ret.add(processCourse(courses.getNextCourse())); 
        }
        ret.done();
        return (ret);
    }
    
    private org.osid.course.Course processCourse(org.osid.course.Course course) 
        throws org.osid.OperationFailedException,
               org.osid.PermissionDeniedException {
               
        if (course.isGraded()) {
            return (processGradingOptions(course));
        } else {
            return (course);
        }
    }
    
    private org.osid.course.Course processGradingOptions(org.osid.course.Courss course) 
        throws org.osid.OperationFailedException,
               org.osid.PermissionDeniedException {
               
        Collection<org.osid.grading.GradeSystem> mappedGradeSystems = new ArrayList<>();
        try (org.osid.grading.GradeSystemList gradeSystems = course.getGradingOptions()) {
            while (ids.hasNext()) {
                org.osid.grading.GradeSystem gradeSystem = gradeSystems.getNextGradeSystem();
                try {
                    mappedGradeSystems.add(getNewGradingOption(gradeSystem.getId()));
                } catch (org.osid.NotFoundException nfe) {
                    mappedGradeSystems.add(gradeSystem);
               }
            }                        
        }
        return (mapCourseToNewGradeSystems(course, mappedGradeSystems);
    }
    
    private org.osid.grading.GradeSystem getNewGradingOption(org.osid.id.Id gradeSystemid)
        throws org.osid.NotFoundException,
               org.osid.OperationFailedException,
               org.osid.PermissionDeniedException {
              
        try (org.osid.relationship.RelationshipList mappings = this.relationLookupSession.getRelationshipsForSource(gradeSystemId)) {
            if (mappings.hasNext()) {
                return (this.gradeSystemLookupSession.getGradeSystem(mappings.getNextRelationship().getDestinationId()));
            } else {
                throw new org.osid.NotFoundException("cannot map " + gradeSystemId);
            }
        }
    }
    
    private org.osid.course.Course mapCourseToNewGradeSystems(course, mappedGradeSystems)
        throws org.osid.OperationFailedException,
               org.osid.PermissionDeniedException {
  
        return (new CourseWrapper(course, mappedGradeSystems));
    }

The above code pushes the logic of each loop into a separate method. The resulting flow describes a procedure. Some of the symptoms include:

  • a trail of private methods strung together

  • method names with the word process in them

  • lack of clarity over which method is responsible for what job

  • the inability to reuse any part of the trail other than the head

  • one utility for the list and another for an individual element on the list

  • needing to declare the same exceptions at each point and the resulting exceptions actually thrown could come from anywhere along the trail.

This factoring approach worked fine in C & Fortran.

See Also

...