Encapsulate Classes with Factory

Prev don't be afraid of buying books Next

Encapsulate Classes with Factory

Clients directly instantiate classes that reside in one package and implement a common interface.



Make the class constructors non-public and let clients create instances of them using a Factory.





Motivation

A client's ability to directly instantiate classes is useful so long as the client needs to know about the very existence of those classes. But what if the client doesn't need that knowledge? What if the classes live in one package and implement one interface, and those conditions aren't likely to change? In that case, the classes in the package could be hidden from clients outside the package by giving a Factory the responsibility of creating and returning instances that implement a common interface.

There are several motivations for doing this. First, it provides a way to rigorously apply the mantra "program to an interface, not an implementation" [DP] by ensuring that clients interact with classes via their common interface. Second, it provides a way to reduce the "conceptual weight" [Bloch] of a package by hiding classes that don't need to be publicly visible outside their package (i.e., clients don't need to know these classes exist). And third, it simplifies the construction of available kinds of instances by making the set available through a Factory's intention-revealing Creation Methods.

The one major issue with this refactoring involves a dependency cycle: whenever you create a new subclass or add/modify an existing subclass constructor, you must add a new Creation Method to your Factory. If you don't often add new subclasses or add/modify constructors to existing subclasses, this is not a problem. If you do, you may wish to avoid this refactoring or transition to a design that lets clients directly instantiate whichever subclasses they like. You can also consider a hybrid approach in which you produce a Factory for the most popular kinds of instances and don't fully encapsulate all of the subclasses so that clients may instantiate classes as needed.

Programmers who make their code available to others as binary code, not source code, may also wish to avoid this refactoring because it doesn't provide clients with the ability to modify encapsulated classes or the Factory's Creation Methods.

This refactoring can yield a class that behaves as both a Factory and an implementation class (i.e., implementing non-creation-based methods). Some are comfortable with this mixture, while others aren't. If you find that such a mixtures obscures the primary responsibility of a class, consider Extract Factory (66).

