Moving to Code

Moving to Code

Now we can take a look at some of the code that implements the ideas I discussed in the previous sections. I'll begin with Phenomenon Type and Phenomenon, since they are quite closely linked.

The first thing to think about is the association between them: Should the interface allow navigability in both directions? In this case, I think so because both directions will be valuable and they are closely linked concepts, in any case. Indeed, I am happy to implement the association with pointers in both directions, too. I shall make it an immutable association, however, as these are objects that are set up and then left alonethey are not modified often, and when they are, we can create them again.

Some people have trouble with two-way links. I don't find them troublesome if I ensure that one class takes the overall responsibility for keeping the link up to date, assisted by a "friend" or helper method, as necessary.

Let's look at some declarations.

				
public class PhenomenonType extends DomainObject {
 public PhenomenonType(String name) {
  super(name);
 };
 
 void friendPhenomenonAdd(Phenomenon newPhenomenon) {
  // RESTRICTED: only used by Phenomenon
  _phenomena.addElement(newPhenomenon);
 };
 
 public void setPhenomena(String[] names) {
  for (int i = 0; i < names.length; i++)
   new Phenomenon(names[i], this);
 };
 
 public Enumeration phenomena() {
  return _phenomena.elements();
 };
 
 private Vector _phenomena = new Vector();
 
 private QuantityRange _validRange;
}

			

I use the convention of adding an underscore before all fields. It helps me avoid getting my names confused.

				
public class Phenomenon extends DomainObject {
 public Phenomenon(String name, PhenomenonType type) {
  super (name);
  _type = type;
  _type.friendPhenomenonAdd(this);
 };
 public PhenomenonType phenomenonType() {
  return _type;
 };
 
 private PhenomenonType _type;
  private QuantityRange _range;
 }
 
 package observations;
 
 public class DomainObject {
  public DomainObject(String name) {
   _name = name;
  };
 
  public DomainObject()    {};
 
  public String name() {
   return _name;
  };
 
  public String toString() {
   return _name;
  };
 
  protected String _name = "no name";
}

			

I've added a DomainObject class, which knows about names and will do any other behavior that I want all of my domain classes to do.

I can now set up these objects with code along the lines of the following:

				
PhenomenonType sex =
 new PhenomenonType("gender").persist();
String[] sexes = {"male", "female"};
sex.setPhenomena(sexes);

			

The persist() operation stores the Phenomenon Type in a registry object so that you can get it again later with a static get() method. I'll skip the details of that.

Next, I'll put in the code to add observations to a patient. Here I don't want all the associations to be two-way. I have the patient hang on to a collection of observations, since they are used in the context of a patient.

				
public class Observation extends DomainObject {
 public Observation(Phenomenon relevantPhenomenon,
  Patient patient, Date whenObserved) {
   _phenomenon = relevantPhenomenon;
   patient.observationsAdd(this);
   _whenObserved = whenObserved;
 };
 
 private Phenomenon _phenomenon;
 
 private Date _whenObserved;
}

public class Patient extends DomainObject {
 public Patient(String name) {
  super(name);
 };

 void observationsAdd(Observation newObs) {
  _observations.addElement(newObs);
 };

 private Vector _observations = new Vector();
}

			

With this I can create observations.

				
new Patient("Adams").persist();
new Observation(PhenomenonType.get("gender").
 phenomenonNamed("male"), Patient.get("Adams"),
 new Date (96, 3, 1) );
class PhenomenonType {
 public Phenomenon phenomenonNamed(String name) {
  Enumeration e = phenomena();
  while (e.hasMoreElements() )
  {
   Phenomenon each = (Phenomenon)e.nextElement();
   if (each.name().equals(name))
    return each;
  };

  return null;
}

			

After creating observations, I need to be able to find the latest phenomenon.

				
class Patient
 public Phenomenon phenomenonOf
  (PhenomenonType phenomenonType)
 {
  return (latestObservation(phenomenonType) ==
   null ? new NullPhenomenon() :
   latestObservation(phenomenonType).phenomenon() );
 }

 private Observation
  latestObservation(PhenomenonType value) {
  return latestObservationIn(observationsOf(value) );
 }

 private Enumeration
  observationsOf(PhenomenonType value) {
   Vector result = new Vector();
   Enumeration e = observations();
   while (e.hasMoreElements() )
   {
    Observation each = (Observation) e.nextElement();
    if (each.phenomenonType() == value)
     result.addElement(each);
   };
   return result.elements();
}
private Observation latestObservationIn
 (Enumeration observationEnum) {
  if (!observationEnum.hasMoreElements() )
   return null;
  Observation result =
   (Observation)observationEnum.nextElement();
  if (!observationEnum.hasMoreElements() )
   return result;

  do
  {
   Observation each =
    (Observation)observationEnum.nextElement();
   if (each.whenObserved().
    after(result.whenObserved() ) )
     result = each;
  }

  while (observationEnum.hasMoreElements() );

  return result;
 }

class Observation
 public PhenomenonType phenomenonType() {
  return _phenomenon.phenomenonType();
 }

			

Several methods combine to do this. You could draw a diagram to show this, but I tend not to bother. The way I decompose a method has more to do with refactoring (see page 30) than it does with prior design.

We can now look at adding the behavior for measurements.

First, let's see the definition of the Measurement class and its constructor.

				
public class Measurement extends Observation {
 public Measurement(Quantity amount,
  PhenomenonType phenomenonType,
  Patient patient, Date whenObserved) {
   initialize (patient, whenObserved);
   _amount = amount;
   _phenomenonType = phenomenonType;
  };

