Table of Contents |
---|
General Java Coding Standards
The Principles of Object-oriented Design
Object-oriented design forces software developers to think in terms of objects, rather than procedures. It is based on the following major techniques: inheritance, polymorphism, encapsulation, and modularity. A set of principles are derived from those techniques and can help us have better understanding about object-oriented design.
Single Responsibility Principle
Panel |
---|
A class has a single responsibility: it does it all, does it well, and does it only. |
Following this principle usually leads to simpler code which is in turn easier to understand, design, and maintain. This principle can also be applied to a method or even a member variable. If a class or method looks too big, it likely has too many responsibilities and probably ought to be refactored into additional methods or even additional classes.
Suggestions:
- Do not create omniscient classes that are all-knowing and all-powerful. (E.g., do not write classes which assume behavior of the implementations of other classes.)
- Keep the maximum length of a method as one screen or one or two pages of program listing, no more than about 150 lines;
- Each variable should have a single purpose.
Open Close Principle
Panel |
---|
Software entities (classes, modules, functions, and so forth) should be open for extension, but closed for modification. |
This principle's guidelines are twofold:
- Keep the class or method from being changed by client developers, which will prevent them from introducing new bugs in the existing code. In order to meet this requirement, the classes or method should perform a single responsibility correctly.
- Design the classes or methods that can be extended to meet new requirements. In order to add new functionality, the client developers can use extending techniques, e.g., adding new subclasses, overriding methods, or reusing existing code through delegation.
Note | ||
---|---|---|
| ||
Our take on this will be slightly different, as we are writing an application which we know that the implementors will want to customize. As such, we need to be careful not to lock them out of customizing pieces of the application. Where we can anticipate customization, we should add hook points to allow for easy overrides. But, private variables and methods interfere with their ability to override something which we did not anticipate. |
Program to an Interface
Panel |
---|
Program to an interface, not an implementation |
An interface specifies the behaviors of an object without defining its implementation. The interface can be implemented by one or more classes with different flavors of logics. In order to increase the reusability of our classes, we want to limit their dependencies on other classes as much as possible. “Program to an interface, not an implementation” is just a powerful and simple way to reduce the dependencies among classes.
The two methods in the following code actually perform the same logic: to get the values from the given key-value pairs. The first method uses the argument of type Map and is able to accept any object of a class implementing the Map interface, for example, EnumMap and TreeMap. The second method, on the other hand, can only accept an object of HashMap or its subclasses. Obviously, the second method is greatly dependent on HashMap, which is not desired.
Code Block |
---|
void retrieveValuesFromMap(Map<String, String> fieldValues);
void retrievevaluesFromHashMap(HashMap<String, String> fieldValues); |
Suggestions:
- During class design, let each class implement at least one interface;
- If a class you need implements an interface, then write your code against that interface;
- If a called class doesn’t implement any interface, then try to write code against its parent far up in the hierarchy;
- Use dependency injection (Spring) whenever possible.
Note | ||
---|---|---|
| ||
This principle applies mainly to services. Our business objects and documents do not follow this principal and the core system assumes concrete classes in a nuber of places. |
Java Programming Paradigms
Enumerated Type
An enumeration is new to Java 5. It is an object type with a finite set of possible values, which is called enum values or enum constants.
Prior to Java 5, public static final int declarations were used as enum values. Here is an example:
Code Block |
---|
public static final int SPRING = 0;
public static final int SUMMER = 1;
public static final int AUTUMN = 2;
public static final int WINTER = 3;
double getAverageTemperatureOfSeason(int season); |
The declarations are still open to another integer values, and even invalid values. For example, “5’ can be passed to the method getAverageTemperatureOfSeason(int). Compared to the public static final int declarations, enumerations provide type safety. In the code below, the four enum values are the only legal parameters for the method getAverageTemperatureOfSeason(Season). If an object of another type is passed in, Java compiler will complain about it.
Code Block |
---|
public enum Season { SPRING, SUMMER, AUTUMN, WINTER };
double getAverageTemperatureOfSeason(Season season); |
Suggestions:
- Avoid implementing enumerations with public static final int declarations;
- Use enumerated types for readability, maintainability and type safety;
- Use enumerated types as an alternative to boolean variables.
Annotations
Java annotations allow developers to add metadata to source code. The metadata can be utilized by tools at compile time and retained in the compiled Java classes for use at runtime. Annotations should never affect the way a Java program runs, but they may affect the behaviors of a compiler or auxiliary tools. For example, the annotation @SuppressWarnings
instructs Java compiler to suppress the specified warning types while the annotation @Transactional
in Spring is used to let the application put the calling class or method in a transactional context.
Currently, there are a few annotations that are available for Kuali Projects:
- Standard annotations defined in Java, including @Deprecated, @Override, and @SuppressWarnings.
- The annotations provided by Spring Framework, for example, @Cacheable and @Transactional.
- Those defined by KFS, such as @ConfigureContext and @NonTransactional.
Java Language Specification requires annotations are not supposed to “directly affect the semantics of a program”. However, there are few standards that dictate what metadata should be used and carried by annotations, so developers are suggested to use annotations with restraint.
Java Generics
Generics were implemented by Java 5 or later. Generics make Java code easier to write and read through making the type of the objects an explicit parameter of the generic code. Using Generics gives us:
- Strong type checking to generic code and issues errors if the code violates type safety. Fixing compile-time errors is easier than fixing runtime errors, which can be difficult to find.
- Code readability (types are far more obvious)
- Cut down on the use of cast and possible run time cast exceptions
Here, we would like to promote the usage of Java generics :
- Design methods and classes with generics whenever possible;
- Specify the types of elements in a Java collection whenever it is used;
- Use Enhanced for-loop whenever possible.
Method-Chaining
A few of programming languages and Java applications are promoting the use of method chaining since it is short and simple. The following code snippet is an example of Java method chaining:
Code Block |
---|
getAccount().getChartOfAccounts().getFinancialObject().getName(); |
This is very convenient, but comes at a cost. When using this syntax, you have no protection from NullPointerException}}s if a link in the chain does not exist. As such, even though it is a pain, you must check the pieces of the chain in sequence. It is not acceptable to catch the {{NullPointerException
, as exceptions should only be used for unanticipated errors, not ones which you could reasonably expect. So the above line would probably have to look something like this:
Code Block |
---|
String name = "Unknown Object Code";
if ( getAccount() != null
&& getAccount().getChartOfAccounts() != null
&& getAccount().getChartOfAccounts().getFinancialObject() != null ) {
name = getAccount().getChartOfAccounts().getFinancialObject().getName();
} |
This works because the Java &&
operator "short-circuits" the expression if the result is known after a certain point. Java evaluates expressions left-to-right, and knows to terminate as soon as it sees a "false" result.
Note | ||
---|---|---|
| ||
The code above assumes normal Java objects. Care must be taken when working with objects retrieved from the database via OJB. If an object is automatically retrieved, as the
which performs the proper checks knowing how to resolve the OJB proxy class. |
Scopes of Variables
Java has three kinds of variables:
- Local variables: are declared within a method body. They are only valid from the line they are declared on until the closing curly brace of the method or code block within which they are declared.
- Instance variables: are declared inside a class body but outside of any method bodies. They can live as long as their enclosing object. Their accessibility can be private, protected, default (package private) or public.
- Class variables: are declared inside a class body but outside of any method bodies as instance variables, but they are static. Their existence is not dependent on the creation of an object. They are available globally from the time the enclosing class is loaded into JVM.
Suggestions:
- Use a variable for a single purpose;
- Keep the life time of variables as short as possible;
- Declare local variables immediately before their use;
- Avoid instance variables if the enclosing object can be accesses by multiple threads concurrently.
Configurability
Panel |
---|
Levy's Eighth Law: No amount of genius can overcome a preoccupation with detail. |
The Law above shows that details are difficult to deal with. For a business-oriented application, details can be everything that needs to be changed with time, such as business rules, constants, and prompt text. If the details are not appropriately manipulated, they can be duplicated and mingled with business logics, which makes the application code hard to learn and modify. Whenever changing the code to accommodate new requirements, we run the high risk of breaking it because failing to uncover all details related to the change.
Ideally, an application should be highly configurable -- all details should be declared out of application code, a set of metadata are defined to represent the details, and the code only interacts with the metadata, instead of the details themselves.
Suggestions:
- Avoid hard-coding constants in the business logic;
- Put constants into the central constant classes or properties files unless they have no purpose in any other component of the application;
- Put non-generic business rules in a separate places, rather than mix them with business logics;
- Keep localization (L10N) and internationalization (I18N) in mind.
Services
- All business logic should be placed in stateless services.
- A client should use an aggregate of services, rather than re-implementing the related logic.
- If an existing service is unable and the required functionalities are out of scope of the client, implement them with an additional service class which utilizes existing services.
- A client should have certain liberties to determine what kinds of things would be included in the results of a service.
Application Architecture and Design Patterns
Component-based multi-tier application architectures have been invented and applied for a long time. Those architectures can greatly help us organizing the application components and promoting their reusability by defining layers of responsibilities.
An application typically has capabilities to access data sources, apply business logic and generate system views for users. Those capabilities can be provided by three components in three layers:
- Presentation layer: represent users to send requests to the underlying system, and present processed data to the users;
- Business Logic layer: encapsulate the business logic of an application, and provide an entry point for operations on the system as a whole;
- Data Access layer: provides a consistent view of the data to business logic layer, and access to the underlying data sources, such as relational database, XML repositories, and flat files.
The architecture logically divides the entire application functionalities into three components and places them in separate logical layers. In the implementation of an application, an object called business object can be used to glue the three layers together by facilitating uniformed data representation.
Within each layer, there might be one or more design patterns applied to implement corresponding component. Four design patterns will be introduced here.
Business Object/Transfer Object
(Avoid complex business logics, rather than put them into business service)
A business process manages business data and business logic/rules. Business data usually Business Objects is used to separate business data and logic.
Application Service
(Avoid direct external resource access)
Data Access Object
(Database, file system and other external data sources)
Model-View-Controller
Suggestions:
- Each layer has a unique responsibility. Avoid responsibility leaking among layers;
Thread Safety
All web applications are multi-threaded applications. This means that a particular section of code may be accessed at the exact same time by multiple users. As such, you must never store an operation's state in a variable which could be accessed from multiple threads. In the best case, an error will occur when the user's sessions collide. In the worst case, there would be silent data corruption as one user's data is saved over the other's.
This is why all services in the application must be stateless, depending only on their passed parameters.
Logging
Kuali defaults logging to the INFO
level. This means that, in production, any messages at INFO level or higher will go into the application logs. Since this is the primary means of monitoring system operation, we need to be careful to not overload these logs with too much detail, as that tends to obscure any problems which may occur. (It may also cause critical troubleshooting information to be rotated out of the logs too soon.)
...