Extract Adapter

Prev don't be afraid of buying books Next

One class adapts multiple versions of a component, library, API, or other entity.



Extract an Adapter for a single version of the component, library, API, or other entity.





Motivation

While software must often support multiple versions of a component, library, or API, code that handles these versions doesn't have to be a confusing mess. Yet I routinely encounter code that attempts to handle multiple versions of something by overloading classes with version-specific state variables, constructors, and methods. Accompanying such code are comments like "This is for version X—please delete this code when we move to version Y!" Sure, like that's ever going to happen. Most programmers won't delete the version X code for fear that something they don't know about still relies on it. So the comments don't get deleted, and many versions supported by the code remain in the code.

Now consider an alternative: for each version of something you need to support, create a separate class. The class name could even include the version number of what it supports, to be really explicit about what it does. Such classes are called Adapters [DP]. Adapters implement a common interface and are responsible for functioning correctly with one (and usually only one) version of some code. Adapters make it easy for client code to swap in support for one library or API version or another. And programmers routinely rely on runtime information to configure their programs with the correct Adapter.

I refactor to Adapters fairly often. I like Adapters because they let me decide how I want to communicate with other people's code. In a fast-changing world, Adapters help me stay insulated from highly useful but rapidly changing APIs, such as those springing eternally from the open source world.

In some cases, Adapters may adapt too much. For example, a client needs access to behavior on an adaptee, yet it cannot access that behavior because it only has access to the adaptee via an Adapter. In that case, the Adapter must be redesigned to accommodate client needs.

Systems that depend on multiple versions of a component, library, or API tend to have a good deal of version-dependent logic scattered throughout the code (a sure sign of the Solution Sprawl smell, 43). While you wouldn't want to complicate a design by refactoring to Adapter too early, it's useful to apply this refactoring as soon as you find complexity, propagating conditionality, or a maintanance issue resulting from code written to handle multiple versions.

Adapter and Facade

The Adapter pattern is often confused with the Facade pattern [DP]. Both patterns make code easier to use, yet each operates on different levels: Adapters adapt objects, while Facades adapt entire subsystems.

Facades are often used to communicate with legacy systems. For example, consider an organization with a sophisticated, two-million-line COBOL system that continually generates a good deal of the organization's income. Such a system may be hard to extend or maintain because it was never refactored. Yet because it contains important functionality, new systems must depend on it.

Facades are useful in this context. They provide new systems with simpler views of poorly designed or highly complex legacy code. These new systems can communicate with Facade objects, which in turn do all the hard work of communicating with the legacy code.

Over time, teams can rewrite entire legacy subsystems by simply writing new implementations for each Facade. The process goes like this:

  • Identify a subsystem of your legacy system.

  • Write Facades for that subsystem.

  • Write new client programs that rely on calls to the Facades.

  • Create versions of each Facade using newer technologies.

  • Test that the newer and older Facades function identically.

  • Update client code to use the new Facades.

  • Repeat for the next subsystem.




Benefits and Liabilities

+

Isolates differences in versions of a component, library, or API.

+

Makes classes responsible for adapting only one version of something.

+

Provides insulation from frequently changing code.

Can shield a client from important behavior that isn't available on the Adapter.







Mechanics

There are different ways to go about this refactoring, depending on how your code looks before you begin. For example, if you have a class that uses a lot of conditional logic to handle multiple versions of something, it's likely that you can create Adapters for each version by repeatedly applying Replace Conditional with Polymorphism [F]. If you have a case like the one shown in the code sketch—in which an existing Adapter class supports multiple versions of a library with version-specific variables and methods—you'll extract multiple Adapters using a different approach. Here I outline the mechanics for this latter scenario.

1. Identify an overburdened adapter, a class that adapts too many versions of something.

2. Create a new adaper, a class produced by applying Extract Subclass [F] or Extract Class [F] for a single version of the multiple versions supported by the overburdened adapter. Copy or move all instance variables and methods used exclusively for that version into the new adapter.

To do this, you may need to make some private members of the overburdened adapter public or protected. It may also be necessary to initialize some instance variables via a constructor in the new adapter, which will necessitate updates to callers of the new constructor.

  • Compile and test.

3. Repeat step 2 until the overburdened adapter has no more version-specific code.

4. Remove any duplication found in the new adapters by applying refactorings like Pull Up Method [F] and Form Template Method (205).

  • Compile and test.

Example

The code I'll refactor in this example, which was depicted in the code sketch at the beginning of this refactoring, is based on real-world code that handles queries to a database using a third-party library. To protect the innocent, I've renamed that library SD, which stands for SuperDatabase.

1. I begin by identifying an Adapter that is overburdened with support for multiple versions of SuperDatabase. This class, called Query, provides support for SuperDatabase versions 5.1 and 5.2.

