User Professor Hans Noodles
Professor Hans Noodles
Level 41

Adapter design pattern

Published in the Java Developer group
6640 members
Hi! Today we will touch on an important new topic: design patterns. What are these patterns? Adapter design pattern - 1I think you must know the expression "don't reinvent the wheel". In programming, as in many other areas, there are a large number of common situations. As software development has evolved, ready-made solutions that work have been created for each of them. These solutions are called design patterns. By convention, a pattern is some solution formulated like this: "if you need to do X in your program, then this is the best way to do it". There are lots of patterns. The excellent book "Head First Design Patterns", which you should definitely become familiar with, is dedicated to them. Adapter design pattern - 2Put concisely, a pattern consists of a common problem and a corresponding solution that can be considered a kind of standard. In today's lesson, we will meet one of these patterns: Adapter. It's name says it all, and you've encountered adapters many times in real life. Some of the most common adapters are the card readers that many computers and laptops have. Adapter design pattern - 3Suppose we have some sort of memory card. So what's the problem? It doesn't know how to interact with the computer. They don't share a common interface. The computer has a USB port, but we can't insert the memory card in it. The card can't be plugged into the computer, so we can't save our photos, videos, and other data. A card reader is an adapter that solves this problem. After all, it has a USB cable! Unlike the card itself, the card reader can be plugged into the computer. They share a common interface with the computer: USB. Let's see how this looks in practice:

public interface USB { 

   void connectWithUsbCable(); 
}
This is our USB interface with just one method for connecting via USB.

public class MemoryCard { 

   public void insert() { 
       System.out.println("Memory card successfully inserted!"); 
   } 

   public void copyData() { 
       System.out.println("The data has been copied to the computer!"); 
   } 
}
This is our class representing the memory card. It already has the 2 methods we need, but here's the problem: It doesn't implement the USB interface. The card cannot be inserted into the USB port.

public class CardReader implements USB { 

   private MemoryCard memoryCard; 

   public CardReader(MemoryCard memoryCard) { 
       this.memoryCard = memoryCard; 
   } 

   @Override 
   public void connectWithUsbCable() { 
       this.memoryCard.insert(); 
       this.memoryCard.copyData(); 
   } 
}
And here's our adapter! What does the CardReader class do and what exactly makes it an adapter? It's all simple. The class being adapted (MemoryCard) becomes one of the adapter's fields. This makes sense. When we put a memory card inside a card reader in real life, it also becomes part of it. Unlike the memory card, the adapter shares an interface with the computer. It has a USB cable, i.e. it can be connected to other devices via USB. That's why our CardReader class implements the USB interface. But what exactly happens inside this method? Exactly what we need to happen! The adapter delegates the work to our memory card. Indeed, the adapter doesn't do anything itself. A card reader doesn't have any independent functionality. Its job is only to connect the computer and memory card in order to allow the card to do its work — copying files! Our adapter enables this by providing its own interface (the connectWithUsbCable() method) to meet the memory card's "needs". Let's create some client program that will simulate a person who wants to copy data from a memory card:

public class Main { 

   public static void main(String[] args) { 

       USB cardReader = new CardReader(new MemoryCard()); 
       cardReader.connectWithUsbCable(); 
   } 
}
So what did we get? Console output:

Memory card successfully inserted! 
The data has been copied to the computer!
Excellent. We achieved our objective! Here's a link to a video with information about the Adapter pattern:

Reader and Writer abstract classes

Now we'll return to our favorite activity: learning about a couple of new classes for working with input and output :) I wonder how many we've already learned about. Today we'll talk about the Reader and Writer classes. Why specifically those classes? Because they are related to our previous section about adapters. Let's examine them in more detail. We'll start with Reader. Reader is an abstract class, so we won't be able to create objects explicitly.  But you're actually already familiar with it! After all, you are well-acquainted with the BufferedReader and InputStreamReader classes, which are its descendants :)

public class BufferedReader extends Reader { 
… 
} 

public class InputStreamReader extends Reader { 
… 
}
The InputStreamReader class is a classic adapter. As you probably remember, we can pass an InputStream object to its constructor. To do this, we usually use the System.in variable:

public static void main(String[] args) { 

   InputStreamReader inputStreamReader = new InputStreamReader(System.in); 
}
But what does InputStreamReader do? Like every adapter, it converts one interface to another.  In this case, the InputStream interface to the Reader interface. Initially, we have the InputStream class. It works well, but you can only use it to read individual bytes. In addition, we have a Reader abstract class. It has some very useful functionality — it knows how to read characters! We certainly need this ability. But here we face the classic problem usually solved by adapters — incompatible interfaces. What does that mean? Let's take a look at the Oracle documentation. Here are the methods of the InputStream class. Adapter design pattern - 4A set of methods is precisely what an interface is. As you can see, this class has a read() method (a few variants, in fact), but it can only read bytes: either individual bytes, or several bytes using a buffer. But this option doesn't suit us — we want to read characters. We need the functionality that is already implemented in the Reader abstract class. We can also see this in the documentation. Adapter design pattern - 5However, the InputStream and Reader interfaces are incompatible! As you can see, every implementation of the read() method has different parameters and return values. And this is where we need InputStreamReader! It will act as an adapter between our classes. As in the example with the card reader, which we considered above, we put an instance of the class being adapted "inside" the adapter class, i.e. we pass one to its constructor. In the previous example, we put a MemoryCard object inside CardReader. Now we're passing an InputStream object to the InputStreamReader constructor! We use our familiar System.in variable as the InputStream:

public static void main(String[] args) { 

   InputStreamReader inputStreamReader = new InputStreamReader(System.in); 
}
And indeed, looking at the documentation for InputStreamReader, we can see that the adaptation succeeded :) Now we have methods to read characters at our disposal. Adapter design pattern - 6And although our System.in object (the stream bound to the keyboard) initially did not allow this, the language's creators solved this problem by implementing the adapter pattern. The Reader abstract class, like most I/O classes, has a twin brother — Writer. It has the same big advantage as Reader — it provides a convenient interface for working with characters. With output streams, the problem and its solution look the same as with input streams. There's an OutputStream class that can only write bytes, there's a Writer abstract class that knows how to work with characters, and there are two incompatible interfaces. This problem is once again solved by the adapter pattern. We use the OutputStreamWriter class to easily adapt the two interfaces of the Writer and OutputStream classes to each other. After passing an OutputStream byte stream to the constructor, we can use an OutputStreamWriter to write characters rather than bytes!

import java.io.*; 

public class Main { 

   public static void main(String[] args) throws IOException { 

       OutputStreamWriter streamWriter = new OutputStreamWriter(new FileOutputStream("C:\\Users\\Username\\Desktop\\test.txt")); 
       streamWriter.write(32144); 
       streamWriter.close();
   } 
}
We wrote the character with code 32144 (綐) to our file, eliminating the need to work with bytes :) That's it for today. See you in the next lessons! :)
Comments (3)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
Andrei Level  20
5 February 2021
Very nice article, a bit too late though, as usual, 😂
Andrei Level  20
5 February 2021
Why can't we implement the USB interface in the Memory Card class?

public interface USB {

   void connectWithUsbCable();
}

public class MemoryCard {

   public void insert() {
       System.out.println("Memory card successfully inserted!");
   }

   public void copyData() {
       System.out.println("The data has been copied to the computer!");
   }
}
Henrique Level  41, São Paulo, Brazil
1 July 2020
Memory card adapter is a very good example.