Back to Thoughts On Technology Page | Back to Peter's Home Page | Back to Family Home Page



Designing Efficient Business Service Objects
Peter Rose - 01/2005


ABSTRACT

SUMMARY

DEFINING AN OBJECT

LAYERING OF ENCAPSULATED BUSINESS SERVICE OBJECTS

UNDERSTANDING AGENT CLASSES

DECOMPOSING THE PROBLEM DOMAIN

BUSINESS SERVICE OBJECTS AT THE CODE LEVEL

WRITING EFFICIENT BUSINESS SERVICE METHODS

ACHIEVING CLASS REUSE

DEALING WITH LARGE NUMBERS OF HIGHLY ENCAPSULATED LOOSELY COUPLED CLASSES





Abstract
This document's intent is to provide a set of simple guidelines for the development of business objects that clearly demonstrate highly cohesive and loosely coupled behavior.

High cohesion means that a class encapsulates not only its properties but also its business services, i.e. that work which it is designed to perform. A loosely coupled object is one which performs all of the work that it needs to do from its own internal data and/or other data/services passed into it through its public api.

Summary
To show highly cohesive and loosely coupled object behavior, an example TicketValidator class will be developed which validates and updates an airline ticket submitted by a customer from an airline's online reservation booking system website.

The class only has one public method. All business logic of the class is expressed within this main validateTicket() method. validateTicket() calls out to other methods within the class and uses the services of other objects to facilitate its full functionality.

Defining An Object
Per Dr. Carl Argila defines an object in the following manner:

"An object is an independent, asynchronous, concurrent entity which 'knows things' (i.e. stores data), 'does work' (i.e. encapsulates services), and 'collaborates with other objects' (by exchanging messages) to perform the overall functions of the system".

So, he said rather matter-of-factly in the class I attended: "This just means that an object knows stuff (its data), it knows all about how to use that data (its methods), and it knows how to provide the data in a form other objects it communicates with need to have it in (its services)." He went on to further state that if an object did not have all three of these characteristics, then it really wasn't an object that was useful in solving the problem domain of the system.

What he was really stressing was the fact that as application programmers, the solution of the problem domain was done by these types of "business objects". Other objects help facilitate this process, but they are not central to the solution or to the design. For example, raw data objects, i.e. DTO or Data Transfer Objects only have data. They are used to collect data for or from input boxes on an HTML page, for example. The servlet would immediately validate this data and then hand the object off to a page object graph population object, i.e. an object that knows how to take this specific data and either distribute it out to an existing object graph of business objects, or create that object graph.

Another type of object is referred to as an Agent object (or sometimes pure controller object). An Agent object contains no data but rather just has methods which enlist other objects in some defined manner to solve the problem at hand.

Layering of Encapsulated Business Service Objects
The term "Layering of Encapsulated Business Service Objects" is just another way of saying that you are encapsulating objects that do work (i.e. "services" for objects that solve business logic) in the same manner that you encapsulate data. All this means is having more discrete (i.e. encapsulated and non-coupled) objects, each of which has one particular and well defined task to solve. This can be seen as simply breaking up a Use Case into manageable chunks and layering descendent service objects hierarchally for solution.

A Problem Domain is decomposed in succeeding layers to more specific objects until you get to the final object that actually coordinates the efforts of many objects to solve one specific issue. Objects that pass control down (i.e. control routing only) are Agent classes while the actual object at the end of the chain that does the discrete unit of work is referred to as a business service object (or just object because these objects satisfy the criteria in the Object Model of having: data, methods, and services).

Understanding Agent Classes
An Agent class is like a Librarian object that takes a Book object from a Borrower object. The Librarian object then deposits that Book object onto a Shelf object through use of its aggregate business service class LibrarianBookReturnService. The borrower could be programmed to simply bypass the librarian and check the book in by itself, but this would not fully represent the true business process of a library. Agent classes provide services for other objects rather than for themselves.

The LibrarianBookReturnService class is an example of a business service object. It could be responsible for all actions needed to check a book back into the library. The LibrarianBookReturnService might thus instantiate a CheckAccountForLateFeesService as a layer further down in the LibrarianBookReturnService class's object graph.

Many times - most times - a business service object acts in a container/aggregate relationship with the other objects it interacts with to do its work, i.e. the aggregate knows nothing of its container but the container has perfect knowledge of its aggregates. This is how encapsulated business service layering is facilitated through the system.

