Spatial Interaction Modelling: Structuring into methods
[Practical 5 of 11 - Part 2]
Factoring Out Model Errors:
We have now refactored the original spatial interaction model code into more sensible blocks. Ones to load the data and ones to run the model in separate classes. Within those classes we have factored the code into separate blocks to load different parts of the data or calculate different sections of the model equation.
This is all good progress but it has introduced a series of errors. These can be seen by the white on red highlighted exclamation marks on the class, package and project symbols and on the class tab in the text editor area. The exact lines where the compile-time errors have been identified are shown in the right hand scroll bar of the text editor area by the red bars, the little black line is the location of your cursor. This can be seen in Figure 16.
Lets correct the first error in the class, this is on line 17 in Figure 16. This error occurs because we
are trying to access the length
attribute of the origins
array. However, we no
longer have an origins
array. The first thing is to gain access to the data where the array
holding the origin data can be accessed.
When we calibrate models we have the requirement to run them across a dataset many times but we only want to load the data once as this can be a processor intensive operation taking some time to complete. So we don't want to put the call to load the data within the model. The best way to handle the data is as a parameter which is preloaded and passed into our model when it is constructed.
To do this lets create a constructor for our model which takes a parameter of type DataHandler
.
Make the constructor public
access. To be explicit that we don't want anyone to make a
Model
object without giving it some data to work from we will also declare an empty
constructor that takes in no parameters and give that private access.
So now we can create a Model
object and provide a DataHandler
loaded with data.
But none of the other methods can access it, we need to expose it at the class level. Declare an instance
variable of type DataHandler
and call it data
with private
access.
Inside our constructor simply set the instance level variable data
to equal the parameter
data
.
Now we have some data we can use this to get to the length of the origins array. Replace the code
origins.length
in the for loop with a call to the origins array in the data
object. We then use the .length
to access the length property of the array that is returned.
We can do all of this on one line as in Figure 18, or we could return the origin array into a declared
array and use that local variable to access the attribute. The two approaches are the equivalent. To
use the second approach we would use the code double[] origins = data.getOrigin();
and then
use the local variable origins
to access the array length attribute in the for loop with
for( int i = 0; i < origins.length; i++){
.
Note, after you have typed the name of the data
object and used the .
operator you are provided with a list of accessible options. This is called intelisense and is another
way that the IDE tries to make our lives easier... We don't have to remember everything!
We can use the same approach for the destination loop as shown in Figure 19. The only difference here is
that instead of calling the getOrigin()
method we call the getDestination()
.
We can also alter the code to access the values. For the origin and destination values we must remember
that we are getting an array back so using the accessor method will not provide one double
value but an array object. To get to the individual value we need to still use the index of the element.
There are two ways to approach this. The first is to get a local reference to the array by creating a variable pointing to it. Remember that getting a local reference just puts another label on the object it does not create another object.
So we could do double[] origins = data.getOrigin();
and
then use the local variable like this double tij = ai * origins[i] * Math.pow(destinations[j],
alpha) * Math.exp(distances[i][j] * beta);
What we are going to do is a little more elegant than that. We will access the data directly from the
data
object. We get the array object using data.getOrigin()
and then access
the required element using the [i]
. So the call becomes data.getOrigin()[i];
.
We can use the same approach for both origin and destination values. See Figure 20.
Accessing the distance value is much more straight forward. We changed the accessor so that it returns a single double value dependent on the indexes being correct. However, the index bounds we use to control the loops are not the distance array bounds so we need to check and make sure we are not entering incorrect index values.
To check the index values we can test the distance return value. If it is -1.0 then we know things have gone wrong and to check our input data and we will report this to the screen in a helpful way.
First of all we get the distance value for this origin and destination pair using
double distance = data.getDistance(i, j);
. We can test the value and if it is greater than
-1.0 do the calculation otherwise report the failure to the screen. Wrap the equation
in an if(){}else{}
to do this. If you get stuck the code is shown in Figure 21.
We are nearly there. If you look at line 63 in Figure 21 you will see that we have introduced another
error. This is a scope error. The variable tij
is now created inside the if
statement, but we are trying to use it outside of the if
statement. The variable only exists
within the brace block where it is created, so by the time the code gets to line 63 the variable no longer
exists.
This is relatively easy to fix but it is important you understand what is going on here. To fix it we can
simply declare the variable above the if
block and then use it inside and outside. Alter the
code to move the variable declaration as in Figure 22.
Figure 22 shows that there is only one error left to solve in this section of code. We no longer calculate
the balancing factor in the same scope as the model equation so the ai
variable is no longer available.
To solve this we are going to create an instance variable for
ai
and set this within our balancing factor code. First create an instance variable for
ai
of type double with private access as shown in Figure 23.
One problem remains with this solution, the ai
balancing factor will always equal 0.0 because
it is never calculated! We need to put some calls in to calculate the balancing factor. Place two calls
to the method calculateBalancingFactors
with the parameters configured as shown in Figure 24
Factoring Out Balancing Factor Errors:
We have six errors in the balancing factor code and the same approach can be use here to fix three of
these as we used in the main model code. To begin with replace the two calls to the
destinations
array with data.getDestination()
as shown in Figure 25.
We need to access the distance between the origin and destination next. However, we don't know which origin or destination the model is on. The destination is less important in this case, but in other model configurations that too would be required so we will set both. For this reason we will set both indexes so that the balancing factor code can access them.
To enable the balancing factor code to access the current origin / destination in the model code we will
adjust the for-loop counters i
and j
. First create two instance variables called
i
and j
with private
access and of type int
as shown
in Figure 26.
Next the declaration of the i
and j
for loop counters are removed in the
calculate
method as shown in Figure 27. Change the for loops from
for(int i = 0; i < data.getOrigin().length; i++){
to
for(; i < data.getOrigin().length; i++){
and from
for(int j = 0; j < data.getDestination().length; j++){
to
for(; j < data.getDestination().length; j++){
The origin and destination cycles now use the instance level counters instead of creating there own. This means that the index of the origin and destination can be accessed anywhere in the class.
This means that the j
value will not automatically reset on each origin cycle. Therefore,
this must be done manually. The code on line 90 in Figure 27 demonstrates this. Insert this line after
the closing brace of the destination cycle but before the closing brace of the origin cycle.
Caution must be exercised in this situation, reading from the i
and j
variables is fine, but if we alter their values it will impact on the cycling within the for-loops!
Now the distance can be accessed using the instance level index i
and the local destination
index j
. Replace the array reference with the accessor for the distance value
data.getDistance(i, j)
as highlighted in Figure 28.
We do not need to check the return of the distance value as this is done and any discrepancies reported in the main model equation code.
Having a local level and instance level variable j
is both confusing
and bad practice, although quite legal in Java. You can specify access to the instance level variable
using the keyword this
and access the local variable in the normal way by direct reference.
To keep our code tidy we are going to change the local variable name to jLocal
. In the left
hand margin of the text editor you will see a little yellow light bulb, left click on this and you will
see the options similar to the ones in Figure 29 presented. Select Rename the local variable.
You can then type in the new variable name jLocal
and the variable name will be changed
throughout the method. When you have finished typing the new name press enter.
There are two alterations left to do in this class.
- Make the
alpha
andbeta
parameters accessible to the whole class. - Make the
ai
variable accessible to the whole class.
We have already created an instance variable ai
. The only change we need to make is to delete
the keyword double
from the highlighted line in Figure 30 removing the declaration of a local
variable. The balancing factor will now be stored in the instance level variable.
To make the alpha and beta parameters class level we first need to create two instance variables to hold
the values. Create two private
instance level variables of type double
, one
called alpha
and the other called beta
as shown in Figure 31.
In the method calculate
when the alpha
and beta
parameters are
passed in we simply assign them to the class level variables we just declared, Figure 32. The keyword
this
is used to specify which are the instance variables.
We have now refactored our classes.
Running the Refactored Model:
To run the refactored model we need to add a few lines of code to the main
method in the
SpatialInteractionModel
class.
Below the equation parameters create a new instance of the DataHandler
class
called dh
. Call the loadData()
method on the new DataHandler
object
dh
.
Below these lines create a new instance of the Model
class called
model
using the dh
object as the parameter. Finally, call the
calculate
method on the object model
passing in the alpha
and
beta
parameters.
Once you have done this your code should look like that in Figure 33. You can now run the model by right
clicking on the SpatialInteractionModel
class in the project area and selecting
Run File as you did previously.
Note: the results from running the refactored model with the same parameter configuration will be the same as running the code at the beginning of the practical. It is the structure of the code that has been altered not the functionality.
Summary:
- A class should have a well defined purpose, a specific job to do.
- The process of adjusting code to make it more elegant and flexible is called refactoring.
- Classes are further broken down into sections of code called methods.
- Methods are small sections of code that should be easy to read and understand.
- Structuring code into classes and methods de-couples different tasks.
- De-coupling of tasks facilitates good design and more code re-use.
- Constructors are a special kind of method called when a new object is made from the class.
- Both constructors and methods can be parametrised.
- Both constructors and methods can have access control keywords applies
private
,public
orprotected
. - A method can be declared as
static
but a constructor cannot. - Intelli-sense are the popups that appear to assist with finding methods belonging to a class or object.
- Netbeans has several functions to assist with refactoring code, many of these are found in the Refactor menu.