Sunday, July 10, 2011

Adapter Pattern - Can I mix oil and water?

What is an adapter pattern?

It is a pattern from gang of four's design pattern book. It helps us ,write a wrapper, so that we can encapsulate the incompatible type as if it were always compatible.

Do we write adapter in a brand new system?

We don't do all things from scratch, so there may be times when we want to use some or all of old established code. So, we may write an adapter to use those codes too. It is also one form of code re-use. One thing now, must be clear to you is that adapter is not as fast as if it were compatible in the first place. Let us write a complete program to illustrate this pattern.

Case : I have a very old tape player, which I want to use as my cd player. Can I write a wrapper to operate it like a new cd player. Though I know there will be inconsistencies in the quality of playing and some functions might not work, but I want to do it any way.

Lets first define an interface that our modern cd player uses.


// interface

public interface CDPlayer {

    void insertCD(Album album);

    void nextSong();   

    int getPlayedTimeTillLastSong();

    int currentSongLength();   

}





Lets define Album and Song classes to hold our data


// Album.java

public final class Album {

   

    private final String title;

    private final Song[] songs;

           

    public Album(String title, Song[] songs) {       

        this.title = title;

        this.songs = songs;       

    }   

   

    public String getTitle() {

        return title;

    }   

   

   

    public Song[] getSongs() {

        return songs;

    }

    @Override

    public String toString() {

        return "Album [title=" + title + "]";

    }   

}

// Song.java

public final class Song {

   

    private String title;

    private int length;

    public Song(String title, int length) {

        this.title = title;

        this.length = length;

    }

    public String getTitle() {

        return title;

    }

    public int getLength() {

        return length;

    }

    @Override

    public String toString() {

        return "Song [title=" + title + ", length=" + length + "]";

    }   

}



Implementing class. It is my brand new sony player


public final class SonyPlayer implements CDPlayer {

    private Album album;

    private static int marker;

    // Insert first album.

    public SonyPlayer(Album album) {

        this.album = album;

        marker = 0;

    }

    @Override

    public void nextSong() {

        marker++;

        // Lets make our modern sony player fancy by not adding play method

        System.out.println("Playing : " + marker + ": "

                + album.getSongs()[marker]);

    }

    @Override

    public int getPlayedTimeTillLastSong() {

        int lengthTillLast = 0;

        // We want to return length till last marker

        for (int i = 0; i < marker; i++) {

            lengthTillLast += album.getSongs()[i].getLength();

        }

        return lengthTillLast;

    }

    @Override

    public int currentSongLength() {

        return album.getSongs()[marker].getLength();

    }

    /**

     * Change cd

     */

    @Override

    public void insertCD(Album album) {

        this.album = album;

        marker = 0;

    }

}

Lets test it
SonyPlayerTest.java

...

    @Test

    public void testGetPlayedTimeTillLastSong() {

        CDPlayer player = new SonyPlayer(new Album("Metora", new Song[] {

                new Song("In the end", 4), new Song("Sometimes", 5),

                new Song("Where do I belong", 7) }));

        player.nextSong();

        System.out.println("Last Played Time Till Last Song: "

                + player.getPlayedTimeTillLastSong());

        player.nextSong();

        System.out.println("Last Played Time Till Last Song: "

                + player.getPlayedTimeTillLastSong());

    }

Output
Playing : 1: Song [title=Sometimes, length=5]

Last Played Time Till Last Song: 4

Playing : 2: Song [title=Where do I belong, length=7]

Last Played Time Till Last Song: 9

Now, lets see how our old player looks like
// OldPlayer.java

/**

 * We have a very lonely old player. We don't have any command to insert cd. So

 * lets leave that

 *

 */

public class OldPlayer {

    private static int time = 0;

    public OldPlayer() {

    }

    /**

     * I will make it simple and assume player is smart enough to calculate its

     * time from start.

     *

     * @param time1

     */

    public void seek(int time1) {

        time = time1;

    }

   

    public int getTime() {

        return time;

    }

    public void play() {

        System.out.println("Playing song at time " + time);

    }

}

As we can see it is as incompatible as it can be. Now lets try to write a wrapper or adapter class to make it function like a new player.
// OldPlayerAdapter.java

public class OldPlayerAdapter implements CDPlayer {

    private OldPlayer oldPlayer;

    private Album album;

    public OldPlayerAdapter(OldPlayer oldPlayer) {

        this.oldPlayer = oldPlayer;

    }

    @Override

    public void insertCD(Album album) {

        this.album = album;

    }

    /**

     * It is what we use for playing in modern players

     */

    @Override

    public void nextSong() {

        // Don't get confused with method name. See below at method definition

        oldPlayer.seek(getPlayedTimeTillLastSong());

        oldPlayer.play();

    }

    /**

     * Using time printed on album cover too, we can calculate the song's

     * position. Here we will use a false method name. I.e we will calculate the

     * time of next song;

     */

    @Override

    public int getPlayedTimeTillLastSong() {

        int albumSize = 0;

        for (int i = 0; i < album.getSongs().length; i++) {

            albumSize += album.getSongs()[i].getLength();

            /**

             * We reached the position of next song. So lets get out of here.

             */

            if (albumSize > oldPlayer.getTime())

                break;

        }

        return albumSize;

    }

    @Override

    public int currentSongLength() {

        // It's of no use

        return 0;

    }

}

And at last lets test it
@Test

    public void testNextSong() {

        CDPlayer oldPlayerAdapter = new OldPlayerAdapter(new OldPlayer());

        oldPlayerAdapter.insertCD(new Album("Metora", new Song[] {

                new Song("In the end", 4), new Song("Sometimes", 5),

                new Song("Where do I belong", 7) }));

        oldPlayerAdapter.nextSong();

        oldPlayerAdapter.nextSong();

    }

Output
Playing song at time 4

Playing song at time 9


As you can see at both output. They are playing the same songs. But the old player displays only the time of play. Since it is an adapter you may find some glitches in the sound and the sound quality might be degraded too, but in such a small amount I could not give you anything fancier. The theme is simple, if something does not work as your new standard, framework, api make it work by using some useful old features(methods). If your old code does some or all of work what the new framework expects, but are incompatible in interface, write an adapter or wrapper to wrap the old code in new format.

No comments:

Post a Comment