Back to blog navigation
Design intent
The Adapter Pattern acts as a bridge between two incompatible interfaces. This type of design pattern is structural in that it combines the functionality of two separate interfaces.
In some cases, customers expect to obtain a certain functional interface, but the existing interface cannot meet their needs. For example, the normal power supply voltage of the United States is 110V, and a Chinese person brings a Chinese-made electric appliance to the United States, which can only be charged and used under 220V voltage. In this case, the customer (Chinese) expects the interface to have a 220V voltage to charge the electric appliance, but the actual interface is only 110V voltage to charge the electric appliance. In this case, it is necessary to use a voltage converter (adapter) to make the 110V voltage can be converted into 220V voltage for the customer.
Converting the interface of one class into another that the customer wants is what an adapter does. The adapter pattern makes it possible for classes that would otherwise not work together due to interface incompatibations to work together.
Suitable conditions
- The system needs to use existing classes, and this class interface does not meet the needs of the system (core requirements).
- You want to create a reusable adapter class that works with classes that are not closely related to each other, including classes that may be introduced in the future that do not necessarily have a consistent interface, but that all have a consistent interface through the adapter.
- To insert a class into another class family by interface transformation. (For example, tigers and birds, now more than a flying tiger, without the need to increase the entity, add an adapter, containing a tiger object in the interface to achieve flying.)
design
The adapter pattern is typically implemented in two ways: a class adapter, which is not much used today, and an object adapter, which generally makes code more extensible and maintainable.
Regardless of the approach, the basic implementation idea is to extend the implementation class of an existing interface to implement the target interface that the customer expects.
A class adapter breaks encapsulation by inheriting an existing interface class and implementing the target interface, which exposes the existing interface class to the adapter and gives the adapter all the functionality of the existing interface class. In addition, it is also logically counterintuitive that the purpose of an adapter is to extend the functionality of an existing interface class rather than replace it, and that class adapters are only used under certain conditions.
The object adapter holds an instance of an existing interface class and extends its functionality to implement the target interface. This is the recommended approach, and the preference for composition over inheritance makes the code more maintainable. Also, it makes perfect sense — “Give me a wire and LET me lengthen it to 5m, I don’t need to know what the wire is made of because it’s my job to lengthen it to 5m” — we’re extending functionality without caring about implementation.
Class adapter structure diagram:
Object adapter structure diagram:
- Target: the expected functional interface (220V power supply).
- Cilent: the customer expects access to the Target interface (the customer expects 220V).
- Adaptee: existing interface, this interface needs to be adapted (existing 110V power supply, need to be adapted to 220V).
- Adapter: ADAPTS existing interfaces to meet customer requirements (ADAPTS 110V voltage to 220V voltage).
In Adapter mode, Cilent calls Adapter to get the functionality, and Adapter extends Adaptee to do the functionality.
Code sample
Class adapter:
// Customer expected interface -- 220V voltage charging
interface Target {
void chargeBy220V(a);
}
// Existing interface -- can only charge through 110V voltage
interface Adaptee {
void chargeBy110V(a);
}
// a concrete implementation of the existing interface class, American power supply -- 110V power supply
class americanCharger implements Adaptee {
@Override
public void chargeBy110V(a) {
System.out.println("American Power Supply, for you only, is charging you through 110V."); }}// Class adapter that extends an existing interface by inheriting it
class Adapter extends americanCharger implements Target {
@Override
public void chargeBy220V(a) {
super.chargeBy110V();// Existing functionality
System.out.println("Add 110V again, reach 220V, blunt duck!");// Extend existing functionality}}/ / test class
public class Test {
public static void main(String[] args) throws FileNotFoundException {
// Class adapters mess up code logic
// in this case it seems that Adapter is a 110V us power supply that can be used directly without additional information
// Can be compared with the object adapter
newAdapter().chargeBy220V(); }}/ / output
/* The United States electrical suppliers, only for your service, is through 110V voltage for you to charge 110V, reach 220V, chong duck! * /
Copy the code
Object adapter:
// Customer expected interface -- 220V voltage charging
interface Target {
void chargeBy220V(a);
}
// Existing interface -- can only charge through 110V voltage
interface Adaptee {
void chargeBy110V(a);
}
// a concrete implementation of the existing interface class, American power supply -- 110V power supply
class americanCharger implements Adaptee {
@Override
public void chargeBy110V(a) {
System.out.println("American Power Supply, for you only, is charging you through 110V."); }}// Class adaptor, which extends the existing interface by inheriting it, making it capable of 110V power supply
class Adapter implements Target {
Adaptee adaptee;// Holds a reference to an existing interface implementation object
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void chargeBy220V(a) {
adaptee.chargeBy110V();// Existing functionality of this object
System.out.println("Add 110V again, reach 220V, blunt duck!");// Extend existing functionality}}/ / test class
public class Test {
public static void main(String[] args) throws FileNotFoundException {
// now we have an American 110V power station, but we can't use it
Adaptee adaptee = new americanCharger();
// We pass the power supply to the adapter, which converts to a 220V power supply
Adapter adapter = new Adapter(adaptee);
// Then we charge it through the adapteradapter.chargeBy220V(); }}// same as above
Copy the code
Object adapters extend existing interfaces in a composite manner to meet customer expectations.
Let’s look at an example of a JavaIO flow:
FileInputStream fis = new FileInputStream("qe");
InputStreamReader isrAdapter = new InputStreamReader(fis);
BufferedReader bf = new BufferedReader(isrAdapter);
Copy the code
BufferedReader(in this case, the client) needs to read the file character stream to work. Reading the file character stream is part of the client’s requirement, but according to the existing interface, the only way to read the file is to read the byte stream. FileInputStream is a concrete implementation of the existing interface. To adapt an existing interface, InputStreamReader is an adapter that holds an instance of an existing interface class that reads a stream of file bytes and expands it to a character stream to meet customer requirements. This is the standard object adapter pattern. If you look closely at the source code, you see that the JavaIO library defines an adapter as abstract and inherits that abstract adapter from a concrete adapter, such as InputStreamReader here.
If there are more than one way to implement adaptation, we can declare Adapter as an abstract class and extend it by subclasses:
// Customer expected interface -- 220V voltage charging
interface Target {
void chargeBy220V(a);
}
// Existing interface -- can only charge through 110V voltage
interface Adaptee {
void chargeBy110V(a);
}
// a concrete implementation of the existing interface class, American power supply -- 110V power supply
class americanCharger implements Adaptee {
@Override
public void chargeBy110V(a) {
System.out.println("American Power Supply, for you only, is charging you through 110V."); }}// An abstract class adapter that extends an existing interface by inheriting it
abstract class Adapter implements Target {
Adaptee adaptee;// Holds a reference to an existing interface implementation object
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee; }}// China made its own
class ChinaMakeAdapter extends Adapter {
public ChinaMakeAdapter(Adaptee adaptee) {
super(adaptee);
}
@Override
public void chargeBy220V(a) {
adaptee.chargeBy110V();// Existing functionality of this object
System.out.println("Add 110V again, reach 220V, identify Made in China, blunt duck!");// Extend existing functionality}}/ / test class
public class Test {
public static void main(String[] args) throws FileNotFoundException {
// now we have an American 110V power station, but we can't use it
Adaptee adaptee = new americanCharger();
// We turn over the power station to a Chinese made adapter
Adapter adapter = new ChinaMakeAdapter(adaptee);
// Then we charge it through the adapteradapter.chargeBy220V(); }}// same as above
Copy the code
In addition, an adapter can achieve bidirectional adaptation by implementing two interfaces, that is, interface A can be adapted to interface B, and interface B can also be adapted to interface A. This situation is not common.
// Interface A -- 220V power supply
interface A {
void chargeBy220V(a);
}
// Interface A concrete implementation class, China power supply -- through 220V voltage power supply
class ChinaCharger implements A {
@Override
public void chargeBy220V(a) {
System.out.println("220V China charging, reliable"); }}// interface B -- 110V power supply
interface B {
void chargeBy110V(a);
}
// interface B concrete implementation class, USA power supply -- through 110V voltage supply
class AmericanCharger implements B {
@Override
public void chargeBy110V(a) {
System.out.println("The American Charger, for you only, is charging you at 110V."); }}// Two-way adapter
class Adapter implements A.B {
A a; / / 220 v charging
B b; / / 110 v charging
public Adapter(A a) {
this.a = a;
}
public Adapter(B b) {
this.b = b;
}
@Override
public void chargeBy220V(a) {
b.chargeBy110V(); // Current interface
System.out.println("Add code, add to 220V!!");// adapt the target interface
}
@Override
public void chargeBy110V(a) {
a.chargeBy220V();// Current interface
System.out.println("Buffer voltage is now 110 VOLTS."); }}/ / test class
public class Test {
public static void main(String[] args) throws FileNotFoundException {
// When we go to America, there is an American 110V charging station in the hotel, we need 220V voltage
B b = new AmericanCharger();
// We give the charging station to the adapter to get 220V charging
Adapter adapter1 = new Adapter(b);
// Then we charge it through the adapter
adapter1.chargeBy220V();
System.out.println();
// The American comes to China. There is a Chinese 220V charging station in the hotel, but he needs 110V
A a = new ChinaCharger();
// Pass the charging station to the adapter for 110V charging
Adapter adapter2 = new Adapter(a);
// Then we charge it through the adapteradapter2.chargeBy110V(); }}/ / output
/* American charger, only for your service, is charging through 110V voltage for you to increase the code, increase to 220V!! 220V Chinese charging voltage, trusted buffer voltage, is now 110V
Copy the code
Through the realization of two interfaces, to achieve different interface two-way adaptation, in some cases is very practical, such as TypeC – USB interface converter, both from TypeC to USB can also be from USB to TypeC.
Adapter Pattern Summary
Advantages:
- You can have any two unrelated classes run together.
- Improved class reuse and the ability to unify multiple different interfaces.
- Hiding existing interface implementation classes increases class transparency.
- High flexibility, can be freely adapted.
Disadvantages:
- Too much use of adapters can make the system very cluttered and difficult to grasp as a whole. For example, clearly see is called A interface, in fact, internal adaptation into the IMPLEMENTATION of THE B interface, A system if too much of this situation, is tantamount to A disaster. So if you don’t have to, you can skip the adapter and refactor your system directly.
- Some adaptations can be difficult, such as making a house fly.
When we are motivated to modify the interface of a functioning system, we should consider using the adapter pattern.
** Note: ** Adapters were not added during detailed design, but to address the problem of projects in service that the existing interface may not be changed (it is not possible to change the 110V supply to 220V supply in the US).