In the following code listing, notice the version-specific instance variables, duplicate login() methods, and conditional code in doQuery():

 public class Query...    private SDLogin sdLogin; // needed for SD version 5.1    private SDSession sdSession; // needed for SD version 5.1    private SDLoginSession sdLoginSession; // needed for SD version 5.2    private boolean sd52; // tells if we're running under SD 5.2    private SDQuery sdQuery; // this is needed for SD versions 5.1 & 5.2    // this is a login for SD 5.1    // NOTE: remove this when we convert all aplications to 5.2    public void login(String server, String user, String password) throws QueryException {       sd52 = false;       try {          sdSession = sdLogin.loginSession(server, user, password);       } catch (SDLoginFailedException lfe) {          throw new QueryException(QueryException.LOGIN_FAILED,                                   "Login failure\n" + lfe, lfe);       } catch (SDSocketInitFailedException ife) {          throw new QueryException(QueryException.LOGIN_FAILED,                                   "Socket fail\n" + ife, ife);       }    }    // 5.2 login    public void login(String server, String user, String password,                      String sdConfigFileName) throws QueryException {       sd52 = true;       sdLoginSession = new SDLoginSession(sdConfigFileName, false);       try {          sdLoginSession.loginSession(server, user, password);       } catch (SDLoginFailedException lfe) {          throw new QueryException(QueryException.LOGIN_FAILED,                                   "Login failure\n" + lfe, lfe);       } catch (SDSocketInitFailedException ife) {          throw new QueryException(QueryException.LOGIN_FAILED,                                   "Socket fail\n" + ife, ife);       } catch (SDNotFoundException nfe) {          throw new QueryException(QueryException.LOGIN_FAILED,                                   "Not found exception\n" + nfe, nfe);       }    }    public void doQuery() throws QueryException {       if (sdQuery != null)          sdQuery.clearResultSet();       if (sd52)          sdQuery = sdLoginSession.createQuery(SDQuery.OPEN_FOR_QUERY);       else          sdQuery = sdSession.createQuery(SDQuery.OPEN_FOR_QUERY);       executeQuery();    } 

2. Because Query doesn't already have subclasses, I decide to apply Extract Subclass [F] to isolate code that handles SuperDatabase 5.1 queries. My first step is to define the subclass and create a constructor for it:

  class QuerySD51 extends Query {     public QuerySD51() {        super();     }  } 

