Skip to main content

Design Pattern: Adapter Pattern

Free2015-03-07#Design_Pattern#适配器模式#Adapter Pattern

The Adapter Pattern is used to achieve interface conversion

no_mkd

Before we begin, let's think about a few questions:

  • If a new project can reuse a large amount of legacy code from an old project, would you start the new project from scratch or try to understand the module functions and interfaces of the old project?
  • If after understanding the legacy code, you find that several important module interfaces are different (because they may come from multiple old projects), making them impossible to reuse directly, would you give up using the legacy code?
  • If you decide not to give up (which should be the right choice, as the correctness of legacy code has been verified through practice), does that mean you can only modify the remaining n-1 interfaces, or even rewrite all n interfaces?
  • If not, is there any simpler method?

I. What is the Adapter Pattern?

First, we need to understand what an adapter is. You've probably heard of a laptop power adapter, right? It can convert 220V AC power to the 15V DC power that a laptop needs. Amazing, isn't it? A small power adapter solves the mismatch between household electricity and the type of power a laptop needs. Have you noticed something? Yes, we neither changed the household electricity (to make it 15V DC) nor changed the laptop (to make it accept 220V AC), but we did solve the problem.

Adapter Pattern—a design pattern used to achieve conversion between different interfaces

II. An Example

Suppose we have two encapsulated functional modules, but they require different parameters (although the parameters are essentially the same type of object)

For example, our Module A (text checking module) looks like this:

package AdapterPattern;

/**

  • @author ayqy

  • Text Checking Module (similar to "Spelling and Grammar Check" in MSOffice Word) */ public class TextCheckModule { FormatText text;

    public TextCheckModule(FormatText text){ this.text = text; }

    /*

    • Omitting many specific Check operations... */ }

Module A's entry point requires a FormatText type parameter, defined as follows:

package AdapterPattern;

/**

  • @author ayqy

  • Define Formatted Text */ public interface FormatText { String text = null;

    /**

    • @return Number of logical lines in the text */ public abstract int getLineNumber();

    /**

    • @param index Line number
    • @return Content of line index */ public abstract String getLine(int index);

    /*

    • Omitting other useful methods */ }

And Module B (text display module) looks like this:

package AdapterPattern;

/**

  • @author ayqy

  • Text Display Module */ public class TextPrintModule { DefaultText text;

    public TextPrintModule(DefaultText text){ this.text = text; }

    /*

    • Omitting many display-related operations
    • */ }

Module B's entry point requires DefaultText:

package AdapterPattern;

/**

  • @author ayqy

  • Define Default Text */ public interface DefaultText { String text = null;

    /**

    • @return Number of logical lines in the text */ public abstract int getLineCount();

    /**

    • @param index Line number
    • @return Content of line index */ public abstract String getLineContent(int index);

    /*

    • Omitting other useful methods */ }

Our new project requires implementing a word processing program (similar to MSOffice Word). We need to call Module A to implement text checking functionality, and call Module B to implement text display functionality. But the problem is, the interfaces of the two modules don't match, making it impossible for us to directly reuse the existing A and B modules.

At this point, we seem to have two choices:

  1. Modify FormatText (or DefaultText) to satisfy DefaultText (or FormatText), and also modify the internal implementation of A (or B)
  2. Define a third interface MyText, modify both A and B to use MyText as the parameter, to achieve interface unification

Of course, we might prefer the first option, as it requires relatively fewer modifications. However, even so, the workload is still significant. We need to open A's encapsulation, understand its internal implementation, and modify the method invocation details. Actually, we have a better choice—define an Adapter responsible for converting FormatText to DefaultText (or vice versa):

package AdapterPattern;

/**

  • @author ayqy

  • Define Default Text Adapter */ public class DefaultTextAdapter implements DefaultText{ FormatText formatText = null;//Source object

    /**

    • @param text The source text object to be converted */ public DefaultTextAdapter(FormatText formatText){ this.formatText = formatText; }

    @Override public int getLineCount() { int lineNumber;

     lineNumber = formatText.getLineNumber();
     /*
      * Add additional conversion processing here
      * */
     return lineNumber;
    

    }

    @Override public String getLineContent(int index) { String line;

     line = formatText.getLine(index);
     /*
      * Add additional conversion processing here
      * */
     return line;
    

    } }

Our approach is quite simple:

  1. Define an Adapter that implements the target interface
  2. Obtain and retain the source interface object
  3. Implement each method in the target interface (call the source interface object's methods in the method body and add additional processing to achieve conversion)

The adapter is ready, but how do we use it? Let's implement a Test class to test it:

package AdapterPattern;

/**

  • @author ayqy

  • Test Interface Adapter */ public class Test implements FormatText{

    public static void main(String[] args) { //Create source interface object FormatText text = new Test(); //Create text checking module object TextCheckModule tcm = new TextCheckModule(text); /Call tcm to implement text checking/

     //Create adapter object, perform conversion from source interface object to target interface object
     DefaultTextAdapter textAdapter = new DefaultTextAdapter(text);
     //Use Adapter to create text display module object
     TextPrintModule tpm = new TextPrintModule(textAdapter);
     /*Call tcm to implement text display*/
    

    }

    /Please ignore the shortcut parts below.../ @Override public int getLineNumber() { // TODO Auto-generated method stub return 0; }

    @Override public String getLine(int index) { // TODO Auto-generated method stub return null; }

}

(P.S. Forgive my laziness, who made FormatText an interface...)

Of course, Test won't produce any results, but being able to compile is enough to show that our conversion works correctly.

Actually, we've overlooked a very important issue: in the example, the methods of the source interface and target interface correspond to each other. In other words, the methods defined in the source interface have similar methods in the target interface. Of course, such situations are rare. Usually, there are mismatched methods (the source interface contains methods not defined in the target interface, or vice versa).

At this point, we have two choices:

  • Throw an exception, but should provide detailed explanation in comments or documentation, like this:
    throw new UnsupportedOperationException();//Source interface does not support this operation
    
  • Complete an empty implementation, for example, return false, 0, null, etc.

Which specific choice to make depends on the specific situation; each has its own benefits and cannot be generalized.

III. Another Adapter Implementation Approach

In the example, we adopted the approach of "holding the source interface object and implementing the target interface" to implement the adapter. Actually, there exists another approach—multiple inheritance (or implementing multiple interfaces).

If an Adapter class implements both Interface A and Interface B, then undoubtedly, the Adapter object belongs to both Type A and Type B (the principle of multiple inheritance is similar).

Although Java doesn't support multiple inheritance, we should think of this implementation approach in environments that support multiple inheritance, and then decide whether to use multiple inheritance to implement the Adapter based on the specific situation.

IV. Summary

When we have a two-prong plug and a three-prong socket in hand, we always tend to bend the plug prongs into a V-shape. Why not buy an adapter?

  • We don't need to damage the plug, nor do we need to damage the socket (sometimes code modification is indeed destructive; we avoid modification and thus avoid destruction)
  • More importantly: we can lend the adapter we bought to a friend (reusable)

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment