Unify Interfaces with Adapter

Prev don't be afraid of buying books Next

Clients interact with two classes, one of which has a preferred interface.



Unify the interfaces with an Adapter.



Motivation

Refactoring to an Adapter [DP] is useful when all of the following conditions are true.

  • Two classes do the same thing or similar things and have different interfaces.

  • Client code could be simpler, more straightforward, and more succinct if the classes shared the same interface.

  • You can't simply alter the interface of one of the classes because it's part of a third-party library, or it's part of a framework that many other clients already use, or you lack source code.

The smell Alternative Classes with Different Interfaces (43) identifies when code could be communicating with alternative classes via a common interface but for some reason does not. A simple way to solve such a problem is to rename or move methods until the interfaces are the same. If that isn't possible, say, because you're working with code you can't change (like a third-party class or interface, such as a DOM Element), you may need to consider implementing an Adapter.

Refactoring to an Adapter tends to generalize code and pave the way for other refactorings to remove duplicate code. Typically in this situation you have separate client code for communicating with alternative classes. By introducing an Adapter to unify the interfaces of the alternative classes, you generalize how clients interact with those alternative classes. After that, other refactorings, such as Form Template Method (205), can help remove duplicated processing logic in client code. This generally results in simpler, easier-to-read client code.

Benefits and Liabilities

+

Removes or reduces duplicated code by enabling clients to communicate with alternative classes via the same interface.

+

Simplifies client code by making it possible to communicate with objects via a common interace.

+

Unifies how clients interact with alternative classes.

Complicates a design when you can change the interface of a class rather than adapting it.







Mechanics

1. A client prefers one class's interface over another, yet the client would like to communicate with both classes via a common interface. Apply Extract Interface [F] on the class with the client's preferred interface to produce a common interface. Update any of this class's methods that accept an argument of its own type to accept the argument as type common interface.

The remaining mechanics will now make it possible for the client to communicate with the adaptee (the class with the interface the client does not prefer) via the common interface.

  • Compile and test.

