Lazy instantiation is a technique where objects are created only when they are needed. Lazy instantiation is a useful technique in situations where the number of objects in memory at any given time might be quite large but the number of objects that we currently are working with is small. In our example, where we are working with only a single Employee, but we are enumerating through a large set of Employee instances, we could consider managing the collection of Employee instances within a separate class and creating Employee objects only as they are needed. Chapter 9 contains an example of how to use lazy instantiation.
8.2.3 Managing Collections
Another important decision that we must make is which object is responsible for managing sets of other objects. In Figure 8.5, our PayrollController was responsible for managing a collection of Employee objects. Managing collections isn't necessarily a trivial task. We must consider not only how we store the collection, but also how the individual objects within the collection are instantiated.
Again, the decision is based upon how the Employee class is used in other contexts.
In other use cases, if we find we commonly work with only a single Employee, then managing the collection of Employee instances with PayrollController is acceptable. However, if we find that we're constantly writing code that manages a list of employees, we might consider a class that manages a list of employees for us. Not only might this class manage the collection, but it also might also be responsible for knowing how to create the objects within the collection.
Consider that's it's highly likely that Employee objects will be retrieved from some persistent data store. In this case, our EmployeeCollection might know how to retrieve this collection of employees from the database and return individual
Employee objects on demand. Doing so enables us to perform lazy instantiation of the actual Employee objects. We elaborate on this topic in Chapter 9.
8.2.4 Accessor and Mutator Methods
While not used on any of the diagrams we've produced to this point, it's very common to see objects with accessor methods present. Accessor methods also are commonly referred to as simply get and set methods. These methods enable us to obtain the value of an object's attribute (the get), as well as change the value of an object's attribute (the set).
Most developers are familiar with encapsulation, and it's quite common to see classes with all instance attributes declared as private. Accessor methods that allow other objects to access these attributes also are common. On the one hand, it's good because various rules can be associated with the get and set methods to ensure that the attribute is not altered incorrectly. For instance, consider an Employee class with a Salary attribute. Were the Salary attribute public, it would be easy for any other object to access that Salary attribute, setting it to any desired value. However, if the salary attribute is private, and wrapped with accessor methods, we can prevent the setting of that attribute if it doesn't fall within a certain range. For instance, if a rule states that an employee's salary must be between $15,000 and $75,000, the
setSalary method can contain a rule that enforces this restriction. At this point, no other object in the system can change the employee's salary to any amount not within this range. Consider the following accessor methods:
private void setSalary(float salary) {
if ((salary < 15000.0F) || (salary > 75000.0F)) {
throw new SalaryOutOfRangeException("Employee salary must " +
While using accessor and mutator methods is certainly advantageous, it also can be problematic. It still allows another object to access the internal attributes of
Employee, perform some function on that attribute, and then change the value of that attribute on that Employee. Therefore, in a sense, accessor methods can be seen as a violation of encapsulation. While not all accessor methods are violations,
examining the object interactions in many systems typically results in the discovery
that a number of accessor methods are violations. If we find that a single class has accessor methods for each of its attributes, we should question it. It's likely that one of the two following conditions are present when we find accessor methods present:
• The attribute might not belong to the class in which it currently resides and should be moved to the class that is using the accessor method.
• The behavior might not belong to the class in which it is currently defined and should be moved to the class that contains the attribute.
One of the problems with an accessor method might be in name only. For instance, consider our Employee class discussed previously. If we really do desire to set an Employee instance salary, it might be best if we incorporate a bit more meaning into the reason we're setting the salary. For instance, if the salary is being adjusted to provide the employee with a raise, we may consider changing the name of the method to the following:
private void adjustSalary(float salary) {
if ((salary < 15000.0F) || (salary > 75000.0F)) {
throw new SalaryOutOfRangeException("Employee salary must " +
"be between $15000 and $75000");
} else {
this._salary = salary;
}
While this change is certainly subtle, it's simply a much better practice, and it
communicates to other developers who might be using the Employee class what the method actually does much better than what we previously had. Therefore, be guarded against blindly creating accessor methods simply because you suspect another class needs that attribute. Question whether the attribute or method is placed correctly. As we've stated previously, any time we associate a new responsibility with a class, it should be a conscious decision, and even simple accessor and mutator methods fall under this guideline.
8.2.5 Additional Diagrams
Figure 8.7 shows a sequence diagram for the Maintain Time Card use case's primary flow of events. Many of the considerations discussed previously also hold true here.
We see that an Employee interacts with his or her TimeCard. However, we don't see the need for Employee instances to interact with a Paycheck. Each of these situations supports our decisions in the sequence diagrams we created previously, such as that in Figure 8.5. Thus, while it's important to consider each use case independently to manage complexity, it's also important to consider use cases as a
collective whole representing our system because coupling is impacted by those classes that participate in more than one use case.
Figure 8.7. Maintain Time Card Primary Flow Sequence Diagram