The Model 2 flavor of the M-V-C design pattern supports this segmentation of object graph responsibility through the use of sub controller services - Agent classes (which many times are nested servlets) - that segment a Problem Domain into discrete and manageable business process flow solution paths. But they don't necessarily need to be servlets; they can be any Agent responsibility class.

Decomposing The Problem Domain
Understanding how to write an effective business service object is really dependant on us understanding how the service we are writing fits into the larger scope of the overall application. For example, in a system to address the overall needs of a manufacturing company we might have different main divisions of work:

The point of all this discussion is two fold:
  1. To show how a complex business system can be decomposed into discrete encapsulated hierarchical components that can easily have additional functionality added.
  2. To show how this decomposition process facilitates key object ("actor") identification so that solid Use Case analysis can be done.
Let's look just at Human Resources, or what we will call Employee Services. Everything we want to know about dealing with an employee will be found in this section of the code. We will look at 3 aspects of employee management:

Employee Services
   Updating an employee's payroll account
   Registering new employees
   Employee termination processes

We can further decompose our problem domain as it applies to the employee services by looking at just registering new employees:

Employee Services
   Updating an employee's payroll account
   Registering new employees
      Processes for employees needing regular clearance
      Process for employees who will be contractors
   Employee termination processes

Let's say our task - our Problem Domain - is to add a module onto this existing structure for processing employees needing secret clearance that will require some form of background checking:

      Processes for employees needing secret clearance
         Fill in a background check form on employee

Adding this into our domain structure results in the following hierarchy:

Employee Services
   Updating an employee's payroll account
   Registering new employees
      Processes for employees needing regular clearance
      Processes for employees needing secret clearance
         Fill in a background check form on employee
      Process for employees who will be contractors
   Employee termination processes

We can easily see that the immediate actor classes we will need for this new functionality will be: HRRep, Employee, ClearenceRequirementsMgr, and BackgroundCheckMgr. Of these, we already have HRRep and Employee as existing actors of the system so our development of Use Cases for this new module will focus on ClearenceRequirementsMgr and BackgroundCheckMgr.

Another key thing to note is each identified business process flow (i.e. "Processes for employees needing secret clearance", "Fill in a background check form on employee", etc.) really represent major branches of application functionality and thus would become controller service servlets in the design model.

Diagrammatically, this might be represented as follows:

diagram

Business Service Objects At The Code Level
To take this to the next step, let's look at a simple TicketValidator class that will be developed which validates and updates an airline ticket submitted by a customer from an airline's online reservation booking system website.

The class only has one public method. All business logic of the class is expressed within this main validateTicket() method. validateTicket() calls out to other methods within the class and uses the services of other objects to facilitate its full functionality.


// ************************************************************************
/**
 * TicketValidator.java
 *
 * Simple example class to demonstrate highly cohesive and loosely
 * coupled behavior.
 *
 * The example shows a highly encapsulated class that has only one public
 * method. All business logic of the class is expressed within this main
 * validateTicket() method. validateTicket() calls out to other methods within
 * the class or uses the services of other objects to facilitate its full
 * functionality.
*/
// ************************************************************************
public class TicketValidator {
  public boolean validateTicket(Ticket aTicket) {
    boolean okTicket = true;
      try {
        if(AirportMgr.isValidOrigin(aTicket) &&
              AirportMgr.isValidDestination(aTicket)){
          if( isInternationalFlight(aTicket) ) {
            if( CountrySecurityMgr.isLegalDestination(aTicket) ) {
              aTicket = TarrifMgr.assignInternationalTarrifs(aTicket);
              aTicket = FlightRulesMgr.getZoneFlightRules(aTicket);
              if( !(new FlightZoneValidator(aTicket.getFlightRules()).okMatch()) ) {
                aTicket = FlightRulesMgr.reZoneFlightRules(
                    aTicket.getFlightRules());
              } //endif
            } else {
              okTicket = false;
            } //endif
          } else {
            aTicket = TarrifMgr.assignDomesticTarrifs(aTicket);
            aTicket = FlightRulesMgr.getDomesticFlightRules(aTicket);
          } //endif

        if( okTicket ) {
          AirlineMgr airlineMgr = new AirlineMgr(aTicket);
          if(airlineMgr.isFlightOutAvailable() && airlineMgr.okNumberOfSeats()) {
            aTicket = airlineMgr.verifyMealRequest();
            aTicket = airlineMgr.makeSeatAssignment();
          } else {
            okTicket = false;
          } //endif
        } //endif
      } else {
        //-- Either not a valid origin or destination
        okTicket = false;
      } //endif
    } catch(TicketValidationException tve) {
      okTicket = false;
    } //endtry

    return( okTicket );
  } //endmethod: validateTicket

