33
loading...
This website collects cookies to deliver better user experience
Adapter
pattern transforms the class interface to another one requested by the client. Using the Adapter
allows incompatible classes to interact with each other. Another term for this pattern is Wrapper
.Adaptee
) to the expected interface (Target
) by the client class without adding another level of inheritance. Such inheritance would not always be possible if Target
was a class rather than an interface. The new class would have to be both Target
andAdaptee
at the same time.Adapter
class, you may also add a method in the client that takes an object with an Adaptee
interface, but it will cause the class to expand with a lot of similar methods. The client class can also come from 3rd party dependency, and then it will not be possible to change it.adapter
is essentially a single class, but it's important to understand its surroundings.Target
objectClient
classTarget
interface that adapts theAdaptee
object, most often the adapted object will be assigned to the field in this classClient
class Client(private val target: Target) {
private val argument = BigDecimal(10.0)
fun doWork() {
target.method(argument)
}
}
// the client will accept only this interface
interface Target {
fun method(argument: BigDecimal): Double
}
// and this is the interface we want to use with the client
interface Adaptee {
fun originalMethod(argument: CustomArgument): CustomResult
}
// the Adapter implementing Target interface and taking the Adaptee in the constructor
class Adapter(private val adaptee: Adaptee) : Target {
override fun method(argument: BigDecimal): Double {
val stringArgument = argument.toCustomArgument()
// calling method from adapted interface
return adaptee.originalMethod(stringArgument).toDouble()
}
}
// usage
fun main() {
val target1: Target = TargetImpl()
val adaptee: Adaptee = AdapteeImpl()
val adapter: Target = Adapter(adaptee)
val client1: Client = Client(target1)
val client2: Client = Client(adaptee) // error! wrong interface
val client3: Client = Client(adapter) // using the Adapter with Adaptee instance
}
Adapter
class uses the Adaptee
object within the Target
interface. The Adapter
encapsulates the logic of mapping one interface to another. This avoids unnecessary extending or modifying the client or Adaptee
only for a specific client.Adaptee
class. Thanks to this, there is no need to adapt Adaptee
to a specific client or several clients. Just look at this monster:// Adaptee implements all interfaces expected by all clients
class Adaptee: Target1, Target2, Target3{
// the only actual method of the Adaptee
fun originalMethod(argument: CustomArgument): CustomResult
// interface methods
override fun target1Method()
override fun target2Method()
override fun target3Method()
}
// unchanged Adaptee class
class Adaptee{
fun originalMethod(argument: CustomArgument): CustomResult
}
// Adapters can be put in packages closer to the Client than to the Adaptee
class Adapter1(val adaptee: Adaptee) : Target1{
override fun target1Method()
}
class Adapter2(val adaptee: Adaptee) : Target2{
override fun target2Method()
}
class Adapter3(val adaptee: Adaptee) : Target3{
override fun target3Method()
}
// lets assume that this class is comming from 3rd party library
// `data class` cannot be extended
data class Item(val name: String)
// and we have this interface in our system
interface PrettyPrintableItem {
fun prettyPrint(): String
}
// Adapter/Wrapper providing `PrettyPrintableItem` functionality for the `Item` object
class ItemAdapter(private val item: Item) : PrettyPrintableItem {
override fun prettyPrint(): String {
return "hello, my name is: ${item.name}"
}
}
// usage
fun main() {
// list of items returned from 3rd party lib, that we want to "pretty print"
val list: List<Item> = listOf(
Item("Adam"),
Item("Not Adam"),
Item("Adam Maybe"),
Item("Yes"),
)
list.forEach {
// adapting item
val adapted = ItemAdapter(it)
println(adapted.prettyPrint())
}
}
Adapter
allows a clean link between classes that we have no control over (from external libraries) with our code with different interfaces. It is a good practice not to use the 3rd party interfaces in the whole system, if possible, but only at the point where the library meets our code. That provides the exchangeability of libraries, easy version update, and protects your code from the forced changes dictated by API alterations in independent software pieces.strong typing
where we know for 100% that the object is a duck because it inherits from the class Duck
, here we are interested in the available behavior of the object. You can see that in Python or JavaScript. Kotlin is strongly typed language, and does not allow for multi-inheritance, but extension functions do allow you to "append" functionality to a class. So we can have a species-fluid dog:class York: Dog{
fun bark(){}
}
// adding `quacking` to York class
fun York.quack(){}
York().bark()
// and now it quacks
York().quack()
Adapter
, it would be enough to add an extension function for Item
:fun Item.prettyPrint(): String {
return "hello, my name is: ${this.name}"
}
// Item now is able to "pretty print" itself without additional Adapter
list.forEach {
println(it.prettyPrint())
}
// previous version with Adapter
list.forEach {
val adapted = ItemAdapter(it)
println(adapted.prettyPrint())
}
Extension functions
are generally a great feature, and in such a simple example, they'll probably do a better job than the additional Adapter
class. They can also be added to classes that we have no control over, such as those from libraries.interface Item
class FirstItem : Item
class AnotherItem : Item
// extension function for generic interface `Item`
fun Item.prettyPrint(): String = "hello, I'm: $this"
// extension function for concrete class `AnotherItem`
fun AnotherItem.betterPrint(): String = "greetings, I'm: $this"
// extension function overriding `prettyPrint` from the `Item` interface
fun AnotherItem.prettyPrint(): String = "yo, I'm: $this"
fun main() {
val item1: Item = FirstItem()
val item2: Item = AnotherItem()
val item3: AnotherItem = AnotherItem()
item1.prettyPrint()
item2.prettyPrint()
item3.prettyPrint() // which method will be called here?
item3.betterPrint()
}
prettyPrint
is especially interesting, where the implementation is different for the genericItem
interface and for the specific AnotherItem
class. The IDE and the compiler don't see any problem. The second method will be just used with AnotherItem
. The extension functions
can be written anywhere in your code, even in very distant places from where the class is declared.extension function
to a generic interface and used it for a long time without any issues with all the different types implementing this interface. At one point a completely different team needed an extension function
for a concrete type, so someone wrote a method with the same name and signature at a convenient place in code (for them), overriding your method for the generic interface. Without any override
keyword :) tests may catch it, but they don't have to. Code Review was probably done by someone on the other team, so until something starts behaving in an unexpected way, you probably won't find out about the entire operation. And then looking for the cause of the error may not be pleasant...extension functions
for an interface that can be easily overwrittenextension functions
all over the systemItemPrinter
would avoid misunderstandings and aid code review. In Git, you can easily see who authored or recently modified this class and add them to a pull request as well. For extension functions
, this option also exists, but methods can be scattered throughout the system, making it harder to find authors, and if something is more difficult than hassle-free, no one will do it.interface Shape {
fun draw()
fun createManipulator(): ShapeManipulator<out Shape>
}
interface ShapeManipulator<T : Shape> {
fun drag()
fun resize(scale: Float)
}
internal class Circle : Shape {
override fun draw() {}
override fun createManipulator() = CircleManipulator(this)
}
internal class CircleManipulator<T>(private val shape: T) : ShapeManipulator<Circle> {
override fun drag() = println("CircleManipulator is manipulating circle $shape")
override fun resize(scale: Float) = println("CircleManipulator is resizing circle $shape")
}
Shape
interface which is implemented by e.g.Circle
. Additionally, each shape has its own 'ShapeManipulator', an object that knows how to modify the size, position, etc. of a specific shape.Window
class displays the figures on the screenclass Window() {
fun drawShape(shape: Shape) {
// magic
}
}
TextView
class is not a shape, it has a different interface and comes from 3rd party library, for example. It is also such a complex class that there is no chance of rewriting it using the Shape
interface, or changing the behavior of the Window
class.class TextView {
fun displayText() {}
fun changeSize() {}
fun changePosition() {}
}
Adapter
class TextViewAdapter(val textView: TextView) : Shape {
override fun draw() {
textView.displayText()
}
// anonymous object instead of separate class
override fun createManipulator(): ShapeManipulator<out Shape> {
return object : ShapeManipulator<Shape> {
override fun drag() {
textView.changePosition()
}
override fun resize(scale: Float) {
textView.changeSize()
}
}
}
}
Window
behaves like any other figurefun main() {
val window = Window()
val circle = Circle()
window.drawShape(circle)
val textView = TextView()
window.drawShape(textView) // error! wrong interface
window.drawShape(TextViewAdapter(textView)) // using Adapter
}
Adapter
in the name is necessary. On the one hand, it's clear information that the object only maps one interface to another. On the other hand, this information may not be needed at all, clients are only interested in the interface. Does ItemAdapter
or ItemWrapper
say more than ItemWithPrettyPrint
? Moreover, if we create more adapters for different clients for Item
, naming them ItemForClient1Adapter
doesn't look great.Adaptee
), and the interface they implement (Target
).anticorruption layer
. Adaptee interface changes will only affect the Adapter
and not the rest of the code.extension functions
, to provideAdapter
-like functionality without having to create an entirely new class. This will make sense when you are not interested in the type of object but its capabilities, which is often referred to as Duck Typing
. However, extension functions
can obscure the actual class interface, override one another, and cause chaos in general. By limiting their scope, you can deal with it, but if their number starts growing for a specific class, it may be worth setting up a separate wrapper class to organize them.Adapter
with the required interface. The adapted class can change independently of the clients, and it is the job of the Adapter
to reconcile these changes with the client interface.extension function
will provide sufficient functionality, but in large projects with multiple teams working on the same code base, this may have unforeseen consequences. This problem can be partially solved by limiting the scope of extension functions
.