|
Design aspects of security, testability, reusability and concurrency all benefit when we take steps to strictly control paths of execution through the code base. Limiting execution paths provides a way of focusing scarce developer resources on the most common code pathways while limiting the likelihood that problems associated with less common pathways will occur. The approach is: if you can't fix a problem, at least hide it.
General Design-4 : Wherever possible, limit the paths of execution that are likely to occur.
One of the best ways of limiting execution paths is to properly encapsulate access to an object's data through accessor methods. It is considered a good coding convention to declare class variables as private and make them accessible to client code through set and get methods. Hiding the class variables from other classes provides two benefits:
General Design-5 : By default, give class variables and class methods the most restrictive visibility.
Execution paths can be further constrained by limiting the visibility of classes with respect to one
another. The RIF code base has been organised into packages of classes that share a similar theme.
For example, all the business classes used to support study submission and viewing study results appear
in the rifServices.businessConceptLayer package. Wherever possible, classes are given package-level
visibility so that classes in other packages cannot use them. For example, whereas
rifServices.dataStorageLayer.SQLInvestigationManager
has a need to see the class
rifServices.businessConceptLayer.Investigation
, there is no need for the Investigation
class to see the SQLInvestigationManager
. The SQLInvestigationManager
class is
given package-level access so that only the other classes in rifServices.dataStorageLayer may see it.
Throughout the course of development, the visibility of some classes has had to be made public. For
example, rifServices.dataStorageLayer.SQLConnectionManager
now has public-level access
because code used in the services for the data loader tool need to use the same connection
management facility.
General Design-6 : By default, give classes package-level access. Increase the visibility of a class only when there is a need to do so.
So far, we have limited the execution paths in the code by restricting the visibility of an object's data and methods, and by limiting the access that one class has to use another class. Our third way of limiting execution paths is to encapsulate the data storage and business concept layers of the architecture using service interfaces.
The access of client applications to the code base is limited to a set of methods that are published as part of service APIs. Although the service APIs have public-level access, the classes which implement them do not. We do this to promote secure access, so that clients cannot access methods that are part of the class but which are not part of the service contract advertised in the interface.
General Design-7 : Encapsulate business concept and data storage layers of the architecture through service APIs. Do not allow clients to know which class is implementing the service interfaces.
Another general design decision is embodied in the Coding Philosophy. We state it here because it helps lead into describing some of our general coding conventions:
General Design-8 : Apply the steps of the coding philosophy:
- get it working
- get it working for the next developer
- get it working well
As well, favour coding to convention over coding to circumstance.
Forcing client applications to interact with the code base via interfaces greatly limits sources of security and concurrency threats. Assuming that the RIF database is installed on a secure Intranet, the main source of threats will come via calls to the service methods. As well, a major source of concurrency threats will occur when clients have multiple threads accessing the objects they pass to the service calls. Both of these issues, and the broader topics of concurrency and security, will be addressed in other sections of the design manual.
Another way to minimise problems with security and concurrency is to make sure that all classes which are not abstract are marked final. "Final" ensures that a class cannot be sub-classed. Preventing a class from being sub-classed eliminates the chance that a malicious class can override one of its superclass methods and produce a damaging side-effect. It also ensures that a subclass will not have methods which introduce new concurrency problems that could occur in the superclass. In both cases, preventing a class from being sub-classed helps ensure that the code will have more predictable behaviour.
Developers are divided about whether this practice is beneficial in projects or not. One opinion is that marking classes as "final" is a practice which is too restrictive and hinders other developers from adapting or reusing the code. In my opinion, it is more important to assert that code suits its original purpose than to assert how it could be adapted for other uses. Reuse presumes use, and use of software presumes that it behaves both correctly and predictably.
Although we would like other developers to use our code, it is not a project priority and it should not outrank more important concerns related to security, concurrency and testability. Our minimum unit of software reuse in the RIF project is not at the class level but at the service level. Because we have made this decision, we do not feel compelled to make the code easy to adapt. Instead, we take a very different approach: until we assert that a class should be extended, it should not be extended.
Marking classes as final does not prevent developers from modifying the code to suit their interests. However, the effort they spend to make the changes should cause them to pause for thought and ask themselves if they are sure they know what the code will do.
General Design-9 : If a class is not abstract, it should be marked "final".
By default, all parameters in all methods will be prefixed by the keyword "final". When a piece of code calls
a method, it passes actual parameters which are assigned to formal parameters. In the example below, the
variable investigation
is declared and assigned the same reference as myInvestigation
.
Later on, investigation
is reassigned the reference to
Later on in the code, we are able to reassign investigation
the reference for
anotherInvestigation
. From this point onwards, any changes we attempt to make to the investigation's
fields will affect the ones in anotherInvestigation
, not investigation
Calling code:
{
Investigation myInvestigation = Investigation.newInstance();
myInvestigation.setName("Investigation 1");
...
...
x.doSomething(myInvestigation);
//You'd think it prints "Investigation 2"
//but it actually prints "Investigation 1"
System.out.println(investigation);
}
public void processInvestigation(Investigation investigation) {
...
//Should print "Investigation 1"
System.out.println(investigation);
//this is a bad idea but it will run without error
Investigation anotherInvestigation = Investigation.newInstance();
investigation = anotherInvestigation;
//we are no longer altering a field value in the
//Investigation object that was passed to the method
investigation.setName("Investigation 2");
...
...
}
Few developers would intentionally try to reassign the value of a method parameter within the method. However, they could do it accidentally, thereby introducing errors which may be difficult to detect in some situations.
If lone developers are maintaining large amounts of code, they may want to leverage the
power of the compiler to identify mistakes they make. If investigation
is declared as final
, the compiler will complain that investigation is being
reassigned a value.
by Kevin Garwood, Nan Lin