The sketch at the start of this refactoring gives you a glimpse of some object-to-relational database mapping code. Before the refactoring was applied, programmers (including myself) occasionally instantiated the wrong subclass or the right subclass with incorrect arguments (e.g., we called a constructor that took a primitive Java int when we really needed to call the constructor that took Java's Integer object). The refactoring reduced our defect count by encapsulating the knowledge about the subclasses and producing a single place to get a variety of well-named subclass instances.

Benefits and Liabilities

+

Simplifies the creation of kinds of instances by making the set available through intention-revealing methods.

+

Reduces the "conceptual weight" [Bloch] of a package by hiding classes that don't need to be public.

+

Helps enforce the mantra "program to an interface, not an implementation" [DP].

Requires new/updated Creation Methods when new kinds of instances must be created.

Limits customization when clients can only access a Factory's binary code, not its source code.







Mechanics

In general, you'll want to apply this refactoring when your classes share a common public interface, share the same superclass, and reside in the same package.

1. Find a client that calls a class's constructor in order to create a kind of instance. Apply Extract Method [F] on the constructor call to produce a public, static method. This new method is a creation method. Now apply Move Method [F] to move the creation method to the superclass of the class with the chosen constructor.

  • Compile and test.

2. Find all callers of the chosen constructor that instantiate the same kind of instance as the creation method and update them to call the creation method.

  • Compile and test.

3. Repeat steps 1 and 2 for any other kinds of instances that may be created by the class's constructor.

4. Declare the class's constructor to be non-public.

  • Compile.

5. Repeat steps 1–4 for all classes you would like to encapsulate.

Example

The following example is based on object-to-relational mapping code that is used to write and read objects to and from a relational database.

1. I begin with a small hierarchy of classes that reside in a package called descriptors. These classes assist in mapping database attributes to the instance variables of objects:

 package descriptors; public abstract class AttributeDescriptor...    protected AttributeDescriptor(...) public class BooleanDescriptor extends AttributeDescriptor...    public BooleanDescriptor(...) {       super(...);    } public class DefaultDescriptor extends AttributeDescriptor...    public DefaultDescriptor(...) {       super(...);    } public class ReferenceDescriptor extends AttributeDescriptor...    public ReferenceDescriptor(...) {       super(...);    } 

The abstract AttributeDescriptor constructor is protected, and the constructors for the three subclasses are public. While I'm showing only three subclasses of AttributeDescriptor, there are actually about ten in the real code.

I'll focus on the DefaultDescriptor subclass. The first step is to identify a kind of instance that can be created by the DefaultDescriptor constructor. To do that, I look at some client code:

 protected List createAttributeDescriptors() {    List result = new ArrayList();    result.add(new DefaultDescriptor("remoteId", getClass(), Integer.TYPE));    result.add(new DefaultDescriptor("createdDate", getClass(), Date.class));    result.add(new DefaultDescriptor("lastChangedDate", getClass(), Date.class));    result.add(new ReferenceDescriptor("createdBy", getClass(), User.class,       RemoteUser.class));    result.add(new ReferenceDescriptor("lastChangedBy", getClass(), User.class,       RemoteUser.class));    result.add(new DefaultDescriptor("optimisticLockVersion", getClass(), Integer.TYPE));    return result; } 

Here I see that DefaultDescriptor is being used to represent mappings for Integer and Date types. While it may also be used to map other types, I must focus on one kind of instance at a time. I decide to produce a creation method that will create attribute descriptors for Integer types. I begin by applying Extract Method [F] to produce a public, static creation method called forInteger(…):

 protected List createAttributeDescriptors()...    List result = new ArrayList();    result.add( forInteger("remoteId", getClass(), Integer.TYPE));    ...  public static DefaultDescriptor forInteger(...) {     return new DefaultDescriptor(...);  } 

Because forInteger(…) always creates AttributeDescriptor objects for an Integer, there is no need to pass it the value Integer.TYPE:

 protected List createAttributeDescriptors()...    List result = new ArrayList();    result.add(forInteger("remoteId", getClass()  , Integer.TYPE));    ... public static DefaultDescriptor forInteger(...) {    return new DefaultDescriptor(...,  Integer.TYPE); } 

I also change the forInteger(…) method's return type from DefaultDescriptor to AttributeDescriptor because I want clients to interact with all AttributeDescriptor subclasses via the AttributeDescriptor interface:

 public static  AttributeDescriptor  DefaultDescriptor forInteger(...)... 

Now I move forInteger(…) to the AttributeDescriptor class by applying Move Method [F]:

 public abstract class AttributeDescriptor {     public static AttributeDescriptor forInteger(...) {        return new DefaultDescriptor(...);     } 

The client code now looks like this:

 protected List createAttributeDescriptors()...    List result = new ArrayList();    result.add( AttributeDescriptor.forInteger(...));    ... 

I compile and test to confirm that everything works as expected.

2. Next, I search for all other callers to the DefaultDescriptor constructor that produce an AttributeDescriptor for an Integer, and I update them to call the new creation method:

 protected List createAttributeDescriptors() {    List result = new ArrayList();    result.add(AttributeDescriptor.forInteger("remoteId", getClass()));    ...    result.add( AttributeDescriptor.forInteger("optimisticLockVersion", getClass()));    return result; } 

I compile and test. Everything is working.

3. Now I repeat steps 1 and 2 as I continue to produce creation methods for the remaining kinds of instances that the DefaultDescriptor constructor can create. This leads to two more creation methods:

 public abstract class AttributeDescriptor {    public static AttributeDescriptor forInteger(...) {       return new DefaultDescriptor(...);    }     public static AttributeDescriptor forDate(...) {        return new DefaultDescriptor(...);     }     public static AttributeDescriptor forString(...) {        return new DefaultDescriptor(...);     } 

4. I now declare the DefaultDescriptor constructor protected:

 public class DefaultDescriptor extends AttributeDescriptor {     protected DefaultDescriptor(...) {       super(...);    } 

I compile and everything goes according to plan.

5. I repeat steps 1–4 for the other AttributeDescriptor subclasses. When I'm done, the new code

  • Gives access to AttributeDescriptor subclasses via their superclass.

  • Ensures that clients obtain subclass instances via the AttributeDescriptor interface.

  • Prevents clients from directly instantiating AttributeDescriptor subclasses.

  • Communicates to other programmers that AttributeDescriptor subclasses aren't meant to be public. Clients interact with subclass instances via their common interface.

Variations

Encapsulating Inner Classes

Java's java.util.Collections class contains a remarkable example of what encapsulating classes with Creation Methods is all about. The class's author, Joshua Bloch, needed to give programmers a way to make collections, lists, sets, and maps unmodifiable and/or synchronized. He wisely chose to implement this behavior using the protection form of the Proxy [DP] pattern. However, instead of creating public java.util Proxy classes (for handling synchronization and unmodifiabilty) and then expecting programmers to protect their own collections, he defined the proxies in the Collections class as non-public inner classes and then gave Collections a set of Creation Methods from which programmers could obtain the kinds of proxies they needed. The sketch on page 87 shows a few of the inner classes and Creation Methods specified by the Collections class.

Notice that java.util.Collections even contains small hierarchies of inner classes, all of which are non-public. Each inner class has a corresponding method that receives a collection, protects it, and then returns the protected instance, using a commonly defined interface type (such as List or Set). This solution reduced the number of classes programmers needed to know about, while providing the necessary functionality. The java.util.Collections class is also an example of a Factory.



Amazon


Refactoring to Patterns (The Addison-Wesley Signature Series)
Refactoring to Patterns
ISBN: 0321213351
EAN: 2147483647
Year: 2003
Pages: 103

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net