  public PhenomenonType phenomenonType() {
   return _phenomenonType;
  };

  public String toString() {
   return _phenomenonType + ": " + _amount;
  };

  private Quantity _amount;

  private PhenomenonType _phenomenonType;
 }

 class Observation
  protected void initialize(Patient patient,
   Date whenObserved) {
    patient.observationsAdd(this);
    _whenObserved = whenObserved;
  }

			

Note that a class diagram gives us a good start on developing this.

We again need the latest measurement.

				
Class Patient
 public Quantity latestAmountOf(PhenomenonType value) {
  return ((latestMeasurement(value) == null) ) ?
  new NullQuantity():latestMeasurement(value).amount();
 }

 private Measurement
  latestMeasurement(PhenomenonType value) {
   if (latestObservation(value) == null)
    return null;
   if (!latestObservation(value).isMeasurement() )
    return null;
   return (Measurement)latestObservation(value);
}

			

In both of these cases, the class diagram suggests basic structure, and we add behavior to it to support more interesting queries.

At this stage, we could describe our position with the specification-perspective class diagram shown in Figure 11-7.

Figure 11-7. Another Patient Observation Specification Model
graphics/11fig07.gif

Take a look at how this diagram stresses interface over implementation. I've modeled the role from Phenomenon Type to Phenomenon as a qualified role because that's the primary interface on Phenomenon Type. Similarly, I've shown Observation with a link to Phenomenon Type because the interface exists there, even though the measurement is the only one with a direct pointer to Phenomenon.

Looking at this diagram, we can see that the only difference between Measurement and Observation is that Measurement has a quantity. We could remove the Measurement class entirely from the specification model by allowing any observation to have a (potentially null) quantity.

We could still have a separate Measurement class, which would have amount and phenomenon type fields, but nobody outside the package would be aware of the class's existence. We would need to add Factory methods (Gamma, Helm, Johnson, and Vlissides 1995) either on Observation or on Patient to allow the appropriate class to be created.

I will leave that change as an exercise for the reader and move on to assigning a Phenomenon automatically for a Measurement.

Figure 11-7 illustrates the general process.

First, we need to add a method call to Measurement's constructor.

				
Class Measurement
 public Measurement (Quantity amount,
 PhenomenonType phenomenonType,
 Patient patient, Date whenObserved)
  initialize (patient, whenObserved);
  _amount = amount;
  _phenomenonType = phenomenonType;
  _phenomenon = calculatePhenomenonFor(_amount);

			

This delegates the task to Phenomenon Type.

				
Class Measurement
 public Phenomenon calculatePhenomenonFor(Quantity arg)
 {
  return _phenomenonType.phenomenonIncluding(arg);
 }

			

This asks each phenomenon in turn.

				
Class PhenomenonType
 public Phenomenon phenomenonIncluding (Quantity arg) {
  Enumeration e = phenomena();
  while (e.hasMoreElements())
  {
   Phenomenon each = (Phenomenon) e.nextElement();
   if (each.includes(arg))
    return each;
  };

  return null;
 }
Class Phenomenon
 public boolean includes (Quantity arg) {
  return (_range == null ? false:_range.includes(arg));
 }

			

The code flows out well from the sequence diagram. In practice, I usually use a sequence diagram to rough out the interaction and then make some changes as I code it. If the interaction is important, I will update the sequence chart as part of my documentation. If I think that the sequence chart will not add much clarity to the code, I file the rough sequence chart in the circular filing cabinet.

This is a brief example of how to use the UML with a programming language, but it should give you a good idea of the process. You don't have to be on a high-ceremony project to find using bits of the UML handy. You don't have to use all of it, just the bits you find useful.

Sketching out a design with a class diagram and an interaction diagram can help get your thoughts in order and make it easier to code. I think of these sketches as fast prototypes. You don't have to hold on to the diagrams later, but you may find it easier for yourself and others to understand the code if you do.

You don't need a fancy and expensive CASE tool. A whiteboard and a simple drawing tool on your computer will do fine. Of course, there are useful CASE tools, and if you are involved in a larger project, you might consider getting one.

If you do, compare it to the baseline of a simple drawing tool and a word processor. (It's amazing how much you can do with Visio and Word, for instance.) If the tool has code generation, look carefully at how it generates code. Code generation forces the CASE tool to take a very particular interpretation of the diagrams, which will affect the way you draw the diagrams and what the diagrams mean.

You can find more about this example via my Web site. The version of the example at the site goes more deeply into some of the layering issues involved in getting this model to a user interface.



 
ASPTreeView.com
 
Evaluation has ЩДіЪґ¶Сexpired.
Info...