	// < --- other class methods --- >

} //endclass: TicketValidator

Code Level Encapsulation Guidelines
I find the following guidelines very helpful in developing my code. Let's see how many of them are incorporated in this method:
  1. Object encapsulation is at the forefront of consideration.
  2. Use Interfaces to create object contracts
  3. Follow descriptive naming conventions for all classes and methods.
  4. If an object contains more than 3 public methods (other than accessor methods) then it is a sign that the object is not sufficiently encapsulated, particularly controller classes which I many times limit to just 1 public method.
  5. Refrain from passing raw data into methods. Rather favor objects as parameters.
  6. Limit method parameters to 3 or less unless there is compelling reason for more. Most times a method that needs more parameters is not encapsulated and needs to be broken up into other methods.
  7. Limit method body text to what is easily seen in one screen.
  8. Limit each method to one specific discrete operation regardless of how small, even if the operation is not called from multiple other places in the class.
  9. Provide verbose and detailed JavaDoc comments no matter how trivial the method.
  10. JavaDoc all method parameters, returns, and exceptions.
  11. Do not nest conditional logic more than 2 levels; more indicates the need to break out into another method call.
  12. Consolidate all class functionality (i.e. business purpose and process) in one main public method which calls out to other methods in the class to do the work.
  13. Do not nest method calls more than 2 layers deeper then from the main public method (i.e. main public calls method1 which in turn calls method2 - but method2 is not allowed to cascade further).
  14. Try not to go more than 3 levels of inheritance before getting to your concrete class.
  15. Rather than using inheritance hierarchies to facilitate business solution responsibility, modern Object Oriented Design technology suggests combining aggregation (has a) with composition (is a type of) within inheritance hierarchies. For example, pure inheritance might have an ancestor class Person with an Employee descendant which might in turn subclass an HourlyWorker. But HourlyWorker "is a type of" Employee, not an "is a" Employee. Thus, Person will extend Employee but Employee will have an aggregate abstract EmployeeType which will then extend HourlyWorker, SeasonalWorker, etc. as descendants. Deep inheritance hierarchies tend to be quite bloated and clog up the pipe with excess object overhead and referencing through the stack. They can also be quite difficult to understand from a maintenance perspective with later developers coming into the system.

Refactoring For Better Method Encapsulation
Of note from the above list are the bullets regarding the size of a method and the depth of conditional nesting. This is not only for readability but for better encapsulation of the task of that method.

We have to remember that after we are done developing an object, we are going to have to maintain it. Though the entire class's business logic is very succinctly described in this example main controlling method, it does take a little reading to work through the conditionals. How about if we replace some of this clutter with the following refactored method:


public class TicketValidator2 {
  AirlineMgr airlineMgr = null;
  // ===================================================================
  public boolean validateTicket(Ticket aTicket) {
    boolean okTicket = true;
    try {
      airlineMgr = new AirlineMgr(aTicket);
      if(AirportMgr.isValidOrigin(aTicket) && AirportMgr.isValidDestination(aTicket)){
        if( okTarrifAndZoneStatus(aTicket) ) {
          okTicket = okFlightConstraints(aTicket);
        } else {
          okTicket = false;
        } //endif
      } else {
        //-- Either not a valid origin or destination
        okTicket = false;
      } //endif
    } catch(TicketValidationException tve) {
      okTicket = false;
    } //endtry

    return( okTicket );
  } //endmethod: validateTicket

And the refactored methods would look something like this:


  // ===================================================================
  private boolean okTarrifAndZoneStatus(Ticket aTicket) {
    boolean okStatus = true;
      try {
        if( isInternationalFlight(aTicket) ) {
          if( CountrySecurityMgr.isLegalDestination(aTicket) ) {
            aTicket = TarrifMgr.assignInternationalTarrifs(aTicket);
            aTicket = FlightRulesMgr.getZoneFlightRules(aTicket);
            if( !(new FlightZoneValidator(aTicket.getFlightRules()).okMatch()) ) {
              aTicket = FlightRulesMgr.reZoneFlightRules(aTicket.getFlightRules());
            } //endif
          } else {
            okStatus = false;
          } //endif
        } else {
          aTicket = TarrifMgr.assignDomesticTarrifs(aTicket);
          aTicket = FlightRulesMgr.getDomesticFlightRules(aTicket);
        } //endif
      } catch(TicketValidationException tve) {
        okStatus = false;
      } //endtry

      return( okStatus );
  } //endmethod: okTarrifAndZoneStatus

