When do we need to go for Adapter pattern? If po开发者_如何学Gossible give me a real world example that suits that pattern.
I worked on a system which needed to interface with external DVRs. For the most part, all DVRs have the same basic functionality: start recording from a certain video source; stop recording; start playback from a certain time; stop playback, etc.
Every DVR manufacturer provided a software library, allowing us to write code to control their device (for sake of this discussion, I'll refer to it as the SDK). Even though every SDK provided APIs for all the basic functionality, none of them were quite the same. Here's a very rough example, but you get the idea:
- BeginPlayback(DateTime startTime);
- StartPlayback(long startTimeTicks);
- Playback(string startDate, string startTime);
Our software needed to be able to interact with all DVRs. So instead of writing horrible switch/cases for each different SDK, we created our own common IDVRController interface, and wrote all of our system code to that interface:
- Playback(DateTime startTime);
We then wrote a different adapter implementation for each SDK, all of which implemented our IDVRController interface. We used a config file to specify the type of DVR the system would connect to, and a Factory pattern to instantiate the correct implementer of IDVRController for that DVR.
In that way, the adapter pattern made our system code simpler: we always coded to IDVRController. And it allowed us to roll out adapters for new SDKs post-deployment (our Factory used reflection to instantiate the correct IDVRController instance).
Existing Interface
interface Shape {
public int calculateArea(int r);
}
Current Implementation for Shape interface
class Square implements Shape {
@Override
public int calculateArea(int r) {
return r * r;
}
}
Now Consider that you want Circle class to adapt to our existing interface which in no way we can modify (Written by third party).
class Circle {
public double calculateCircularArea (int r) {
return 3.14 * r * r;
}
}
Now we have adapt Circle implementation to our Shape interface. So we need an adaptor as they are incompatible.
class CircleAdaptor extends Circle implements Shape {
@Override
public int calculateArea(int r) {
return (int) calculateCircularArea(r);
}
}
CircleAdaptor - Is the Adaptor for Circle;
Circle - Is the Adaptee;
Shape - Is the Target Interface.
public class AdapterPattern {
public static void main(String[] args) {
Shape circle = new CirCleAdaptor();
System.out.println("Circle Area " + circle.calculateArea(5));
Shape square = new Square();
System.out.println("Square Area " + square.calculateArea(5));
}
}
Hope this gives a better idea about when to use it.
See also what is Decorator pattern?
In computer programming, the adapter pattern (often referred to as the wrapper pattern or simply a wrapper) is a design pattern that translates one interface for a class into a compatible interface. An adapter allows classes to work together that normally could not because of incompatible interfaces, by providing its interface to clients while using the original interface. The adapter translates calls to its interface into calls to the original interface, and the amount of code necessary to do this is typically small. The adapter is also responsible for transforming data into appropriate forms. For instance, if multiple boolean values are stored as a single integer but your consumer requires a 'true'/'false', the adapter would be responsible for extracting the appropriate values from the integer value.
Wikipedia!!!
You can use the Adapter design pattern when you have to deal with different interfaces with similar behavior (which usually means classes with similar behavior but with different methods). An example of it would be a class to connect to a Samsung TV and another one to connect to a Sony TV. They will share common behavior like open menu, start playback, connect to a network and etc but each library will have a different implementation of it (with different method names and signatures). These different vendor specific implementations are called Adaptee in the UML diagrams.
So, in your code (called Client in the UML diagrams), instead of hard code the method calls of each vendor (or Adaptee), you could then create a generic interface (called Target in UML diagrams) to wrap these similar behaviors and work with only one type of object.
The Adapters will then implement the Target interface delegating its method calls to the Adaptees that are passed to the Adapters via constructor.
For you to realize this in Java code, I wrote a very simple project using exactly the same example mentioned above using adapters to deal with multiple smart TV interfaces. The code is small, well documented and self explanatory so dig on it to see how a real world implementation would look like.
Just download the code and import it to Eclipse (or your favorite IDE) as a Maven project. You can execute the code by running org.example.Main.java. Remember that the important thing here is to understand how classes and interfaces are assembled together to design the pattern. I also created some fake Adaptees in the package com.thirdparty.libs. Hope it helps!
https://github.com/Dannemann/java-design-patterns
Adapter pattern is required in following scenario:
Say you have defined an interface I1
with method M1
and M2
C1
and C2
implements this interface I1
, now for C1
while implementing M1
and M2
you have found no help from other existing classes so you need to write all logic by yourself.
Now while implementing class C2
you have come across class C3
with methods M3
and M4
that can be used to implement M1
and M2
for C2
so to utilize those M3
and M4
in class C2
you extends class C3
and use M3
and M4
of C3
.
In this example C2
becomes Adapter class
and C3
becomes adaptee
package com.design.patterns;
public class AdapterExample {
public static void main(String[] args) {
Shape line = new LineShape();
line.draw();
Shape text = new TextShape();
text.draw();
}
}
//==Start from here
interface Shape{
public void draw();
}
class LineShape implements Shape{
@Override
public void draw() {
System.out.println("write some logic and draw line");
}
}
//Adapter
class TextShape extends TextView implements Shape{
@Override
public void draw() {
System.out.println("logic is already there in class TextView");
drawText();
}
}
// Adaptee
class TextView{
public void drawText() {
System.out.println("Drawing Text Shape");
}
}
Incompatible interfaces
EuroPlug
connector connects only to european electrical sockets:
interface EuroPlug {
fun plugIn()
}
class EuroSocket {
fun supplyCurrent(plug: EuroPlug) = plug.plugIn()
}
USPlug
connector connects only to US electrical sockets:
interface USPlug {
fun plugIn()
}
class USSocket {
fun supplyCurrent(plug: USPlug) = plug.plugIn()
}
Creating an adapter
When we have USSocket
and EuroPlug
, we create an adapter to convert the EuroPlug
to USPlug
:
class EuroToUSPlugAdapter(private val euroPlug: EuroPlug) : USPlug {
override fun plugIn() = euroPlug.plugIn()
}
Now EuroToUSPlugAdapter
adapts the interface USPlug
of the already existing class USSocket
without changing it.
Using the adapter
Here we have a USSocket
but with a EuroPlug
object. So, we pass the EuroPlug
object to the EuroToUSPlugAdapter
which does the work of converting a EuroPlug
to the USPlug
:
fun main() {
val usSocket = USSocket()
val euroPlug = object : EuroPlug {
override fun plugIn() {
println("Euro plug adapted for US Socket")
}
}
val euroAdapter = EuroToUSPlugAdapter(euroPlug)
usSocket.supplyCurrent(euroAdapter)
}
That's it! This is how the adapter pattern allows two incompatible interfaces to work together. Hope that helps.
A very common example of the adapter pattern is done through the Service Provider Interface and is commonly used in a lot of the Java EE framework.
The reason for it is to allow different implementations of Java EE but programmers simply code to the Java EE spec rather than something implementation specific.
As opposed to something like coding directly using WebSphere classes which lock you into using WebSphere.
Or worse (from my experience), Apache HTTP Client and find out later that because you coded to that implementation rather than the normal HttpUrlConnection you have to do a lot of recoding because it does not support the current version of TLS which would've been avoided if the original developer coded to a more stable API and we just need to upgrade Java runtime.
From: Alexey Soshin's Book “Kotlin Design Patterns and Best Practices Second Edition”:
The main goal of the Adapter design pattern is to convert one interface to another interface. In the physical world, the best example of this idea would be an electrical plug adapter or a USB adapter. Imagine yourself in a hotel room late in the evening, with 7% battery left on your phone. Your phone charger was left in the office at the other end of the city. You only have an EU plug charger with a Mini USB cable. But your phone uses USB-C, as you had to upgrade. You're in New York, so all of your outlets are (of course) USB-A. So, what do you do? Oh, it's easy. You look for a Mini USB to USB-C adapter in the middle of the night and hope that you have remembered to bring your EU to US plug adapter as well. Only 5% battery left – time is running out! So, now that we understand what adapters are for in the physical world, let's see how we can apply the same principle in code. Let's start with interfaces. USPlug assumes that power is Int. It has 1 as its value if it has power and any other value if it doesn't:
interface USPlug {
val hasPower: Int
}
EUPlug treats power as String, which is either TRUE or FALSE:
interface EUPlug {
val hasPower: String // "TRUE" or "FALSE"
}
For UsbMini, power is an enum:
interface UsbMini {
val hasPower: Power
}
enum class Power {
TRUE, FALSE
}
Finally, for UsbTypeC, power is a Boolean value:
interface UsbTypeC {
val hasPower: Boolean
}
Our goal is to bring the power value from a US power outlet to our cellphone, which will be represented by this function:
fun cellPhone(chargeCable: UsbTypeC) {
if (chargeCable.hasPower) {
println("I've Got The Power!")
} else {
println("No power")
}
}
Let's start by declaring what a US power outlet will look like in our code. It will be a function that returns a USPlug:
// Power outlet exposes USPlug interface
fun usPowerOutlet(): USPlug {
return object : USPlug {
override val hasPower = 1
}
}
Our charger will be a function that takes EUPlug as an input and outputs UsbMini:
// Charger accepts EUPlug interface and exposes UsbMini
// interface
fun charger(plug: EUPlug): UsbMini {
return object : UsbMini {
override val hasPower=Power.valueOf(plug.hasPower)
}
}
Next, let's try to combine our cellPhone, charger, and usPowerOutlet functions:
cellPhone(
// Type mismatch: inferred type is UsbMini but // UsbTypeC
was expected
charger(
// Type mismatch: inferred type is USPlug but //
EUPlug was expected
usPowerOutlet()
)
)
As you can see, we get two different type errors – the Adapter design pattern should help us solve these. Adapting existing code We need two types of adapters: one for our power plugs and another one for our USB ports. We could adapt the US plug to work with the EU plug by defining the following extension function:
fun USPlug.toEUPlug(): EUPlug {
val hasPower = if (this.hasPower == 1) "TRUE" else
"FALSE"
return object : EUPlug {
// Transfer power
override val hasPower = hasPower
}
}
We can create a USB adapter between the Mini USB and USB-C instances in a similar way:
fun UsbMini.toUsbTypeC(): UsbTypeC {
val hasPower = this.hasPower == Power.TRUE
return object : UsbTypeC {
override val hasPower = hasPower
}
}
Finally, we can get back online again by combining all those adapters together:
cellPhone(
charger(
usPowerOutlet().toEUPlug()
).toUsbTypeC()
)
The Adapter design pattern is more straightforward than the other design patterns, and you'll see it used widely. Now, let's discuss some of its real world uses in more detail.
Adapters in the real world
You've probably encountered many uses of the Adapter design pattern already. These are normally used to adapt between concepts and implementations. For example, let's take the concept of a JVM collection versus the concept of a JVM stream. A list is a collection of elements that can be created using the listOf() function:
val list = listOf("a", "b", "c")
A stream is a lazy collection of elements. You cannot simply pass a collection to a function that receives a stream, even though it may make sense:
fun printStream(stream: Stream<String>) {
stream.forEach(e -> println(e))
}
printStream(list) // Doesn't compile
Luckily, collections provide us with the .stream() adapter method:
printStream(list.stream()) // Adapted successfully
Many other Kotlin objects have adapter methods that usually start with to as a prefix. For example, toTypedArray() converts a list to an array.
Caveats of using adapters
Have you ever plugged a 110 V US appliance into a 220 V EU socket through an adapter, and fried it totally? If you're not careful, that's something that could also happen to your code. The following example uses another adapter, and it also compiles well:
val stream = Stream.generate { 42 }
stream.toList()
But it never completes because Stream.generate() produces an infinite list of integers. So, be careful and adopt this design pattern wisely.
I found the clearest and kind of real life example in Bharath Thippireddy design pattern course.
We have WeatherUI
class which is looking for temperature by zip-code and we have WeatherFinderImpl
class which knows temperature by city name so we need to create WeatherAdapter
class which will take zipcode, change it to city name and call WeatherFinder
class to return proper temperature.
WeatherUi
class:
public class WeatherUI {
public void showTemperature(int zipcode) {
//I just have zipcode
}
}
and WeatherFinder
interface:
public interface WeatherFinder {
int find(String city);
}
and WeatherFinderImpl
which can find temperature only by city name:
public class WeatherFinderImpl implements WeatherFinder{
@Override
public int find(String city) {
return 25;
}
}
having these three in WeatherUI
class we can create a method which will translate zipcode into city or we can create a new adapter class:
public class WeatherAdapter {
public int findTemperature(int zipcode){
String city = null;
//here most probably we would take it from db, in example hardcode is enough
if(zipcode == 63400){
city = "Ostrow Wielkopolski";
}
WeatherFinder finder = new WeatherFinderImpl();
int temperature = finder.find(city);
return temperature;
}
}
and call it in WeatherUI
:
public class WeatherUI {
public void showTemperature(int zipcode) {
WeatherAdapter adapter = new WeatherAdapter();
System.out.println(adapter.findTemperature(63400));
}
}
test for it is as simple as:
public static void main(String[] args) {
WeatherUI ui = new WeatherUI();
ui.showTemperature(63400);
}
精彩评论