Object factories enable us to decouple the instantiation of an object from the object that actually uses the instance. A more detailed discussion on object factories can be found in Section 9.3.2, later in this chapter.
We've also solved our type safety problem identified when using simple attributes. In this situation, we have only three classes that can be instantiated. We don't have the ability to create an instance of any other type of class that could cause a similar problem.
While a bit more complex, this approach certainly is more flexible than the solution illustrated in Figure 9.7. However, it doesn't come without its disadvantages. First, inheritance is a very static solution space. Were we to identify additional behavioral variants that didn't fit into the realm of our subclasses, we would be left with a structure that doesn't accommodate our behavior. For instance, consider a new or changing requirement that states that employee benefits are deducted from paychecks based upon the status of the employee. Unfortunately, the employee status may not fall along the same lines as the employee type; our employee status may fall into one of three categories梖ull-time, part-time, or on-leave. If this new requirement impacts how salary is calculated, or impacts any behavior on the Employee class, we might find we're relegated to using a simple attribute with conditionals. Let's examine some of our options.
First, we can try subclassing each of our employee descendents directly and create separate status classes for each employee type subclass, which would result in a proliferation of classes and a high probability of duplicate code. In essence, we would have a full-time, part-time, and on-leave set of subclasses for each employee
descendent, resulting in nine new classes. This option is extremely inflexible with many disadvantages.
Second, we could define default status behavior in our ancestor Employee class and override this behavior in descendents with variant behavior. This option, however, is not without its problems. For instance, this solution doesn't work well if a salaried employee has different calculations for salaries depending on the status, which would result in complex conditionals for salaried employees based on whether they are full-time, part-time, or on-leave.
Another problem with using this subclass approach becomes apparent when we ask,
"What happens if an employee can switch types in the middle of a pay period?" For instance, the first two days of the week, the employee's status is full-time, and the remaining three days, the employee is commissioned. Or perhaps an employee
receives a small salary plus commissions. The approach in Figure 9.8 assumes that we have a single descendent instance for each Employee. Doing so results in overhead if we have common attributes defined on the Employee class because each of these attributes will be loaded and stored in memory twice. Therefore, our system shouldn't represent a single employee with multiple Employee instances in the system.
If it's not yet apparent that our system shouldn't represent a single employee with multiple Employee instances in the system, we need only consider some of the other uses of our Employee class within our system and across use cases. When
maintaining a time card, an employee is required to log on to the system by entering a user ID and password. Part of the authentication and later authorization process of that employee in accessing various functions of the system might be to store the
Employee instance on a context object within the application, such as the Session object in a Java Web application. If an employee is both salaried and commissioned, we wouldn't want to store two objects in memory repre senting that single employee 梩hat doesn't make sense, nor does it work out very well.
Discriminator
The discriminator is a term used to describe the single variant that is the cause for our creation of subclasses. The relationship between any
descendent and ancestor should involve only a single discriminator. If we find that a ancestor-descendent relationship has multiple discriminators, we should carefully consider an alternative solution, such as composition.
Sometimes, however rare, subclassing the descendent based on another
discriminator works, too. This solution, however, isn't without its problems either.
In summary, while this second approach works well for simple situations, it doesn't scale well as complexity increases or if we use the Employee class itself to store common attributes and behaviors. Any time we decide to use inheritance, we should fully understand the discriminator that exists that is the cause for the creation of the descendents.
Figure 9.9 shows our final alternative. It depicts a single Employee class that's composed of an EmployeeType abstract class. We again see that upon instantiation of the Employee class, the EmployeeType is passed to the constructor. While a bit more complex, this solution is much more flexible. We certainly have the ability to extend EmployeeType and create new types of employees without modifying the Employee class directly. Hence, we have achieved OCP.
Figure 9.9. Employee Class Composed of EmployeeType
In addition, were a new requirement introduced that necessitated incorporating employee status into the structure, we easily could create a new EmployeeStatus abstract class with the appropriate descendents to satisfy status-specific behavior. In other words, our inheritance hierarchies are very small and involve only a single discriminator.
This approach is actually a direct application of CRP, which we view as the most flexible solution. However, it should be used only in those situations that require this degree of flexibility, due to the associated complexity.
9.3.2 Factories
Object factories are specialized design classes whose intent it is to create instances.
Factories are useful when we want to decouple the process of creation from the classes that ultimately will use the created instance. Factories typically are used in situations where we deem the creation of objects to be fairly complex algorithmically, or we wish to decouple classes from a particular implementation. We certainly have this second case in the diagram in Figure 9.9.
Structurally, the Employee class is not dependent on any of the concrete
EmployeeType classes. However, we must ask ourselves how we intend to create concrete instances of the EmployeeType. In Figure 9.9, an EmployeeType instance is passed to the constructor of the Employee class. This implies, however, that the class using Employee must be aware of the various EmployeeType subclasses, which may be undesirable because the class using Employee most likely has other responsibilities. For instance, one of the classes in our Run Payroll use case is the PayrollController, and referring to the sequence diagram in Figure 8.5 back in Chapter 8, we see that this PayrollController creates an Employee instance. We can assume that we'll pass the EmployeeType descendent to the constructor of the Employee class. Of course, this sce nario implies that the PayrollController must create the appropriate EmployeeType instance.
The creation of EmployeeType instances certainly isn't overly difficult. However, examining the sequence diagram in Figure 8.7 in Chapter 8, we see that the
LoginForm also instantiates an Employee, which implies that we must also create the appropriate EmployeeType here. Again, this task isn't difficult; however, stepping back and examining the overall picture, we see a subtle violation of OCP. If it's necessary to extend the system with new EmployeeType subclasses, we must ensure that both PayrollController and the
MaintainTimecardController be modified to create the new
EmployeeType subclass appropriately. Thus, we've achieved closure within the Employee class because it need not be modified when we subclass
EmployeeType. However, the remainder of the system is wide open to maintenance difficulties.
A case certainly can be made for the Employee class containing the responsibility for creating its own EmployeeType subclass. The problem with this option is that the Employee class would no longer be closed to modification, which is the situation we've been trying to avoid all along and is the reason we created the
EmployeeType hierarchy. Therefore, we require an approach that results in closure for both the Employee class and the classes using Employee. To implement this approach, we'll use an object factory.
The diagram in Figure 9.10 is an updated version of that in Figure 9.9 and includes a new class named EmployeeTypeFactory. The intent of this class is to create EmployeeType subclass instances. Another key change has been made to this diagram as well. We no longer pass the EmployeeType to the constructor of the Employee class but instead pass the employee ID. This employee ID is obtained within the PayrollController when it retrieves a list of employees for which payroll must be processed. If we pass this employee ID to the constructor of the Employee class, the Employee class can be responsible for retrieving its own internal data set, which includes an indicator that informs this instance of the type of Employee, which we'll assume is a simple numeric field on a relational database table. The Employee class invokes the static getInstance method on the EmployeeTypeFactory, passing this indicator. Based on this indicator, the getInstance method returns the appropriate EmployeeType instance.
Figure 9.10. Employee Usage of an Object Factory
We've achieved closure for our entire system in regard to EmployeeType, aside from the EmployeeTypeFactory, which is perfectly acceptable because creation must occur somewhere, implying that it's virtually impossible to achieve closure systemwide. However, because of the cohesive nature of the
EmployeeTypeFactory, it should be apparent to maintainers that new
EmployeeType descendents require an update to our EmployeeTypeFactory.
While this class isn't closed to modification, the remainder of our system is, including Employee, PayrollController, and MaintainTimecardController.
The tradeoff we've made is an additional class and some additional complexity for increased maintainability and system closure. In the majority of cases, we should be willing to make this tradeoff. Should we need to extend the system with an additional
EmployeeType in the future, we need only make one small change to
EmployeeTypeFactory. We don't have to search the entire system looking for classes that create the appropriate EmployeeType and make the change there as well. Nor do we have to search the Employee class itself.
While ease of maintenance certainly is a benefit of the approach we've just described, there's another, less obvious, benefit. Whenever we need to extend our system, the less existing code that we have to change, the less likely we are to introduce new bugs into our system. Considering the small change we have to make only to
EmployeeTypeFactory, we're virtually guaranteed that we won't introduce new errors into the system梕xcept for errors in the new EmployeeType subclass. When we run our test cases, if we find that we've introduced a bug, we know that it is either in the EmployeeTypeFactory class, which is unlikely because of the simplistic nature of the change, or the new EmployeeType subclass. However, because the remaining classes in the system worked before, they still work because they have not been changed.
Quite a few variations of object factories are available. [GOF95] discusses five types of object factories, which are categorized as creational patterns. Depending on the flexibility required, we may wish to utilize one of these more advanced creational designs to further improve the maintainability of our system. These additional creational patterns are, however, beyond the scope of this book.
9.3.3 Collections
In our previous discussion on factories, as well as in Chapter 7, we realized that PayrollController must retrieve a list of employee IDs for which we'll process payroll. In its simplest form, this list of employee IDs can be stored as a simple ArrayList. However, the PayrollController still is responsible for
retrieving the list of employee IDs and storing them on ArrayList. As we realize other use cases, if we find that we're repeatedly writing code to manage a list of employees, we might find that a custom collection class is a better approach.
In our situation, we might create a custom EmployeeCollection class that manages a list of employees. It's quite possible that this custom collection class is simply a thin wrapper around one of the Java collection classes, such as ArrayList.
A number of benefits are associated with a custom collection class:
• Other classes that must work with employee collections no longer have to manage the collection internally. Instead, they can use the collection class, which reduces the likelihood that we'll have multiple classes in our application with duplicate collection management code.
• The internal management of the list can change very easily. We can start by managing only a simple list of employee IDs. If necessary, we can change the implementation to manage a list of Employee objects in the future.
• If the list of objects in the collection is quite long, we can employ lazy instantiation. In this case, the collection isn't really managing a list at all but actually is creating the appropriate instances as needed. Clients of the collection, however, still see it as a list.
Let's take a look at how this custom collection class might be designed. Figure 9.11 is an updated version of the diagram in Figure 9.10. In Figure 9.11, we've introduced our EmployeeCollection class. The PayrollController class is no longer responsible for managing the list of Employee instances. Instead, this work is the responsibility of the EmployeeCollection class, and the
PayrollController simply takes advantage of this behavior.
Figure 9.11. Employee Collection to Manage Lists of Employees
As can be seen from the code example in this diagram, the EmployeeCollection constructor retrieves a listing of all the Employee instances that the collection must manage. The PayrollController simply calls the next method to retrieve the next Employee.
A couple of interesting behavioral traits of the EmployeeCollection class
deserve discussion. First, notice the return data type of the next method. Because the EmployeeCollection class is responsible for managing only a list of
Employee instances, we perform the cast before we return the instance to the calling object. When using a standard Java collection class, a cast typically must be
performed each time we get elements off the list. Because we know we're dealing with only Employee instances, there's no reason not to make this cast the responsibility of the EmployeeCollection.