Class design guidelines are helpful for designing sound classes. This section summarizes some of the guidelines.
Cohesion
A class should describe a single entity, and all the class operations should logically fit together to support a coherent purpose. You can use a class for students, for example, but you should not combine students and staff in the same class, because students and staff are different entities.
A single entity with many responsibilities can be broken into several classes to separate the responsibilities. The classes String, StringBuilder, and StringBuffer all deal with strings, for example, but have different responsibilities. The String class deals with immutable strings, the StringBuilder class is for creating mutable strings, and the StringBuffer class is similar to StringBuilder except that StringBuffer contains synchronized methods for updating strings.
Consistency
Follow standard Java programming style and naming conventions. Choose informative names for classes, data fields, and methods. A popular style is to place the data declaration before the constructor and place constructors before methods.
Make the names consistent. It is not a good practice to choose different names for similar operations. For example, the length() method returns the size of a String, a StringBuilder, and a StringBuffer. It would be inconsistent if different names were used for this method in these classes.
In general, you should consistently provide a public no-arg constructor for constructing a default instance. If a class does not support a no-arg constructor, document the reason. If no constructors are defined explicitly, a public default no-arg constructor with an empty body is assumed.
If you want to prevent users from creating an object for a class, you can declare a private constructor in the class, as is the case for the Math class.
Encapsulation
A class should use the private modifier to hide its data from direct access by clients. This makes the class easy to maintain.
Provide a getter method only if you want the data field to be readable, and provide a setter method only if you want the data field to be updateable. For example, the Rational class provides a getter method for numerator and denominator, but no setter method, because a Rational object is immutable.
Clarity
Cohesion, consistency, and encapsulation are good guidelines for achieving design clarity. Additionally, a class should have a clear contract that is easy to explain and easy to understand.
Users can incorporate classes in many different combinations, orders, and environments. Therefore, you should design a class that imposes no restrictions on how or when the user can use it, design the properties in a way that lets the user set them in any order and with any combination of values, and design methods that function independently of their order of occurrence. For example, the Loan class contains the properties loanAmount, numberOfYears, and annualInterestRate. The values of these properties can be set in any order.
Methods should be defined intuitively without causing confusion. For example, the substring(int beginIndex, int endIndex) method in the String class is somewhat confusing. The method returns a substring from beginIndex to endIndex – 1, rather than to endIndex. It would be more intuitive to return a substring from beginIndex to endIndex.
You should not declare a data field that can be derived from other data fields. For example, the following Person class has two data fields: birthDate and age. Since age can be derived from birthDate, age should not be declared as a data field.
public class Person {
private java.util.Date birthDate;
private int age;
...
}
Completeness
Classes are designed for use by many different customers. In order to be useful in a wide range of applications, a class should provide a variety of ways for customization through properties and methods. For example, the String class contains more than 40 methods that are useful for a variety of applications.
Instance vs. Static
A variable or method that is dependent on a specific instance of the class must be an instance variable or method. A variable that is shared by all the instances of a class should be declared static. For example, the variable numberOfObjects in CircleWithPrivateDataFields in here is shared by all the objects of the CircleWithPrivateDataFields class and therefore is declared static. A method that is not dependent on a specific instance should be defined as a static method. For instance, the getNumberOfObjects() method in CircleWithPrivateDataFields is not tied to any specific instance and therefore is defined as a static method.
Always reference static variables and methods from a class name (rather than a reference variable) to improve readability and avoid errors.
Do not pass a parameter from a constructor to initialize a static data field. It is better to use a setter method to change the static data field. Thus, the following class in (a) is better replaced by (b).
Instance and static are integral parts of object-oriented programming. A data field or method is either instance or static. Do not mistakenly overlook static data fields or methods. It is a common design error to define an instance method that should have been static. For example, the factorial(int n) method for computing the factorial of n should be defined static, because it is independent of any specific instance.
A constructor is always instance, because it is used to create a specific instance. A static variable or method can be invoked from an instance method, but an instance variable or method cannot be invoked from a static method.
Inheritance vs. Aggregation
The difference between inheritance and aggregation is the difference between an is-a and a has-a relationship. For example, an apple is a fruit; thus, you would use inheritance to model the relationship between the classes Apple and Fruit. A person has a name; thus, you would use aggregation to model the relationship between the classes Person and Name.
Interfaces vs. Abstract Classes
Both interfaces and abstract classes can be used to specify common behavior for objects. How do you decide whether to use an interface or a class? In general, a strong is-a relationship that clearly describes a parent–child relationship should be modeled using classes. For example, since an orange is a fruit, their relationship should be modeled using class inheritance. A weak is-a relationship, also known as an is-kind-of relationship, indicates that an object possesses a certain property. A weak is-a relationship can be modeled using interfaces. For example, all strings are comparable, so the String class implements the Comparable interface. A circle or a rectangle is a geometric object, so Circle can be designed as a subclass of GeometricObject. Circles are different and comparable based on their radii, so Circle can implement the Comparable interface.
Interfaces are more flexible than abstract classes, because a subclass can extend only one superclass but can implement any number of interfaces. However, interfaces cannot contain concrete methods. The virtues of interfaces and abstract classes can be combined by creating an interface with an abstract class that implements it. Then you can use the interface or the abstract class, whichever is convenient.