Next, I find all client calls to Query's constructor and, where appropriate, change the code to call the QuerySD51 constructor. For example, I find the following client code, which holds onto a Query field called query:

 public void loginToDatabase(String db, String user, String password)...    query = new Query();    try   {       if (usingSDVersion52()) {          query.login(db, user, password, getSD52ConfigFileName());  // Login to SD 5.2       } else {          query.login(db, user, password); // Login to SD 5.1       }       ...    } catch(QueryException qe)... 

I change this to:

 public void loginToDatabase(String db, String user, String password)...      query = new Query();    try   {       if (usingSDVersion52()) {           query = new Query();          query.login(db, user, password, getSD52ConfigFileName()); // Login to SD 5.2       } else {           query = new QuerySD51();          query.login(db, user, password);  // Login to SD 5.1       }       ...    } catch(QueryException qe) { 

Next, I apply Push Down Method [F] and Push Down Field [F] to outfit QuerySD51 with the methods and instance variables it needs. During this step, I have to be careful to consider the clients that make calls to public Query methods, for if I move a public method like login() from Query to QuerySD51, the caller will not be able to call the public method unless its type is changed to QuerySD51. Because I don't want to make such changes to client code, I proceed cautiously, sometimes copying and modifying public methods instead of completely removing them from Query. While I do this, I generate duplicate code, but that doesn't bother me now—I'll get rid of the duplication in the final step of this refactoring.

 class Query...      private SDLogin sdLogin;      private SDSession sdSession;     protected SDQuery sdQuery;    // this is a login for SD 5.1     public void login(String server, String user, String password) throws QueryException {        // I make this a do-nothing method     }    public void doQuery() throws QueryException {       if (sdQuery != null)          sdQuery.clearResultSet();         if (sd52)       sdQuery = sdLoginSession.createQuery(SDQuery.OPEN_FOR_QUERY);         else            sdQuery = sdSession.createQuery(SDQuery.OPEN_FOR_QUERY);       executeQuery();    } class QuerySD51 {     private SDLogin sdLogin;     private SDSession sdSession;     public void login(String server, String user, String password) throws QueryException {         sd52 = false;        try {           sdSession = sdLogin.loginSession(server, user, password);        } catch (SDLoginFailedException lfe) {           throw new QueryException(QueryException.LOGIN_FAILED,                                    "Login failure\n" + lfe, lfe);        } catch (SDSocketInitFailedException ife) {           throw new QueryException(QueryException.LOGIN_FAILED,                                    "Socket fail\n" + ife, ife);        }     }    public void doQuery() throws QueryException {       if (sdQuery != null)          sdQuery.clearResultSet();         if (sd52)            sdQuery = sdLoginSession.createQuery(SDQuery.OPEN_FOR_QUERY);         else        sdQuery = sdSession.createQuery(SDQuery.OPEN_FOR_QUERY);       executeQuery();    } } 

I compile and test that QuerySD51 works. No problems.

3. Next, I repeat step 2 to create QuerySD52. Along the way, I can make the Query class abstract, along with the doQuery() method. Here's what I have now:

Query is now free of version-specific code, but it is not free of duplicate code.

4. Now I go on a mission to remove duplication. I quickly find some in the two implementations of doQuery():

 abstract class Query...    public abstract void doQuery() throws QueryException; class QuerySD51...    public void doQuery() throws QueryException {        if (sdQuery != null)           sdQuery.clearResultSet();        sdQuery = sdSession.createQuery(SDQuery.OPEN_FOR_QUERY);        executeQuery();    } class QuerySD52...    public void doQuery() throws QueryException {        if (sdQuery != null)           sdQuery.clearResultSet();        sdQuery = sdLoginSession.createQuery(SDQuery.OPEN_FOR_QUERY);        executeQuery();    } 

Each of these methods simply initializes the sdQuery instance in a different way. This means that I can apply Introduce Polymorphic Creation with Factory Method (88) and Form Template Method (205) to create a single superclass version of doQuery():

 public abstract class Query...     protected abstract SDQuery createQuery();         // a Factory Method [DP]     public void doQuery() throws QueryException {     // a Template Method [DP]        if (sdQuery != null)           sdQuery.clearResultSet();        sdQuery = createQuery();                    // call to the Factory Method        executeQuery();     } class QuerySD51...     protected SDQuery createQuery() {        return sdSession.createQuery(SDQuery.OPEN_FOR_QUERY);     } class QuerySD52...     protected SDQuery createQuery() {        return sdLoginSession.createQuery(SDQuery.OPEN_FOR_QUERY);     } 

After compiling and testing the changes, I now face a more obvious duplication problem: Query still contains the SD 5.1 and 5.2 login() methods, even though they no longer do anything (the real login work is now handled by the subclasses). The signatures for these two login() method are identical, except for one parameter:

 // SD 5.1 login public void login(String server, String user, String password) throws QueryException... // SD 5.2 login public void login(String server, String user,                   String password,  String sdConfigFileName) throws QueryException... 

I decide to make the login() signatures the same, by simply supplying QuerySD52 with the sdConfigFileName information via its constructor:

 class QuerySD52...     private String sdConfigFileName;     public QuerySD52(String sdConfigFileName) {        super();        this.sdConfigFileName = sdConfigFileName;     } 

Now Query has only one abstract login() method:

 abstract class Query...    public abstract void login(String server, String user,                               String password) throws QueryException... 

Client code is updated as follows:

 public void loginToDatabase(String db, String user, String password)...     if (usingSDVersion52())        query = new QuerySD52(getSD52ConfigFileName());     else        query = new QuerySD51();    try   {        query.login(db, user, password);       ...    } catch(QueryException qe)... 

I'm nearly done. Because Query is an abstract class, I decide to rename it AbstractQuery, which communicates more about its nature. But making that name change necessitates changing client code to declare variables of type AbstractQuery instead of Query. I don't want to do that, so I apply Extract Interface [F] on AbstractQuery to obtain a Query interface that AbstractQuery can implement:

  interface Query {     public void login(String server, String user, String password) throws QueryException;     public void doQuery() throws QueryException;  } abstract class  AbstractQuery  implements Query...      public abstract void login(String server, String user,                                 String password) throws QueryException… 

Now, subclasses of AbstractQuery implement login(), while AbstractQuery doesn't even need to declare the login() method because it is an abstract class.

I compile and test to see that everything works as planned. Each version of SuperDatabase is now fully adapted. The code is smaller and treats each version in a more uniform way, all of which makes it easier to:

  • See similarities and differences between the versions

  • Remove support for older, unused versions

  • Add support for newer versions

Variations

Adapting with Anonymous Inner Classes

The first version of Java (JDK 1.0) included an interface called Enumeration, which was used to iterate over collections like Vector and Hashtable collections. Over time, better collections classes were added to the JDK, along with a new interface called Iterator. To make it possible to interoperate with code written using the Enumeration interface, the JDK provided the following Creation Method, which used Java's anonymous inner class capability to adapt an Iterator with an Enumeration:

 public class Collections...    public static Enumeration enumeration(final Collection c) {       return new Enumeration() {          Iterator i = c.iterator();          public boolean hasMoreElements() {             return i.hasNext();          }          public Object nextElement() {             return i.next();          }       };    } 

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