  // ===================================================================
  private boolean okFlightConstraints(Ticket aTicket) {
    boolean okStatus = true;
    if(airlineMgr.isFlightOutAvailable() && airlineMgr.okNumberOfSeats()) {
      aTicket = airlineMgr.verifyMealRequest();
      aTicket = airlineMgr.makeSeatAssignment();
    } else {
      okTicket = false;
    } //endif
    return( okStatus );
  } //endmethod: okFlightConstruanits

  // < --- other class methods --- >

  } //endclass: TicketValidator2

The above code is just for explanatory purposes as to how highly cohesive and loosely coupled objects are structured. In order to create the type of refactorable code I needed in order to demonstrate this process, I violated one of my primary principals of writing methods: do not change an object method parameter by reference when the method is either void or returns another type or object.

Ignoring this fact for purposes of this example (as it would be dealt with through a different design in a later refactoring) it is plain to see that this refactored code is not only much easier to read, but much easier to debug as well. And, if something goes wrong, we can zero right in on where the error came from.

It is also much easier to add business rules to this sort of structure: you either add them directly to one of the methods if the rule fits into that scope, or you create a new method and add it into one of the conditional calls in the controlling method (i.e. in this case the validateTicket() method).

Dangers Of Over Refactoring
Now, we could get carried away with this refactoring process and end up with a main driver method that looks like this:


// ************************************************************************
public class TicketValidator3 {
	Ticket ticket = new Ticket();
	// ===================================================================
	public boolean validateTicket(Ticket aTicket) {
		ticket = aTicket;
		boolean okTicket = true;
		try {
			if( !okFlightSelection(aTicket)   ) {
				okTicket = false;
			} //endif
		} catch(TicketValidationException tve) {
			okTicket = false;
		} //endtry

		return( okTicket );
	} //endmethod: validateTicket

But this really tells you nothing, i.e. the method really does nothing but delegate to another method. The idea to keep in mind for a controlling method in this type of business service class is to fully reveal the work that the object does without cluttering up the view with the details of how it does its work.

And the type of poorly designed method shown here that just delegates down into another method indicates something worse to follow. What happens 90% of the time is that the method that is called from the controlling method will most likely in turn call other methods within the same class (which may call other methods…) creating a hierarchal calling stack that becomes difficult to understand - and thus maintain.

Writing Efficient Business Service Methods
A controlling method in a business service class must be descriptive enough, but not too descriptive. And it must be terse enough not to create clutter, but not too terse. It is only with experience that the developer can come to an efficient structure. That helps a lot, right!

If we think of the controlling method as an outline of the steps the class will have to go through to execute its business purpose in the application, then that creates the initial conditional structure of the solution path. This is pretty much reflected in the design of the first TicketValidator class shown.

Notice that in our first re-factoring to TickeValidator2, I stripped out the following code and made it a method of its own:


  if( isInternationalFlight(aTicket) ) {
    if( CountrySecurityMgr.isLegalDestination(aTicket) ) {
      aTicket = TarrifMgr.assignInternationalTarrifs(aTicket);
      aTicket = FlightRulesMgr.getZoneFlightRules(aTicket);
      if( !(new FlightZoneValidator(aTicket.getFlightRules()).okMatch()) ) {
        aTicket = FlightRulesMgr.reZoneFlightRules(
        aTicket.getFlightRules());
      } //endif
    } else {
      okTicket = false;
    } //endif
  } else {
    aTicket = TarrifMgr.assignDomesticTarrifs(aTicket);
    aTicket = FlightRulesMgr.getDomesticFlightRules(aTicket);
  } //endif

Which simplified the main controlling method to:


