More about the problem
First, we'll simulate the behavior of the old system. Suppose it generates excuses for being late to work or school. To do this, it has anExcuse
interface that has generateExcuse()
, likeExcuse()
and dislikeExcuse()
methods.
public interface Excuse {
String generateExcuse();
void likeExcuse(String excuse);
void dislikeExcuse(String excuse);
}
The WorkExcuse
class implements this interface:
public class WorkExcuse implements Excuse {
private String[] excuses = {"in an incredible confluence of circumstances, I ran out of hot water and had to wait until sunlight, focused using a magnifying glass, heated a mug of water so that I could wash.",
"the artificial intelligence in my alarm clock failed me, waking me up an hour earlier than normal. Because it is winter, I thought it was still nighttime and I fell back asleep. Everything after that is a bit hazy.",
"my pre-holiday mood slows metabolic processes in my body, leading to depression and insomnia."};
private String [] apologies = {"This will not happen again, of course. I'm very sorry.", "I apologize for my unprofessional behavior.", "There is no excuse for my actions. I am not worthy of this position."};
@Override
public String generateExcuse() { // Randomly select an excuse from the array
String result = "I was late today because " + excuses[(int) Math.round(Math.random() + 1)] + "\\n" +
apologies[(int) Math.round(Math.random() + 1)];
return result;
}
@Override
public void likeExcuse(String excuse) {
// Duplicate the element in the array so that its chances of being chosen are higher
}
@Override
public void dislikeExcuse(String excuse) {
// Remove the item from the array
}
}
Let's test our example:
Excuse excuse = new WorkExcuse();
System.out.println(excuse.generateExcuse());
Output:
"I was late today because my pre-holiday mood slows metabolic processes in my body, leading to depression and insomnia.
I apologize for my unprofessional behavior.
Now imagine that you've launched an excuse-generating service, collected statistics, and noticed that most of your users are university students. To better serve this group, you asked another developer to create a system that generates excuses specifically for university students.
The development team conducted market research, ranked excuses, hooked up some artificial intelligence, and integrated the service with traffic reports, weather reports, and so on. Now you have a library for generating excuses for university students, but it has a different interface: StudentExcuse
.
public interface StudentExcuse {
String generateExcuse();
void dislikeExcuse(String excuse);
}
This interface has two methods: generateExcuse
, which generates an excuse, and dislikeExcuse
, which prevents the excuse from appearing again in the future.
The third-party library cannot be edited, i.e. you cannot change its source code.
What we have now is a system with two classes that implement the Excuse
interface, and a library with a SuperStudentExcuse
class that implements the StudentExcuse
interface:
public class SuperStudentExcuse implements StudentExcuse {
@Override
public String generateExcuse() {
// Logic for the new functionality
return "An incredible excuse adapted to the current weather conditions, traffic jams, or delays in public transport schedules.";
}
@Override
public void dislikeExcuse(String excuse) {
// Adds the reason to a blacklist
}
}
The code cannot be changed. The current class hierarchy looks like this:
This version of the system only works with the Excuse interface. You cannot rewrite the code: in a large application, making such changes could become a lengthy process or break the application's logic.
We could introduce a base interface and expand the hierarchy:
To do this, we have to rename the Excuse
interface. But the extra hierarchy is undesirable in serious applications: introducing a common root element breaks the architecture.
You should implement an intermediate class that will let us use both the new and old functionality with minimal losses. In short, you need an adapter.
The principle behind the adapter pattern
An adapter is an intermediate object that allows the method calls of one object to be understood by another. Let's implement an adapter for our example and call itMiddleware
.
Our adapter must implement an interface that is compatible with one of the objects. Let it be Excuse
. This allows Middleware
to call the first object's methods.
Middleware
receives calls and forwards them in a compatible way to the second object. Here's is the Middleware
implementation with the generateExcuse
and dislikeExcuse
methods:
public class Middleware implements Excuse { // 1. Middleware becomes compatible with WorkExcuse objects via the Excuse interface
private StudentExcuse superStudentExcuse;
public Middleware(StudentExcuse excuse) { // 2. Get a reference to the object being adapted
this.superStudentExcuse = excuse;
}
@Override
public String generateExcuse() {
return superStudentExcuse.generateExcuse(); // 3. The adapter implements an interface method
}
@Override
public void dislikeExcuse(String excuse) {
// The method first adds the excuse to the blacklist,
// Then passes it to the dislikeExcuse method of the superStudentExcuse object.
}
// The likeExcuse method will appear later
}
Testing (in client code):
public class Test {
public static void main(String[] args) {
Excuse excuse = new WorkExcuse(); // We create objects of the classes
StudentExcuse newExcuse = new SuperStudentExcuse(); // that must be compatible.
System.out.println("An ordinary excuse for an employee:");
System.out.println(excuse.generateExcuse());
System.out.println("\n");
Excuse adaptedStudentExcuse = new Middleware(newExcuse); // Wrap the new functionality in the adapter object
System.out.println("Using new functionality with the adapter:");
System.out.println(adaptedStudentExcuse.generateExcuse()); // The adapter calls the adapted method
}
}
Output:
An ordinary excuse for an employee:
I was late today because my pre-holiday mood slows metabolic processes in my body, leading to depression and insomnia.
There is no excuse for my actions. I am not worthy of this position.
Using new functionality with the adapter:
An incredible excuse adapted to the current weather conditions, traffic jams, or delays in public transport schedules.
The generateExcuse
method simply passes the call to another object, without any additional changes. The dislikeExcuse
method required us to first blacklist the excuse. The ability to perform intermediate data processing is a reason why people love the adapter pattern. But what about the likeExcuse
method, which is part of the Excuse
interface but not part of the StudentExcuse
interface? The new functionality does not support this operation.
The UnsupportedOperationException
was invented for this situation. It is thrown if the requested operation is not supported. Let's use it.
This is how the Middleware
class's new implementation looks:
public class Middleware implements Excuse {
private StudentExcuse superStudentExcuse;
public Middleware(StudentExcuse excuse) {
this.superStudentExcuse = excuse;
}
@Override
public String generateExcuse() {
return superStudentExcuse.generateExcuse();
}
@Override
public void likeExcuse(String excuse) {
throw new UnsupportedOperationException("The likeExcuse method is not supported by the new functionality");
}
@Override
public void dislikeExcuse(String excuse) {
// The method accesses a database to fetch additional information,
// and then passes it to the superStudentExcuse object's dislikeExcuse method.
}
}
At first glance, this solution doesn't seem very good, but imitating the functionality can complicate the situation. If the client pays attention, and the adapter is well documented, such a solution is acceptable.
When to use an adapter
When you need to use a third-party class, but its interface is incompatible with the main application. The example above shows how to create an adapter object that wraps calls in a format that a target object can understand.
When several existing subclasses need some common functionality. Instead of creating additional subclasses (which will lead to duplication of code), it is better to use an adapter.
Advantages and disadvantages
Advantage: The adapter hides from the client the details of processing requests from one object to another. The client code doesn't think about formatting data or handling calls to the target method. It's too complicated, and programmers are lazy :) Disadvantage: The project's code base is complicated by additional classes. If you have a lot of incompatible interfaces, the number of additional classes can become unmanageable.Don't confuse an adapter with a facade or decorator
With only a superficial inspection, an adapter could be confused with the facade and decorator patterns. The difference between an adapter and a facade is that a facade introduces a new interface and wraps the whole subsystem. And a decorator, unlike an adapter, changes the object itself rather than the interface.Step-by-step algorithm
First, be sure you have a problem that this pattern can solve.
Define the client interface that will be used to indirectly interact with incompatible objects.
Make the adapter class inherit the interface defined in the previous step.
In the adapter class, create a field to store a reference to the adaptee object. This reference is passed to the constructor.
Implement all the client interface methods in the adapter. A method may:
Pass along calls without making any changes
Modify or supplement data, increase/decrease the number of calls to the target method, etc.
In extreme cases, if a particular method remains incompatible, throw an UnsupportedOperationException. Unsupported operations must be strictly documented.
If the application only uses the adapter class through the client interface (as in the example above), then the adapter can be painlessly expanded in the future.
GO TO FULL VERSION