Mohamad Abuzaid 1 year ago
mohamad-abuzaid #general

Design Patterns  -  [2] Structural

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.


[2] Structural Design Patterns


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:

  1. Adapter pattern: This pattern allows two incompatible objects to work together by providing a wrapper around one of the objects to make it compatible with the other. For example, a class that works with a specific interface can be adapted to work with another interface.
  2. Bridge pattern: This pattern allows to separate the abstract class or interface from its implementation, so that the two can evolve independently. For example, a class that represents a shape can be separated from its implementation, so that different shapes can be implemented differently.
  3. Composite pattern: This pattern allows to treat a group of objects in the same way as a single object, so that complex structures can be built from simple ones. For example, a class that represents a tree structure can be created from a class that represents a single node in the tree.
  4. Decorator pattern: This pattern allows to add new behaviors or responsibilities to an object dynamically, by wrapping the object with a decorator object. For example, a class that represents a text message can be decorated with additional behaviors, such as encryption or compression.

--------------------------


(A) Adapter Pattern


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.


* Disadvantages:

Some potential disadvantages of the Adapter pattern are:

  1. Increased complexity: It adds an extra layer of complexity to the system, as it involves creating an adapter class that acts as a bridge between the two incompatible objects. This extra layer of complexity can make the code more difficult to understand and maintain.
  2. Increased code size: It requires writing more code than the other structural design patterns, as it involves creating two or more classes (the target class, the adapter class, and the adaptee class). This can result in a larger codebase that is harder to manage.
  3. Performance overhead: It can add some performance overhead, as it requires converting the data between the target class and the adaptee class. This can lead to slower execution times and increased memory usage.
  4. Limited functionality: It may not be suitable for all use cases, as it only converts the interface of an object and does not modify its underlying implementation. In cases where more extensive modification of the adaptee class is required, the Adapter pattern may not be the best solution.

---------------------

(B) Bridge Pattern


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.


* Disadvantages:

Some of the disadvantages of the Bridge pattern include:

  1. Increased complexity: It can add complexity to a system, as it involves creating two separate classes for the abstraction and implementation, and establishing a relationship between them. This extra layer of complexity can make the code harder to understand and maintain.
  2. Increased code size: It requires writing more code than other structural design patterns, as it involves creating two separate classes. This can result in a larger codebase that is harder to manage.
  3. Performance overhead: It can add some performance overhead, as it involves creating two separate objects for the abstraction and implementation and establishing a relationship between them. This can lead to slower execution times and increased memory usage.
  4. Limited functionality: It may not be suitable for all use cases, as it only separates the abstraction and implementation of an object, and does not modify the underlying implementation. In cases where more extensive modification of the implementation is required, the Bridge pattern may not be the best solution.
  5. Difficulty in maintenance: Maintaining the relationship between the abstraction and implementation classes can be difficult, especially if changes are made to one of the classes that affect the other. This can lead to errors and bugs in the code that are difficult to fix.

----------------------


(C) Composite Pattern


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.


* Disadvantages:

The Composite pattern can have the following disadvantages:

  1. Increased Complexity: It can make the code more complex, especially if you need to handle multiple levels of composition.
  2. Difficult to change: Once the composition structure is established, it can be difficult to change. For example, adding or removing components can require significant code changes.
  3. Increased coupling: It increases the coupling between the components, as they all depend on the common Component interface. This can make the code more rigid and harder to maintain.
  4. Performance Overhead: It can result in a performance overhead, especially if the tree structure is deep and the number of nodes is large. The overhead comes from the overhead of managing the tree structure, as well as the overhead of the calls to the operation method.

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.

-------------------------


(D) Decorator Pattern


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


* Disadvantages:

Some of the disadvantages of the Decorator pattern are:

  1. Complexity: It can add significant complexity to your code, especially if you have many decorators and many types of objects that can be decorated. This can make your code harder to understand and maintain.
  2. Performance: It can introduce performance overhead, especially if the decorators add complex behavior to the decorated objects. This can make your program slower, especially if you have many objects that need to be decorated.
  3. Testing: Testing code that uses the Decorator pattern can be more difficult, since you need to test not only the decorated objects but also each decorator individually, and then test the combination of decorators. This can lead to a combinatorial explosion of test cases that need to be written and maintained.
  4. Overuse: Overuse of the Decorator pattern can lead to complex and confusing class hierarchies, especially if you have many different types of objects that need to be decorated and many different types of decorators. This can make your code hard to understand and maintain.

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

1
765
SOLID principles explained

SOLID principles explained

1675112374.jpg
Mohamad Abuzaid
1 year ago
Jetpack Compose — quick review

Jetpack Compose — quick review

1675112374.jpg
Mohamad Abuzaid
1 year ago
Security in Android App Development (2/3)

Security in Android App Development (2/3)

1675112374.jpg
Mohamad Abuzaid
7 months ago
In Kotlin Coroutines...

In Kotlin Coroutines...

1675112374.jpg
Mohamad Abuzaid
1 year ago
Jetpack Compose Animation

Jetpack Compose Animation

1675112374.jpg
Mohamad Abuzaid
2 months ago