				if( okTarrifAndZoneStatus() ) {

If you look at the body of this method, it all has to do with some sort of tariff and flight zone rules. It would appear from this that these business rules are either mutually dependant on each other or are at least close enough in function to be grouped together (which in reality may or may not be the case; this is just for demonstration purposes…). If this is the case, then the refactoring is a good one because the name of the method fully summarizes the unique encapsulated action it is intended to perform.

Avoiding An Improper Refactor Code To Method
What we have to watch out is when we refactor code and parts of the extracted code do not go together. A possible bad solution to this would be to re-factor the okTarrifAndZoneStatus() method to the following:


  if( isInternationalFlight(aTicket) ) {
    if( CountrySecurityMgr.isLegalDestination(aTicket) ) {
      aTicket = TarrifMgr.assignInternationalTarrifs(aTicket);
      aTicket = FlightRulesMgr.getZoneFlightRules(aTicket);
      aTicket = checkZoneFlightRules();
    } else {
      okTicket = false;
    } //endif
  } else {
    aTicket = TarrifMgr.assignDomesticTarrifs(aTicket);
    aTicket = FlightRulesMgr.getDomesticFlightRules(aTicket);
  } //endif

Where we have substituted


  if( !(new FlightZoneValidator(aTicket.getFlightRules()).okMatch()) ) {
    aTicket = FlightRulesMgr.reZoneFlightRules(
    aTicket.getFlightRules());
  } //endif

with another nested class method call


    aTicket = checkZoneFlightRules();

The reason this is bad, is because for someone to figure out what the okTarrifAndZoneStatus() method does, they will have to break their concentration and visit the nested method. Or worse: they won't know enough to even look, and thus miss an important function of the method. This creates very hard to understand and manage code.

So, how do you solve this problem if in fact you do not feel that code fragment belongs as part of the okTarrifAndZoneStatus() method? The answer is that you must refactor the controlling method, not the methods it calls. This can be very difficult because more often than not, it will require not only re-writing the controlling method, but re-thinking the whole design of the class. It might also reveal to you that you really need to create another business service class to solve this particular type of problem. But you simply have to do this type of refactoring in order to keep clarity in your code. It is not easy to make code simple.

If you do not do this, however, then the next developer won't either. The class very rapidly becomes what we use to call in the old days of structured programming in Pascal, COBOL, C, etc. "spaghetti code", i.e. code that runs limply all over the place. As developers, this type of code structure is our responsibility, not the application architect's or the lead designer's. Look at it in terms of what we teach our kids: treat others as we would have them treat us. Write your code with respect for those who will have to maintain it because that person may be you!

Dealing With New Requirements
If we look back at our refactored validateTicket() method, it aptly conveys the entirety of the work of this class. Nothing about "what" the class does is lost, only that more of the "how" this service is rendered has been put under the sheets. This helps eliminate the clutter and background noise someone has to go through who needs to quickly determine what the method does. This is shown here:


	// ===================================================================
	public boolean validateTicket(Ticket aTicket) {
		boolean okTicket = true;
		try {
			airlineMgr = new AirlineMgr(aTicket);
			if(AirportMgr.isValidOrigin(aTicket) && AirportMgr.isValidDestination(aTicket)){
				if( okTarrifAndZoneStatus(aTicket) ) {
					okTicket = okFlightConstraints(aTicket);
				} else {
					okTicket = false;
				} //endif
			} else {
				//-- Either not a valid origin or destination
				okTicket = false;
			} //endif
		} catch(TicketValidationException tve) {
			okTicket = false;
		} //endtry

		return( okTicket );
	} //endmethod: validateTicket

Again, the main controller method is a broad outline that covers all of the things that the class needs to consider to render its desired result. In our case, we immediately see that in order to validate an ticket for this airline there are three very basic tasks that have to be done:

  1. Check to see that the user has selected valid origin and destination points for their trip based on other information in the ticket they have filled out. We don't have to know anything more than that to know that anything to do with origin and destination are covered in the results of these two method calls in the one conditional.
  2. Verify that all tariff requirements and zones the flight will travel through are appropriate.
  3. And make sure all other "flight constraints" (whatever those are) are satisfied. It should be noted that the term "flight constraints" will mean something very specific to a knowledgable user, i.e. that flight constraints (in this example, anyway) deal with just meal requests and seating assignments and that these two requirements are usually grouped together for confirmation purposes. That is why only domain experts should work on these types of systems, otherwise the code becomes a restatement of the obvious and cluttered with unnecessary checking.

It is clear that all 3 of these conditions must be met to issue a valid ticket. It is also clear that these are the only 3 things that need to be considered in making this decision. If we see that either of those two statements is incorrect, or if we are given new requirements then we need to include them into the validateTicket() method.

For example, let's say that the airline gives us a new requirement that in order to have a valid ticket at least one of the passengers listed in the ticket must be of a certain age. We would simply change the validateTicket() method to reflect this:


	// ===================================================================
	public boolean validateTicket(Ticket aTicket) {
		boolean okTicket = true;
		try {
			if( noOfAgePassengerListedOnTicket(aTicket) ) {
				return( false );
			} //endif

			airlineMgr = new AirlineMgr(aTicket);
			if(AirportMgr.isValidOrigin(aTicket) && AirportMgr.isValidDestination(aTicket)){
				if( okTarrifAndZoneStatus(aTicket) ) {
					okTicket = okFlightConstraints(aTicket);
				} else {
					okTicket = false;
				} //endif
			} else {
				//-- Either not a valid origin or destination
				okTicket = false;
			} //endif
		} catch(TicketValidationException tve) {
			okTicket = false;
		} //endtry

		return( okTicket );
	} //endmethod: validateTicket

All we did was add the following code to the top of the method:


			if( noOfAgePassengerListedOnTicket(aTicket) ) {
				return( false );
			} //endif

This check is called a "show stopper": if the check is true then there is no reason to do any checking against any other aspect of the ticket. The ticket is not valid and we're done.

Well, how about if a new requirement is made that a ticket must be flaged as invalid if the destination country of the flight is in zone "5"? The method becomes:


	// ===================================================================
	public boolean validateTicket(Ticket aTicket) {
		boolean okTicket = true;
		try {
			if( noOfAgePassengerListedOnTicket(aTicket) ) {
				return( false );
			} //endif

			airlineMgr = new AirlineMgr(aTicket);
			if(AirportMgr.isValidOrigin(aTicket) && AirportMgr.isValidDestination(aTicket)){
				if( okTarrifAndZoneStatus(aTicket) && notInZone(5) ) {
					okTicket = okFlightConstraints(aTicket);
				} else {
					okTicket = false;
				} //endif
			} else {
				//-- Either not a valid origin or destination
				okTicket = false;
			} //endif
		} catch(TicketValidationException tve) {
			okTicket = false;
		} //endtry

		return( okTicket );
	} //endmethod: validateTicket

A simple solution is to make a change of


				if( okTarrifAndZoneStatus(aTicket) ) ) {

to the following:


				if( okTarrifAndZoneStatus(aTicket) && notInZone(5) ) {

This starts to get sticky, though, because now we are starting to show some of the plumbing. So we have to ask ourselves if this new requirement is a hard and fast critical requirement, or just one of a potential series of other "rules" that may be applied as time goes on.

If this new requirement is part of the major business rules the airline identifies for having a valid ticket, then its prominent placement in the conditional check in the controller method is fine. For example, the airline may have a policy of not flying passengers to a certain zone even though that zone is a valid zone. This needs to be a separate requirement from the more generic check that is addressed by the okTarrifAndZoneStatus(aTicket) method.

On the other hand, if the requirement could change to zone "2" or maybe on every other Tuesday to zone "8" then we are in a whole new realm. What we would need to do then is pull all of these specialized nonconforming zone rules out into a separate zone rules class.

In anticipation of just this sort of thing happening - because of the evident nature of the potential changeablity this type of requirement check implies - we should probably design a new business service class to handle these types of rules, and figure out where in validateTicket() it should be called from. It might even just start out as an empty stub. Determining how the overall structure of the method's conditional logic should be framed to include this type of checking is going to be a lot easier now than it might be later when other enhancements or changes have been made to the method to account for other requirements.

In fact, it might even be an important enough of a discovery that we create some sort of strategy or factory pattern to return different types of rule objects and then execute the rule checks by a polymorphic executeRules(aTicket) call on the returned object. But the point is, if a significant new and different validation needs to be done, then it must be reflected in the main controller method of the class.

Achieving Class Reuse
The less a class does, the more likely it can be reused. A reusable class becomes a strong candidate to be put into either the company's system library or application library.

Well written business service classes tend to have few public methods. If you think about it, this makes sense. If you only want a class to do one thing, then you should not need a lot of public methods cluttering up the api. The class should figure out what it needs to do to solve the problem - you should not have to do this by sequentially calling a bunch of public methods. You should not have to concern yourself with the plumbing.

For example, in our refactoring of the validateTicket() method, we abstracted out the following code into it's own method:


	// ===================================================================
	private boolean okFlightConstraints(Ticket aTicket) {
		boolean okStatus = true;
		if(airlineMgr.isFlightOutAvailable() && airlineMgr.okNumberOfSeats()) {
			aTicket = airlineMgr.verifyMealRequest();
			aTicket = airlineMgr.makeSeatAssignment();
		} else {
			okTicket = false;
		} //endif
		return( okStatus );
	} //endmethod: okFlightConstraints

But this looks like something that could be a valid business service question to ask from more than just the context of the current TicketValidator class. This is how we identify candidates for creating new classes. Though only a few lines of code, this class constitutes a very reusable piece of code:


// ************************************************************************
public class FlightConstraints {
	AirlineMgr airlineMgr = null;
	Ticket ticket = new Ticket();
	// ===================================================================
	public boolean okFlightConstraints(Ticket aTicket) {
		ticket = aTicket;
		boolean okStatus = true;
		airlineMgr = new AirlineMgr(aTicket);
		if(airlineMgr.isFlightOutAvailable() && airlineMgr.okNumberOfSeats()) {
			aTicket = airlineMgr.verifyMealRequest();
			aTicket = airlineMgr.makeSeatAssignment();
		} else {
			okTicket = false;
		} //endif
		return( okStatus );
	} //endmethod: okFlightConstraints
} //endclass: FlightConstraints

Dealing With Large Numbers of Highly Encapsulated Loosely Coupled Classes
A class with a lot of public methods could likely be refactored into multiple classes just as we have seen a method with a lot of parameters or different activities can be refactored into multiple other smaller more encapsulated methods. Though this creates a lot more "stuff" (i.e. classes and methods in classes), the result is a much simpler system to understand and maintain.

The key to understanding such a system is to identify the business service objects from the clutter of entity beans, DTOs, agent classes, etc. Careful naming of these objects and their placement in the application's package structure can facilitate this quite a bit.

There is a principal in psychology called the Seven Plus Or Minus Two theory. It basically states that the most the human mind can keep track of at any one time is about 7 things, give or take a couple depending on circumstances.

Thus, it would seem contradictory to say that creating large numbers of highly cohesive and loosely coupled objects simplifies our lives. But if you think about this for a second, it makes a lot of sense. Look at it first of all from the standpoint of just having one huge mother of all classes that does everything - say a huge controller servlet in an M-V-C Model 2 web based application which handles action parameters from every screen in the system.

We've all seen classes like this where the business logic is jammed into hundreds of lines of 5 (and more!) level deep nested conditional and control statements requiring pages and pages or screens and screens of code to consider before a conditional is terminated. How easy is all that to hold in your head? Not very.

Now look at a controller servlet which immediately routes control to another class that deals with that branch of logic identified by the action parameter - say an HttpServletRequest coming from an employee project time allocation screen. The new class evaluates that request object for what the specific request may be, such as create a new project to allocate time against. It then routes control to another class that knows all about such things. That class will control the activities of an entire graph of other objects with all sorts of relationships among them.

The point to all of this is that if the application one day throws a strange exception from someone who was trying to create a new time allocation project, the maintenance programmer - who may have never looked at this application before - will know not only exactly what class to go to, but the exact method that contains the error. How many times have we spent literally hours tracking around through an object graph trying to find what happened with little more to guide us than the log file's exception stack trace dump (why do you think they call it a "dump"…) because of all the cross calling and interdependancies created by large coupled classes?

A well designed system can easily be navigated through by following the path of the Agent classes. Ultimately, this leads to the business service object that we want. And if the work of that business service class is efficiently outlined in the main controlling method, fixing problems or creating new enhancement designs in the class itself or within its related object graph of equally highly cohesive and loosely coupled objects will be relatively straight forward. Balance this with interjecting new code into the middle of 300 lines of 5 level deep nested conditionals.

If you design effective business service objects that are highly cohesive and loosely coupled, then it won't even be Seven Plus Or Minus Two. It will be One Plus Or Minus One. Keep it simple. If not for yourself, then for me because I may be the one that has to enhance your code and you don't want me cursing you 3 years from now.


Back to Thoughts On Technology Page | Back to Peter's Home Page | Back to Family Home Page