Continue our software design patterns overview and a closer look at the Structural pattern
In the previous article [1] Creational Design Patterns, we discussed in details the some of the most famous example of the Creational Design Patterns. Now we will continue with our next design pattern.
Structural design patterns are a type of design pattern that provide a way to simplify complex relationships between objects and classes in a software system. They are concerned with the way objects are composed to form larger structures and with the relationships between those structures. Structural design patterns can help to improve the overall flexibility and maintainability of the code.
Here are some examples of structural design patterns:
--------------------------
The Adapter pattern is a structural design pattern that allows two incompatible objects to work together by converting the interface of one object into an interface that the other object can understand. The adapter acts as a bridge between the two objects, allowing them to communicate with each other.
In other words, the Adapter pattern provides a way to wrap an existing class with a new interface, so that it becomes compatible with the client’s code. The client’s code can then work with the adapted class as if it was the original class, even though the underlying implementation may be completely different.
Here is a code example in Kotlin that demonstrates the Adapter pattern:
interface MediaPlayer { fun play(audioType: String, fileName: String) } interface AdvancedMediaPlayer { fun playVlc(fileName: String) fun playMp4(fileName: String) } class VlcPlayer : AdvancedMediaPlayer { override fun playVlc(fileName: String) { println("Playing vlc file. Name: $fileName") } override fun playMp4(fileName: String) { // do nothing } } class Mp4Player : AdvancedMediaPlayer { override fun playVlc(fileName: String) { // do nothing } override fun playMp4(fileName: String) { println("Playing mp4 file. Name: $fileName") } } class MediaAdapter(audioType: String) : MediaPlayer { private val advancedMusicPlayer: AdvancedMediaPlayer init { when (audioType.toLowerCase()) { "vlc" -> advancedMusicPlayer = VlcPlayer() "mp4" -> advancedMusicPlayer = Mp4Player() else -> advancedMusicPlayer = VlcPlayer() } } override fun play(audioType: String, fileName: String) { when (audioType.toLowerCase()) { "vlc" -> advancedMusicPlayer.playVlc(fileName) "mp4" -> advancedMusicPlayer.playMp4(fileName) } } } class AudioPlayer : MediaPlayer { private val mediaAdapter: MediaAdapter init { mediaAdapter = MediaAdapter("") } override fun play(audioType: String, fileName: String) { when (audioType.toLowerCase()) { "mp3" -> println("Playing mp3 file. Name: $fileName") "vlc", "mp4" -> mediaAdapter.play(audioType, fileName) else -> println("Invalid media. $audioType format not supported") } } }
In this example, the AudioPlayer class is the client that wants to play a variety of audio file formats. The MediaPlayer interface defines the play method that the client needs to call to play an audio file. The AdvancedMediaPlayer interface defines the methods for playing more advanced audio file formats, such as VLC and MP4. The VlcPlayer and Mp4Player classes implement the AdvancedMediaPlayer interface and provide the actual implementation for playing VLC and MP4 files, respectively.
The MediaAdapter class acts as the adapter between the MediaPlayer interface and the AdvancedMediaPlayer interface.
Some potential disadvantages of the Adapter pattern are:
---------------------
The Bridge pattern is a structural design pattern that separates an object’s abstraction from its implementation, allowing the two to vary independently. The pattern involves creating two separate classes: the abstraction class and the implementation class. The abstraction class defines the interface for the object, while the implementation class provides the actual implementation.
Here’s an example of the Bridge pattern implemented in Kotlin:
interface DrawingAPI { fun drawCircle(x: Double, y: Double, radius: Double) } class RedCircleAPI: DrawingAPI { override fun drawCircle(x: Double, y: Double, radius: Double) { println("Drawing Circle[ color: red, x: $x, y: $y, radius: $radius ]") } } class GreenCircleAPI: DrawingAPI { override fun drawCircle(x: Double, y: Double, radius: Double) { println("Drawing Circle[ color: green, x: $x, y: $y, radius: $radius ]") } } abstract class Shape(private val api: DrawingAPI) { abstract fun draw() abstract fun resizeByPercentage(pct: Double) } class CircleShape(x: Double, y: Double, radius: Double, api: DrawingAPI): Shape(api) { private var x = x private var y = y private var radius = radius override fun draw() { api.drawCircle(x, y, radius) } override fun resizeByPercentage(pct: Double) { radius *= pct } }
In this example, the Shape class is the abstraction class, and RedCircleAPI and GreenCircleAPI are the implementation classes. The CircleShape class is the object that uses the abstraction and implementation classes, and is able to draw circles in either red or green by using the api field. The implementation class can be changed at runtime, allowing the implementation of the object to be modified without affecting its abstraction.
Some of the disadvantages of the Bridge pattern include:
----------------------
The Composite pattern is a structural design pattern that allows you to compose objects into tree structures and treat individual objects and compositions of objects uniformly. The pattern involves creating a Component class that defines the interface for objects in the composition and a Composite class that implements the Component interface and stores child components.
Here’s an example of the Composite pattern implemented in Kotlin:
interface Component { fun operation() fun addChild(child: Component) fun removeChild(child: Component) fun getChild(index: Int): Component } class Leaf: Component { override fun operation() { println("Leaf operation") } override fun addChild(child: Component) { throw UnsupportedOperationException() } override fun removeChild(child: Component) { throw UnsupportedOperationException() } override fun getChild(index: Int): Component { throw UnsupportedOperationException() } } class Composite: Component { private val children = mutableListOf<Component>() override fun operation() { for (child in children) { child.operation() } } override fun addChild(child: Component) { children.add(child) } override fun removeChild(child: Component) { children.remove(child) } override fun getChild(index: Int): Component { return children[index] } }
In this example, the Component interface defines the operations that can be performed on objects in the composition, such as adding and removing child components. The Leaf class implements the Component interface and represents individual objects in the composition. The Composite class also implements the Component interface, and stores child components in a List. The Composite class can be used to represent compositions of objects, and its operation method calls the operation method of each of its child components. This allows the code to treat individual objects and compositions of objects in a uniform manner.
The Composite pattern can have the following disadvantages:
Overall, the Composite pattern should be used with care, and its advantages and disadvantages should be carefully considered in the context of the specific problem it is being used to solve.
-------------------------
The Decorator pattern is a structural design pattern that allows you to add behavior to individual objects at runtime without affecting the behavior of other objects of the same class. The pattern involves creating a Component interface that defines the methods that will be decorated, and a ConcreteComponent class that implements the Component interface. The Decorator class also implements the Component interface and wraps an instance of the Component class. The ConcreteDecorator class extends the Decorator class and adds additional behavior to the wrapped Component instance.
Here’s an example of the Decorator pattern implemented in Kotlin:
interface Shape { fun draw(): String } class Circle: Shape { override fun draw(): String { return "Drawing Circle" } } abstract class ShapeDecorator(protected val decoratedShape: Shape): Shape { override fun draw(): String { return decoratedShape.draw() } } class RedShapeDecorator(decoratedShape: Shape): ShapeDecorator(decoratedShape) { override fun draw(): String { return "${decoratedShape.draw()} with red border" } } class BlueShapeDecorator(decoratedShape: Shape): ShapeDecorator(decoratedShape) { override fun draw(): String { return "${decoratedShape.draw()} with blue border" } }
In this example, the Shape interface defines the method draw that will be decorated. The Circle class implements the Shape interface. The ShapeDecorator abstract class implements the Shape interface and wraps an instance of the Shape class, and the RedShapeDecorator and BlueShapeDecorator classes extend the ShapeDecorator class and add additional behavior to the wrapped Shape instance.
This allows you to add or remove decorations from the shapes dynamically at runtime without affecting the behavior of other shapes. For example:
val circle = Circle() println(circle.draw()) val redCircle = RedShapeDecorator(circle) println(redCircle.draw()) val blueCircle = BlueShapeDecorator(redCircle) println(blueCircle.draw())
This would output:
Drawing Circle Drawing Circle with red border Drawing Circle with red border with blue border
Some of the disadvantages of the Decorator pattern are:
In conclusion, the Decorator pattern is a useful tool in certain situations, but it should be used with care, and only when necessary.
---------
Next Part ==> Design Patterns - [3] Behavioral