Hi! Today we will touch on an important new topic: design patterns. What are these patterns?
I 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.
Put 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.
Suppose 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 theReader
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.
A 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.
However, 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.
And 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! :)
GO TO FULL VERSION