The NISTMonte library has been carefully structured for clarity, flexibility and efficiency.
Clarity – Code that is easy to understand is easier to debug.
Flexibility – The library code should handle many different types of problems without modification.
Efficiency – The library should only compute what is necessary and the library should be reasonably efficient.
NISTMonte contains many classes with many relatively simple methods. Each class has clearly defined responsibilities and a clear relationship with other classes. The code within each method is kept as short and focused as possible. Interfaces and abstract base classes are frequently used to enforce common behavior between different implementations of the same type of computation.
The class MonteCarloSS is responsible for tracking
the electron's path. MonteCarloSS does not contain any
code for modeling x-rays or for implementing detectors. Classes that
implement detectors and x-ray production are loosely coupled to the
MonteCarloSS class through an event mechanism. Using an
event architecture is slightly less efficient than integrating
detectors and x-rays into the Monte Carlo but an event architecture
decouples the electron trajectory from the detectors. This tends to
make the code for each component more focused and more clear.
An event architecture is an agreement between an event source
(such as MonteCarloSS) and one or more event sinks
(a detector or x-ray producer). Event sinks tell the event source
that they are interested in receiving notification whenever something
interesting happens at the event source. The event source is then
responsible for notifying the event sink every time an interesting
event occurs. For example, the event source MonteCarloSS
notifies an event sync such as an detector whenever a scattering
event occurs. The detector can then react to this event however it
would like. One detector, like the TrajectoryImage class,
plots onto a bitmap the electron trajectory segment since the last scattering
event. Another, like the BackscatterStats
class, ignores all events except events representing a
backscattered electron. In the case of a backscattered electron, the
BackscatterStats class tabulates statistics.
Event architectures are very flexible because
Once a programmer becomes comfortable with the idea of sources and sinks, it also becomes natural to consider that event sinks can also act as sources of new kinds of events. A sink can act as both a sink and a source. A sink might translate one kind of event into another kind of event which it passes on to a new level of sink. You might be tempted to call such an achitecture a multiple level event architecture.
NISTMonte uses a multiple level event architecture. Two common
event sinks that can be attached to MonteCarloSS are the
XRayEventListener and the BremsstrahlungEventListener.
At each electron scattering point, these event sinks compute the
generation of characteristic and Bremsstrahlung x-ray emission
respectively. They do not tabulate the x-ray emission. Instead
these sinks also act as event sources but sources of a different type
of event – an x-ray emission event. Another level of event
sink, x-ray detector sinks, such as the EmissionImage,
the PhiRhoStats, the EDSDetector or the
Microcalorimeter, tabulate the x-ray events in various
different ways.
While this multilevel architecture might seem unnecessarily complex, it is actually quite efficient. Quantities are calculated only if necessary and only once. If we don't care about x-rays we don't attach x-ray event sinks and we don't waste time computing unused x-ray related quantities. However if we do care about x-rays we can compute the x-ray emission only once and tabulate it many times.
In addition, event architectures allow for separation of concerns. It is possible to add new event listeners without modifying the event source. This minimizes dependences between classes and makes code simpler to understand and more robust to enhancement. In computer science parlance, event based architectures scale better.
In life as in software development, often there are many different ways to do the same thing. Starting at the same starting point there are many paths that will ultimately lead to the same (or similar) destination. The actual path taken can be less important that the result.
In software development, this situation is often compared to a black box. Inputs go into the black-box, something happens in the black box, and the black box pushes out a result. From outside of the black box you can't see what is happening in the black box. All that is known is that certain quantities go into the black box and a certain quantity comes out.
The simplest type of black box is a function with a specific argument set and a specific output type. A classic example is the sine function. The input is a double value representing an angle and the output is a double value representing the sine of the angle. It does not matter how the function transforms the input into the output (series expansion, look-up table, continued fraction expansion, ...) so long as the results is a sufficiently good approximation of the true sine. The Java library documentation specifies the standard library Math.sin contract as follows:
sin public static double sin(double a)
Text 1: The Java library documentation (1.4.2) for the Math.sin function. |
Part of this contract, the function signature, is enforced by the compiler - the argument must be a single double and the result a double. However part of this contract is enforced only by agreement between the library developer and the library user. The sin function strictly need not compute the sine. There is nothing stopping the library developer from implementing a function called sin that computes the cosine. Such a library probably wouldn't get used but from the compiler's perspective there is nothing invalid about this implementation. So the contract is partially enforced by the compiler and partially by documented convention.
The idea of a contract can be readily extended from functions to classes. Classes are simply one or more functions (also known as methods) bound to data. It is easy to imagine developing a contract in which a class is required to implement one or more functions. Part of this contract is enforced by the compiler through the definition of an interface. Part of this contract is a less formalized agreement between the library developer and library user. The later part of the contract is usually communicated between the developer and the user through programmer's documentation.
An interface is an organizational construct specifying zero or more function signatures but not the implementations. A class can declare that it implements an interface by identifying the interface and providing implementations for the functions specified in the interface contract. Multiple different classes can implement the same interface multiple different ways. Each class acts like a black box. The knobs on the outside of the box (the methods) are the same but the insides of the box (the implementation) are different.
Interfaces are used throughout the library to facilitate alternative implementations of algorithms. Abstract classes are similar to interfaces. In fact, you might say abstract classes are like a partially implemented interface. An abstract class is a class in which some of the member functions have been declared but have not been implemented. Classes derived from (based on) the abstract class are expected to implement the unimplemented member functions.
Abstract based classes are used extensively. Many times they are implemented as anonymous classes. Anonymous classes are very useful but a little obtuse. An example might be the best way to explain.
|
Text 2: An example of an abstract class and two anonymous implementations. |
Text 2 shows an example class called Operation
which declares but does not implement a function called perform
which takes two integer arguments and returns a single integer. You
can't create an instance of the class Operation directly
because it is not fully defined. However, Multiply and
Add represent two anonymous instances of the abstract
base class Operation each of which implements the
function perform differently. The items Multiply and
Add are static instances of an anonymous class derived from
Operation. They are referenced much like any other static member
is referenced – Operation.Add or
Operation.Multiply. Operation.Multiply.perform(2,3)
would return 6 and Operation.Add.perform(2,3) would
return 5.
Named instances of anonymous implementations of an abstract base class is very powerful and clear technique for implement different ways of computing the same quantity. Each implementation can be identified by a name which is descriptive of the algorithm. The instances can be readily interchanged by referring to the algorithm by the abstract base class name. This is an incredibly powerful technique for interchanging implementations of an algorithm. Text 3 is a trivial example that hints at how powerful the technique can be.
|
int
function(Operation op){ Text 3: An example of how named objects can represent algorithms using anonymous implementations of abstract based classes. |