2. On the client class that uses the adaptee, apply Extract Class [F] to produce a primitive adapter (a class containing an adaptee field, a getter method for the adaptee, and a setter method or constructor parameter and code for setting the adaptee's value).

3. Update all of the client class's fields, local variables, and parameters of type adaptee to be of type adapter. This involves updating client calls on the adaptee to first obtain an adaptee reference from the adapter before invoking the adaptee method.

  • Compile and test.

4. Wherever the client invokes the same adaptee method (via the adapter's getter method), apply Extract Method [F] to produce an adaptee invocation method. Parameterize this adaptee invocation method with an adaptee and make the method use the parameter value when it invokes the adaptee method. For example, a client makes an invocation on the adaptee, current, which is of type ElementAdapter:

 ElementAdapter childNode = new ElementAdapter(...); current.getElement().appendChild(childNode); // invocation 

The invocation on current is extracted to the method:

 appendChild(current, childNode); 

The method, appendChild(…), looks like this:

 private void appendChild(    ElementAdapter parent, ElementAdapter child) {    parent.getElement().appendChild(child.getElement); } 

  • Compile and test. Repeat this step for all client invocations of adaptee methods.

5. Apply Move Method [F] on an adaptee invocation method to move it from the client to the adapterr. Every client call on the adaptee method should now go through the adapter.

When moving a method to the adapter, make it resemble the corresponding method in the common interface. If the body of a moved method requires a value from the client in order to compile, avoid adding it as a parameter to the method because that will make its method signature differ from the corresponding method on the common interface. Whenever possible, find a way to pass the value without disturbing the signature (e.g., pass it via the adapter's constructor, or pass some other object reference to the adapter so it can obtain the value at runtime). If you must pass the missing value to the moved method as a parameter, you'll need to revise the corresponding method signature on the common interface to make the two equivalent.

  • Compile and test.

Repeat for all adaptee invocation methods until the adapter contains methods with the same signatures as the methods on the common interface.

6. Update the adapter to formally "implement" the common interface. This should be a trivial step given the work already accomplished. Change all adapter methods that accept an argument of type adapter to accept the argument as type common interface.

  • Compile and test.

7. Update the client class so that all fields, local variables, and parameters use the common interface instead of the adapter's type.

  • Compile and test.

Client code now communicates with both classes using the common interface. To further remove duplication in this client code, you can often apply refactorings like Form Template Method (205) and Introduce Polymorphic Creation with Factory Method (88).

Example

This example relates to code that builds XML (see Replace Implicit Tree with Composite, 178; Encapsulate Composite with Builder, 96; and Introduce Polymorphic Creation with Factory Method, 88). In this case, there are two builders: XMLBuilder and DOMBuilder. Both extend from AbstractBuilder, which implements the OutputBuilder interface:

The code in XMLBuilder and DOMBuilder is largely the same, except that XMLBuilder collaborates with a class called TagNode, while DOMBuilder collaborates with objects that implement the Element interface:

 public class DOMBuilder extends AbstractBuilder...    private Document document;    private Element root;    private Element parent;    private Element current;    public void addAttribute(String name, String value) {       current.setAttribute(name, value);    }    public void addBelow(String child) {       Element childNode = document.createElement(child);       current.appendChild(childNode);       parent = current;       current = childNode;       history.push(current);    }    public void addBeside(String sibling) {       if (current == root)          throw new RuntimeException(CANNOT_ADD_BESIDE_ROOT);       Element siblingNode = document.createElement(sibling);       parent.appendChild(siblingNode);       current = siblingNode;       history.pop();       history.push(current);    }    public void addValue(String value) {       current.appendChild(document.createTextNode(value));    } 

And here's the similar code from XMLBuilder:

 public class XMLBuilder extends AbstractBuilder...   private TagNode rootNode;   private TagNode currentNode;   public void addChild(String childTagName) {      addTo(currentNode, childTagName);   }   public void addSibling(String siblingTagName) {      addTo(currentNode.getParent(), siblingTagName);   }   private void addTo(TagNode parentNode, String tagName) {      currentNode = new TagNode(tagName);      parentNode.add(currentNode);   }   public void addAttribute(String name, String value) {      currentNode.addAttribute(name, value);   }   public void addValue(String value) {      currentNode.addValue(value);   } 

These methods, and numerous others that I'm not showing in order to conserve space, are nearly the same in DOMBuilder and XMLBuilder, except for the fact that each builder works with either TagNode or Element. The goal of this refactoring is to create a common interface for TagNode and Element so that the duplication in the builder methods can be eliminated.

1. My first task is to create a common interface. I base this interface on the TagNode class because its interface is the one I prefer for client code. TagNode has about ten methods, five of which are public. The common interface needs only three of these methods. I apply Extract Interface [F] to obtain the desired result:

  public interface XMLNode {     public abstract void add(XMLNode childNode);     public abstract void addAttribute(String attribute, String value);     public abstract void addValue(String value);  } public class TagNode  implements XMLNode...    public void add( XMLNode childNode) {       children().add(childNode);    }    // etc. 

I compile and test to make sure these changes worked.

2. Now I begin working on the DOMBuilder class. I want to apply Extract Class [F] to DOMBuilder in order to produce an adapter for Element. This results in the creation of the following class:

  public class ElementAdapter {     Element element;     public ElementAdapter(Element element) {        this.element = element;     }     public Element getElement() {        return element;     }  } 

3. Now I update all of the Element fields in DOMBuilder to be of type ElementAdapter and update any code that needs to be updated because of this change:

 public class DOMBuilder extends AbstractBuilder...    private Document document;    private  ElementAdapter rootNode;    private  ElementAdapter parentNode;    private  ElementAdapter currentNode;    public void addAttribute(String name, String value) {       currentNode. getElement().setAttribute(name, value);    }    public void addChild(String childTagName) {        ElementAdapter childNode =           new ElementAdapter(document.createElement(childTagName) );       currentNode. getElement().appendChild(childNode .getElement());       parentNode = currentNode;       currentNode = childNode;       history.push(currentNode);    }    public void addSibling(String siblingTagName) {       if (currentNode == root)          throw new RuntimeException(CANNOT_ADD_BESIDE_ROOT);        ElementAdapter siblingNode =           new ElementAdapter(document.createElement(siblingTagName) );       parentNode. getElement().appendChild(siblingNode .getElement());       currentNode = siblingNode;       history.pop();       history.push(currentNode);    } 

4. Now I create an adaptee invocation method for each adaptee method called by DOMBuilder. I use Extract Method [F] for this purpose, making sure that each extracted method takes an adaptee as an argument and uses that adaptee in its body:

 public class DOMBuilder extends AbstractBuilder...    public void addAttribute(String name, String value) {        addAttribute(currentNode, name, value);    }     private void addAttribute(ElementAdapter current, String name, String value) {        currentNode.getElement().setAttribute(name, value);     }    public void addChild(String childTagName) {       ElementAdapter childNode =          new ElementAdapter(document.createElement(childTagName));        add(currentNode, childNode);       parentNode = currentNode;       currentNode = childNode;       history.push(currentNode);    }     private void add(ElementAdapter parent, ElementAdapter child) {        parent.getElement().appendChild(child.getElement());     }    public void addSibling(String siblingTagName) {       if (currentNode == root)          throw new RuntimeException(CANNOT_ADD_BESIDE_ROOT);       ElementAdapter siblingNode =          new ElementAdapter(document.createElement(siblingTagName));        add(parentNode, siblingNode);       currentNode = siblingNode;       history.pop();       history.push(currentNode);    }    public void addValue(String value) {        addValue(currentNode, value);    }     private void addValue(ElementAdapter current, String value) {        currentNode.getElement().appendChild(document.createTextNode(value));     } 

5. I can now move each adaptee invocation method to ElementAdapter using Move Method [F]. I'd like the moved method to resemble the corresponding methods in the common interface, XMLNode, as much as possible. This is easy to do for every method except addValue(…), which I'll address in a moment. Here are the results after moving the addAttribute(…) and add(…) methods:

 public class ElementAdapter {    Element element;    public ElementAdapter(Element element) {       this.element = element;    }    public Element getElement() {       return element;    }     public void addAttribute(String name, String value) {        getElement().setAttribute(name, value);     }     public void add(ElementAdapter child) {        getElement().appendChild(child.getElement());     } } 

And here are examples of changes in DOMBuilder as a result of the move:

 public class DOMBuilder extends AbstractBuilder...    public void addAttribute(String name, String value) {        currentNode.addAttribute(name, value);    }    public void addChild(String childTagName) {       ElementAdapter childNode =          new ElementAdapter(document.createElement(childTagName));        currentNode.add(childNode);       parentNode = currentNode;       currentNode = childNode;       history.push(currentNode);    }    // etc. 

The addValue(…) method is more tricky to move to ElementAdapter because it relies on a field within ElementAdapter called document:

 public class DOMBuilder extends AbstractBuilder...     private Document document;    public void addValue(ElementAdapter current, String value) {       current.getElement().appendChild( document.createTextNode(value));    } 

I don't want to pass a field of type Document to the addValue(…) method on ElementAdapter because if I do so, that method will move further away from the target, which is the addValue(…) method on XMLNode:

 public interface XMLNode...    public abstract void addValue(String value); 

At this point I decide to pass an instance of Document to ElementAdapter via its constructor:

 public class ElementAdapter...    Element element;     Document document;    public ElementAdapter(Element element,  Document document) {       this.element = element;        this.document = document;    } 

And I make the necessary changes in DOMBuilder to call this updated constructor. Now I can easily move addValue(…):

 public class ElementAdapter...     public void addValue(String value) {        getElement().appendChild(document.createTextNode(value));     } 

6. Now I make ElementAdapter implement the XMLNode interface. This step is straightforward, except for a small change to the add(…) method to allow it to call the getElement() method, which is not part of the XMLNode interface:

 public class ElementAdapter  implements XMLNode...    public void add(XMLNode child) {        ElementAdapter childElement = (ElementAdapter)child;       getElement().appendChild( childElement.getElement());    } 

7. The final step is to update DOMBuilder so that all of its ElementAdapter fields, local variables, and parameters change their type to XMLNode:

 public class DOMBuilder extends AbstractBuilder...    private Document document;    private  XMLNode rootNode;    private  XMLNode parentNode;    private  XMLNode currentNode;    public void addChild(String childTagName) {        XMLNode childNode =          new ElementAdapter(document.createElement(childTagName), document);       ...    }    protected void init(String rootName) {       document = new DocumentImpl();       rootNode = new ElementAdapter(document.createElement(rootName), document);       document.appendChild( ((ElementAdapter)rootNode).getElement());       ...    } 

At this point, by adapting Element in DOMBuilder, the code in XMLBuilder is so similar to that of DOMBuilder that it makes sense to pull up the similar code to AbstractBuilder. I achieve that by applying Form Template Method (205) and Introduce Polymorphic Creation with Factory Method (88). The following diagram shows the result.



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