Learning Design Patterns with a Shrubbery

Overview of Software Design Patterns

Design patterns are reusable solutions to common problems that arise in software design. They provide a shared vocabulary, best practices, and a way to improve the structure and maintainability of your code. Design patterns are typically divided into three main categories: creational, structural, and behavioral patterns.

Creational Patterns

Creational patterns focus on the process of object creation, providing various ways to create objects. This allows for flexibility, reusability, and maintainability in your code. The creational patterns include:

Abstract Factory

The Abstract Factory pattern is a creational design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes. It is particularly useful when you need to create objects that belong to different families, but share a common theme, and you want to enforce a consistent usage of these families of objects in your application.

// Abstract product classes
interface Shrubbery {
    fun getDescription(): String
}

interface Fertilizer {
    fun getNutrients(): String
}

// Concrete product classes
class OakShrubbery : Shrubbery {
    override fun getDescription(): String = "A small oak shrubbery."
}

class PineShrubbery : Shrubbery {
    override fun getDescription(): String = "A medium-sized pine shrubbery."
}

class OrganicFertilizer : Fertilizer {
    override fun getNutrients(): String = "Organic nutrients for healthy growth."
}

class SyntheticFertilizer : Fertilizer {
    override fun getNutrients(): String = "Synthetic nutrients for fast growth."
}

// Abstract factory
interface ShrubberyFactory {
    fun createShrubbery(): Shrubbery
    fun createFertilizer(): Fertilizer
}

// Concrete factory classes
class OakShrubberyFactory : ShrubberyFactory {
    override fun createShrubbery(): Shrubbery = OakShrubbery()
    override fun createFertilizer(): Fertilizer = OrganicFertilizer()
}

class PineShrubberyFactory : ShrubberyFactory {
    override fun createShrubbery(): Shrubbery = PineShrubbery()
    override fun createFertilizer(): Fertilizer = SyntheticFertilizer()
}

fun main() {
    val oakFactory: ShrubberyFactory = OakShrubberyFactory()
    val oakShrubbery = oakFactory.createShrubbery()
    val oakFertilizer = oakFactory.createFertilizer()

    println(oakShrubbery.getDescription())
    println(oakFertilizer.getNutrients())

    val pineFactory: ShrubberyFactory = PineShrubberyFactory()
    val pineShrubbery = pineFactory.createShrubbery()
    val pineFertilizer = pineFactory.createFertilizer()

    println(pineShrubbery.getDescription())
    println(pineFertilizer.getNutrients())
}

In this example, we have two families of related objects: Shrubbery and Fertilizer. Each family has concrete classes that implement their respective interfaces, like OakShrubbery, PineShrubbery, OrganicFertilizer, and SyntheticFertilizer.

We define an abstract factory interface called ShrubberyFactory, which declares methods for creating instances of Shrubbery and Fertilizer. Then, we create concrete factory classes, OakShrubberyFactory and PineShrubberyFactory, that implement the ShrubberyFactory interface and produce instances of the corresponding concrete classes.

In the main function, we create instances of the concrete factories, and use them to create instances of the concrete products. By using the Abstract Factory pattern, we can enforce consistency in the usage of these families of objects, and easily switch between different families without changing the client code.

Builder

The Builder pattern is a creational design pattern that separates the construction of a complex object from its representation. It allows the same construction process to create different representations of the object. The pattern is particularly useful when the object creation involves multiple steps or requires a specific order of actions.

// Product class
data class Shrubbery(val type: String, val height: Int, val width: Int)

// Builder interface
interface ShrubberyBuilder {
    fun setType(type: String): ShrubberyBuilder
    fun setHeight(height: Int): ShrubberyBuilder
    fun setWidth(width: Int): ShrubberyBuilder
    fun build(): Shrubbery
}

// Concrete builder
class ShrubberyBuilderImpl : ShrubberyBuilder {
    private lateinit var type: String
    private var height: Int = 0
    private var width: Int = 0

    override fun setType(type: String): ShrubberyBuilder {
        this.type = type
        return this
    }

    override fun setHeight(height: Int): ShrubberyBuilder {
        this.height = height
        return this
    }

    override fun setWidth(width: Int): ShrubberyBuilder {
        this.width = width
        return this
    }

    override fun build(): Shrubbery {
        return Shrubbery(type, height, width)
    }
}

fun main() {
    val shrubberyBuilder: ShrubberyBuilder = ShrubberyBuilderImpl()

    val oakShrubbery = shrubberyBuilder
        .setType("Oak")
        .setHeight(3)
        .setWidth(2)
        .build()

    val pineShrubbery = shrubberyBuilder
        .setType("Pine")
        .setHeight(5)
        .setWidth(4)
        .build()

    println("Oak Shrubbery: $oakShrubbery")
    println("Pine Shrubbery: $pineShrubbery")
}

In this example, we have a Shrubbery data class that represents the product. The ShrubberyBuilder interface defines the methods required to set the properties of the Shrubbery object and to build the final object.

We create a concrete builder class ShrubberyBuilderImpl that implements the ShrubberyBuilder interface. The ShrubberyBuilderImpl class maintains the state of the Shrubbery object being constructed and provides fluent methods for setting the properties and building the final object.

In the main function, we create an instance of the concrete builder and use it to construct two different Shrubbery objects, oakShrubbery and pineShrubbery. The Builder pattern allows us to create complex objects step by step, making it easy to create different representations of the object while keeping the construction process separate from the product class.

Factory Method

The Factory Method pattern is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. It allows the client code to work with the superclass, while the actual object instantiation is deferred to the subclasses.

// Product interface
interface Shrubbery {
    fun getDescription(): String
}

// Concrete products
class OakShrubbery : Shrubbery {
    override fun getDescription(): String = "A small oak shrubbery."
}

class PineShrubbery : Shrubbery {
    override fun getDescription(): String = "A medium-sized pine shrubbery."
}

// Creator abstract class
abstract class ShrubberyCreator {
    abstract fun createShrubbery(): Shrubbery

    fun plantShrubbery() {
        val shrubbery = createShrubbery()
        println("Planting a new shrubbery: ${shrubbery.getDescription()}")
    }
}

// Concrete creators
class OakShrubberyCreator : ShrubberyCreator() {
    override fun createShrubbery(): Shrubbery = OakShrubbery()
}

class PineShrubberyCreator : ShrubberyCreator() {
    override fun createShrubbery(): Shrubbery = PineShrubbery()
}

fun main() {
    val oakCreator: ShrubberyCreator = OakShrubberyCreator()
    oakCreator.plantShrubbery()

    val pineCreator: ShrubberyCreator = PineShrubberyCreator()
    pineCreator.plantShrubbery()
}

In this example, we have a Shrubbery interface that serves as the product interface, and two concrete classes, OakShrubbery and PineShrubbery, implementing the Shrubbery interface.

We define an abstract class ShrubberyCreator that has an abstract method createShrubbery() for creating Shrubbery instances. The ShrubberyCreator class also has a method plantShrubbery() that plants a new shrubbery and prints its description. The actual creation of the shrubbery is delegated to the createShrubbery() method, which is implemented by the concrete creator classes.

We create two concrete creator classes, OakShrubberyCreator and PineShrubberyCreator, that extend the ShrubberyCreator abstract class and override the createShrubbery() method to return instances of the corresponding concrete Shrubbery classes.

In the main function, we create instances of the concrete creator classes and call their plantShrubbery() method. The Factory Method pattern allows us to encapsulate the object creation process and delegate it to the concrete creator classes, making it easy to add new types of shrubberies without changing the client code.

Object Pool

The Object Pool pattern is a creational design pattern that involves maintaining a pool of reusable objects instead of creating and destroying them on demand. This pattern can be useful when object creation is expensive or slow, and the objects can be reused multiple times. The Object Pool pattern can improve performance by reducing the overhead of creating and destroying objects, especially in situations where the demand for objects fluctuates or is unpredictable.

import java.util.concurrent.ConcurrentLinkedQueue

// Reusable class
class Shrubbery(val type: String) {
    fun trim() {
        println("Trimming $type shrubbery")
    }
}

// Object pool
class ShrubberyPool {
    private val available = ConcurrentLinkedQueue<Shrubbery>()

    fun acquire(type: String): Shrubbery {
        return available.poll() ?: Shrubbery(type)
    }

    fun release(shrubbery: Shrubbery) {
        available.add(shrubbery)
    }
}

fun main() {
    val shrubberyPool = ShrubberyPool()

    // Acquire a shrubbery
    val shrubbery1 = shrubberyPool.acquire("Oak")
    shrubbery1.trim()

    // Release the shrubbery
    shrubberyPool.release(shrubbery1)

    // Acquire another shrubbery (reusing the first one)
    val shrubbery2 = shrubberyPool.acquire("Oak")
    shrubbery2.trim()

    // Release the second shrubbery
    shrubberyPool.release(shrubbery2)
}

In this example, we have a Shrubbery class representing a reusable object. We create an ShrubberyPool class that manages the pool of reusable Shrubbery objects. The pool uses a ConcurrentLinkedQueue to store available objects.

The ShrubberyPool class provides two methods: acquire and release. The acquire method returns an available object from the pool if one exists, or it creates a new object if the pool is empty. The release method adds an object back to the pool when it's no longer needed, making it available for reuse.

In the main function, we create an instance of the ShrubberyPool and use it to acquire a Shrubbery object. We then release the object back to the pool and acquire another one. In this example, the second Shrubbery object is actually the same as the first one, demonstrating the reusability aspect of the Object Pool pattern.

Prototype

The Prototype pattern is a creational design pattern that involves creating new objects by cloning an existing object, instead of creating them from scratch. This can be useful when the object creation process is resource-intensive, or when the object's state is complex and difficult to initialize directly. By leveraging the Prototype pattern, you can create new objects efficiently by copying the existing object and potentially modifying some of its properties.

// Prototype interface
interface ShrubberyPrototype : Cloneable {
    fun trim()

    public override fun clone(): ShrubberyPrototype
}

// Concrete prototype class
class Shrubbery(private val type: String) : ShrubberyPrototype {
    override fun trim() {
        println("Trimming $type shrubbery")
    }

    override fun clone(): ShrubberyPrototype {
        return Shrubbery(type)
    }
}

fun main() {
    // Create a prototype object
    val oakShrubbery = Shrubbery("Oak")
    oakShrubbery.trim()

    // Clone the prototype object
    val clonedOakShrubbery = oakShrubbery.clone()
    clonedOakShrubbery.trim()

    // Create another prototype object
    val pineShrubbery = Shrubbery("Pine")
    pineShrubbery.trim()

    // Clone the second prototype object
    val clonedPineShrubbery = pineShrubbery.clone()
    clonedPineShrubbery.trim()
}

In this example, we define a ShrubberyPrototype interface that extends Cloneable. This interface declares the trim method and a clone method to create a copy of the object.

We implement the ShrubberyPrototype interface in the Shrubbery class. The Shrubbery class has a type property and implements the trim and clone methods. The clone method creates a new Shrubbery object with the same type property.

In the main function, we create a prototype Shrubbery object and then clone it. We also create another prototype object with a different type and clone that one as well. The cloned objects have the same state as their prototypes but are distinct instances.

Singleton

The Singleton pattern is a creational design pattern that ensures a class has only one instance and provides a global point of access to that instance. This can be useful when you need to coordinate actions across a system or when you want to share data between different parts of your application.

Kotlin provides a straightforward way to implement the Singleton pattern using the object keyword.

// Singleton class
object ShrubberyManager {
    private val shrubberies = mutableListOf<Shrubbery>()

    fun addShrubbery(shrubbery: Shrubbery) {
        shrubberies.add(shrubbery)
    }

    fun removeShrubbery(shrubbery: Shrubbery) {
        shrubberies.remove(shrubbery)
    }

    fun listShrubberies() {
        println("Shrubberies:")
        shrubberies.forEach { shrubbery ->
            println("- ${shrubbery.type}")
        }
    }
}

// Simple class to represent a shrubbery
data class Shrubbery(val type: String)

fun main() {
    // Add shrubberies to the ShrubberyManager
    ShrubberyManager.addShrubbery(Shrubbery("Oak"))
    ShrubberyManager.addShrubbery(Shrubbery("Pine"))

    // List the shrubberies
    ShrubberyManager.listShrubberies()

    // Remove a shrubbery
    ShrubberyManager.removeShrubbery(Shrubbery("Oak"))

    // List the shrubberies again
    ShrubberyManager.listShrubberies()
}

In this example, we use the object keyword to create a Singleton class called ShrubberyManager. This class manages a list of Shrubbery objects and provides methods to add, remove, and list shrubberies.

We also create a simple Shrubbery data class with a type property.

In the main function, we demonstrate the usage of the Singleton by adding and removing shrubberies and listing them. Since the ShrubberyManager is an object, there is only one instance of it, and we can access it directly using its name.

With this approach, Kotlin guarantees that there will be only one instance of the ShrubberyManager in the application, and it provides a global point of access to it.

Structural Patterns

Structural patterns are concerned with the composition of classes and objects, defining the relationships between them. These patterns help ensure that the structure of your code is efficient, scalable, and maintainable. The structural patterns include:

Adapter

The Adapter pattern is a structural design pattern that allows objects with incompatible interfaces to work together. It does so by wrapping one of the objects in an adapter that implements the expected interface, effectively making the objects compatible. This pattern can be particularly useful when you need to integrate a third-party library or legacy code with a new system.

// Target interface
interface ShrubberyCutter {
    fun cutShrubbery(shrubbery: Shrubbery)
}

// Adaptee class
class OldShrubberyCutter {
    fun trimShrubbery(shrubbery: Shrubbery) {
        println("Trimming ${shrubbery.type} shrubbery with the old cutter")
    }
}

// Adapter class
class ShrubberyCutterAdapter(private val oldShrubberyCutter: OldShrubberyCutter) : ShrubberyCutter {
    override fun cutShrubbery(shrubbery: Shrubbery) {
        oldShrubberyCutter.trimShrubbery(shrubbery)
    }
}

// Simple class to represent a shrubbery
data class Shrubbery(val type: String)

fun main() {
    val oldCutter = OldShrubberyCutter()
    val adapter = ShrubberyCutterAdapter(oldCutter)

    val shrubbery = Shrubbery("Pine")
    adapter.cutShrubbery(shrubbery)
}

In this example, we define a ShrubberyCutter interface, which represents the target interface that we want our code to work with. The ShrubberyCutter interface has a cutShrubbery method.

We also define an OldShrubberyCutter class, which represents the existing, incompatible class (the Adaptee). This class has a trimShrubbery method with the same functionality but a different name.

To make the OldShrubberyCutter compatible with the ShrubberyCutter interface, we create an Adapter class called ShrubberyCutterAdapter. This adapter implements the ShrubberyCutter interface and takes an instance of OldShrubberyCutter as a constructor parameter. The adapter's cutShrubbery method simply delegates the call to the trimShrubbery method of the OldShrubberyCutter.

In the main function, we create an instance of the OldShrubberyCutter and wrap it in a ShrubberyCutterAdapter. We then use the adapter to cut a shrubbery, demonstrating that the adapter makes the OldShrubberyCutter compatible with the ShrubberyCutter interface.

Bridge

The Bridge pattern is a structural design pattern that decouples an abstraction from its implementation, allowing the two to vary independently. This pattern can be useful when you want to separate a complex system into smaller, more manageable components, or when you want to create a system that can be extended without affecting existing code.

// Abstraction
interface ShrubberyRenderer {
    fun renderShrubbery(shrubbery: Shrubbery)
}

// Refined abstraction
class ColorfulShrubberyRenderer(private val color: String) : ShrubberyRenderer {
    override fun renderShrubbery(shrubbery: Shrubbery) {
        println("Rendering a $color ${shrubbery.type} shrubbery")
    }
}

// Implementor
interface ShrubberyPlatform {
    fun drawShrubbery(renderer: ShrubberyRenderer, shrubbery: Shrubbery)
}

// Concrete Implementor
class ShrubberyGarden(private val platform: ShrubberyPlatform) : ShrubberyPlatform {
    override fun drawShrubbery(renderer: ShrubberyRenderer, shrubbery: Shrubbery) {
        println("Drawing a shrubbery in the garden...")
        platform.drawShrubbery(renderer, shrubbery)
    }
}

// Simple class to represent a shrubbery
data class Shrubbery(val type: String)

fun main() {
    val garden = ShrubberyGarden(object : ShrubberyPlatform {
        override fun drawShrubbery(renderer: ShrubberyRenderer, shrubbery: Shrubbery) {
            renderer.renderShrubbery(shrubbery)
        }
    })

    val shrubbery = Shrubbery("Pine")
    val renderer = ColorfulShrubberyRenderer("Green")

    garden.drawShrubbery(renderer, shrubbery)
}

In this example, we define a ShrubberyRenderer interface, which represents the abstraction. This interface has a renderShrubbery method. We also define a ColorfulShrubberyRenderer class, which refines the abstraction by adding a color property.

We then define a ShrubberyPlatform interface, which represents the implementor. This interface has a drawShrubbery method that takes a ShrubberyRenderer and a Shrubbery as parameters. The ShrubberyGarden class is a concrete implementor that implements the ShrubberyPlatform interface.

In the main function, we create an instance of ShrubberyGarden, passing an anonymous object implementing the ShrubberyPlatform interface as a parameter. We also create a Shrubbery object and a ColorfulShrubberyRenderer object. We then call the drawShrubbery method of the ShrubberyGarden, passing the renderer and the shrubbery as parameters. This demonstrates how the Bridge pattern allows the abstraction and its implementation to vary independently.

Composite

The Composite pattern is a structural design pattern that allows you to compose objects into tree structures to represent part-whole hierarchies. This pattern is useful when you want to treat individual objects and compositions of objects uniformly.

// Component
interface ShrubberyComponent {
    fun display(depth: Int)
}

// Leaf
class Shrubbery(val type: String) : ShrubberyComponent {
    override fun display(depth: Int) {
        println("${"-".repeat(depth)} $type")
    }
}

// Composite
class ShrubberyGroup : ShrubberyComponent {
    private val shrubberyComponents = mutableListOf<ShrubberyComponent>()

    fun add(shrubberyComponent: ShrubberyComponent) {
        shrubberyComponents.add(shrubberyComponent)
    }

    fun remove(shrubberyComponent: ShrubberyComponent) {
        shrubberyComponents.remove(shrubberyComponent)
    }

    override fun display(depth: Int) {
        println("${"-".repeat(depth)} Group")
        shrubberyComponents.forEach { it.display(depth + 2) }
    }
}

fun main() {
    val pine = Shrubbery("Pine")
    val oak = Shrubbery("Oak")

    val shrubberyGroup = ShrubberyGroup()
    shrubberyGroup.add(pine)
    shrubberyGroup.add(oak)

    val topLevelGroup = ShrubberyGroup()
    topLevelGroup.add(Shrubbery("Maple"))
    topLevelGroup.add(shrubberyGroup)

    // Displays the tree structure of the shrubberies
    topLevelGroup.display(0)
}

In this example, we define a ShrubberyComponent interface, which represents the component. This interface has a display method that takes an integer parameter representing the depth in the tree structure.

The Shrubbery class represents a leaf node in the tree structure and implements the ShrubberyComponent interface. The display method prints the type of the shrubbery with indentation based on the depth.

The ShrubberyGroup class represents a composite node in the tree structure and also implements the ShrubberyComponent interface. It maintains a list of child components and provides add and remove methods to manage the child components. The display method prints "Group" with indentation based on the depth, and then iterates through the child components, calling their display method with an incremented depth.

In the main function, we create individual Shrubbery objects and ShrubberyGroup objects. We add Shrubbery objects to the shrubberyGroup and then add both individual Shrubbery objects and the shrubberyGroup to the topLevelGroup. Finally, we call the display method on the topLevelGroup to display the tree structure of the shrubberies.

Decorator

The Decorator pattern is a structural design pattern that allows you to attach new responsibilities to objects dynamically without changing their underlying structure. Decorators provide a flexible alternative to subclassing for extending functionality.

// Component
interface Shrubbery {
    fun getDescription(): String
    fun getCost(): Double
}

// Concrete Component
class BasicShrubbery : Shrubbery {
    override fun getDescription() = "Basic Shrubbery"

    override fun getCost() = 10.0
}

// Decorator
abstract class ShrubberyDecorator(private val shrubbery: Shrubbery) : Shrubbery {
    override fun getDescription() = shrubbery.getDescription()

    override fun getCost() = shrubbery.getCost()
}

// Concrete Decorator
class FertilizedShrubbery(private val shrubbery: Shrubbery) : ShrubberyDecorator(shrubbery) {
    override fun getDescription() = "${shrubbery.getDescription()}, Fertilized"

    override fun getCost() = shrubbery.getCost() + 5.0
}

// Another Concrete Decorator
class PrunedShrubbery(private val shrubbery: Shrubbery) : ShrubberyDecorator(shrubbery) {
    override fun getDescription() = "${shrubbery.getDescription()}, Pruned"

    override fun getCost() = shrubbery.getCost() + 7.5
}

fun main() {
    val basicShrubbery: Shrubbery = BasicShrubbery()
    val fertilizedShrubbery: Shrubbery = FertilizedShrubbery(basicShrubbery)
    val prunedAndFertilizedShrubbery: Shrubbery = PrunedShrubbery(fertilizedShrubbery)

    println("Basic Shrubbery: ${basicShrubbery.getDescription()} costs ${basicShrubbery.getCost()}")
    println("Fertilized Shrubbery: ${fertilizedShrubbery.getDescription()} costs ${fertilizedShrubbery.getCost()}")
    println("Pruned and Fertilized Shrubbery: ${prunedAndFertilizedShrubbery.getDescription()} costs ${prunedAndFertilizedShrubbery.getCost()}")
}

In this example, we have a Shrubbery interface, which represents the Component. It has two methods: getDescription() and getCost().

The BasicShrubbery class is the Concrete Component that implements the Shrubbery interface. It provides a basic description and cost for a shrubbery.

The ShrubberyDecorator is an abstract class that also implements the Shrubbery interface. It takes a Shrubbery object as a constructor parameter and delegates the getDescription() and getCost() methods to the wrapped Shrubbery object.

We then define two Concrete Decorator classes: FertilizedShrubbery and PrunedShrubbery. Both extend the ShrubberyDecorator class and override the getDescription() and getCost() methods to add their specific functionality and cost.

In the main function, we create instances of the BasicShrubbery, FertilizedShrubbery, and PrunedShrubbery. We then demonstrate how decorators can be combined to create a pruned and fertilized shrubbery. Finally, we print the descriptions and costs of each shrubbery.

Facade

The Facade pattern is a structural design pattern that provides a simplified interface to a more complex subsystem. It is particularly useful when you want to hide the complexities of a system and provide a cleaner, more straightforward interface for clients to interact with.

// Complex subsystem classes
class SoilPreparation {
    fun prepare() {
        println("Preparing the soil...")
    }
}

class Planting {
    fun plantShrubbery() {
        println("Planting the shrubbery...")
    }
}

class Watering {
    fun water() {
        println("Watering the shrubbery...")
    }
}

class Fertilizing {
    fun fertilize() {
        println("Fertilizing the shrubbery...")
    }
}

// Facade class
class ShrubberyGardener {
    private val soilPreparation = SoilPreparation()
    private val planting = Planting()
    private val watering = Watering()
    private val fertilizing = Fertilizing()

    fun plantShrubbery() {
        soilPreparation.prepare()
        planting.plantShrubbery()
        watering.water()
        fertilizing.fertilize()
        println("Shrubbery successfully planted!")
    }
}

fun main() {
    val shrubberyGardener = ShrubberyGardener()
    shrubberyGardener.plantShrubbery()
}

In this example, we have a complex subsystem that consists of several classes, each representing a step in the process of planting a shrubbery: SoilPreparation, Planting, Watering, and Fertilizing. Each class has a method to perform its specific task.

The ShrubberyGardener class serves as the Facade in this example. It simplifies the process of planting a shrubbery by hiding the complexity of the subsystem behind a single method: plantShrubbery(). Inside this method, it instantiates the subsystem classes and calls their respective methods in the correct order.

In the main function, we create an instance of the ShrubberyGardener facade and call its plantShrubbery() method to plant a shrubbery. Clients only need to interact with the facade, making it easier for them to use the subsystem.

Flyweight

The Flyweight pattern is a structural design pattern that optimizes memory usage by sharing common data between similar objects. It is particularly useful when you have a large number of objects that share common properties, allowing you to reduce the memory footprint by storing these shared properties in a separate flyweight object.

// Flyweight class
data class ShrubberyType(val name: String, val color: String, val height: Double)

// Object that uses the Flyweight
class Shrubbery(val type: ShrubberyType, val x: Int, val y: Int) {
    fun display() {
        println("Displaying shrubbery of type ${type.name} with color ${type.color} and height ${type.height} at position ($x, $y)")
    }
}

// Flyweight factory
class ShrubberyTypeFactory {
    private val shrubberyTypes = mutableMapOf<String, ShrubberyType>()

    fun getShrubberyType(name: String, color: String, height: Double): ShrubberyType {
        val key = "$name-$color-$height"
        return shrubberyTypes.getOrPut(key) { ShrubberyType(name, color, height) }
    }
}

fun main() {
    val factory = ShrubberyTypeFactory()

    val shrubberyType1 = factory.getShrubberyType("Round", "Green", 1.5)
    val shrubberyType2 = factory.getShrubberyType("Tall", "Red", 3.0)

    val shrubberies = listOf(
        Shrubbery(shrubberyType1, 0, 0),
        Shrubbery(shrubberyType1, 1, 1),
        Shrubbery(shrubberyType2, 2, 2),
        Shrubbery(shrubberyType2, 3, 3)
    )

    shrubberies.forEach { it.display() }
}

In this example, the ShrubberyType class serves as the Flyweight. It represents the shared data (name, color, and height) for different shrubberies. The Shrubbery class is the object that uses the Flyweight, and it has a reference to the ShrubberyType instance and its own unique properties, such as x and y coordinates.

The ShrubberyTypeFactory class is responsible for creating and managing the flyweight objects. It maintains a map of existing flyweight instances and creates a new one only if an instance with the requested properties does not already exist.

In the main function, we create a ShrubberyTypeFactory instance and use it to get two different ShrubberyType instances. We then create a list of Shrubbery instances, some of which share the same ShrubberyType. Finally, we display the shrubberies, demonstrating that they share the same ShrubberyType instances while having their own unique properties.

Proxy

The Proxy pattern is a structural design pattern that provides an object acting as an interface to another object, the "real" subject. This pattern is useful when you want to control access to the real subject, enhance its behavior, or delay its instantiation or loading.

interface Shrubbery {
    fun display(): String
}

class RealShrubbery(private val name: String) : Shrubbery {
    init {
        loadShrubbery(name)
    }

    private fun loadShrubbery(name: String) {
        println("Loading shrubbery $name")
    }

    override fun display(): String {
        return "Displaying shrubbery $name"
    }
}

class ShrubberyProxy(private val name: String) : Shrubbery {
    private var realShrubbery: RealShrubbery? = null

    override fun display(): String {
        if (realShrubbery == null) {
            realShrubbery = RealShrubbery(name)
        }
        return realShrubbery!!.display()
    }
}

fun main() {
    val shrubberyProxy = ShrubberyProxy("Round")
    println("Shrubbery proxy created")

    // The real subject is created and loaded only when it's needed
    println(shrubberyProxy.display())
}

In this example, we have a Shrubbery interface that both the RealShrubbery class and the ShrubberyProxy class implement. The RealShrubbery class represents the actual subject, and its constructor simulates loading the shrubbery, which may be an expensive operation.

The ShrubberyProxy class acts as a proxy for the RealShrubbery. It has a reference to a RealShrubbery instance, but it doesn't instantiate it right away. Instead, it creates the real subject only when it's needed, in this case, when the display method is called.

In the main function, we create a ShrubberyProxy instance, which doesn't load the actual shrubbery yet. When we call the display method on the proxy, it instantiates the RealShrubbery, loads it, and then calls its display method.

This example demonstrates the lazy loading technique, which is just one of the many use cases for the Proxy pattern. Other use cases include access control, caching, or even adding functionality to the real subject without modifying it.

Behavioral Patterns

Behavioral patterns define the ways in which objects interact and communicate with one another, encapsulating the logic behind these interactions. They help improve the flexibility, maintainability, and reusability of your code by decoupling components. The behavioral patterns include:

Chain of Responsibility

The Chain of Responsibility pattern is a behavioral design pattern that enables you to create a chain of objects that can handle a request. Each object in the chain can either handle the request or pass it to the next object in the chain. This pattern is useful for decoupling the sender of a request from the receiver by allowing multiple objects to handle the request.

abstract class ShrubberyHandler {
    private var nextHandler: ShrubberyHandler? = null

    fun setNext(handler: ShrubberyHandler): ShrubberyHandler {
        nextHandler = handler
        return handler
    }

    fun handleRequest(request: String) {
        if (canHandleRequest(request)) {
            processRequest(request)
        } else {
            nextHandler?.handleRequest(request)
        }
    }

    abstract fun canHandleRequest(request: String): Boolean

    abstract fun processRequest(request: String)
}

class ShrubberySizeHandler : ShrubberyHandler() {
    override fun canHandleRequest(request: String): Boolean {
        return request.contains("size")
    }

    override fun processRequest(request: String) {
        println("Handling size request: $request")
    }
}

class ShrubberyColorHandler : ShrubberyHandler() {
    override fun canHandleRequest(request: String): Boolean {
        return request.contains("color")
    }

    override fun processRequest(request: String) {
        println("Handling color request: $request")
    }
}

fun main() {
    val sizeHandler = ShrubberySizeHandler()
    val colorHandler = ShrubberyColorHandler()

    sizeHandler.setNext(colorHandler)

    val requests = listOf("size:small", "color:green", "size:large", "color:red")

    for (request in requests) {
        sizeHandler.handleRequest(request)
    }
}

In this example, we have an abstract ShrubberyHandler class that represents a generic handler in the chain. It has a reference to the next handler, which is initially null. The setNext method allows us to create the chain by setting the next handler in the chain. The handleRequest method checks if the handler can handle the request, and if so, processes it. Otherwise, it passes the request to the next handler in the chain.

We then have two concrete handler classes: ShrubberySizeHandler and ShrubberyColorHandler, both extending ShrubberyHandler. Each of them overrides the canHandleRequest and processRequest methods to define their specific behavior.

In the main function, we create the handler objects and set up the chain. We then iterate through a list of requests and call the handleRequest method on the first handler in the chain (sizeHandler). Each request is processed by the appropriate handler in the chain based on its content.

This example demonstrates a simple use case for the Chain of Responsibility pattern. It can be extended and adapted to more complex scenarios, such as handling different types of requests, adding more handlers, or even using a more sophisticated mechanism for determining which handler should process a request.

Command

The Command pattern is a behavioral design pattern that turns a request into a stand-alone object that contains all the information about the request. This allows you to pass requests as method arguments, delay or queue request execution, and support undoable operations.

// Command interface
interface ShrubberyCommand {
    fun execute()
}

// Concrete command classes
class PlantShrubberyCommand(private val shrubbery: String) : ShrubberyCommand {
    override fun execute() {
        println("Planting shrubbery: $shrubbery")
    }
}

class TrimShrubberyCommand(private val shrubbery: String) : ShrubberyCommand {
    override fun execute() {
        println("Trimming shrubbery: $shrubbery")
    }
}

// Invoker class
class ShrubberyGardener {
    private val commandQueue = mutableListOf<ShrubberyCommand>()

    fun addCommand(command: ShrubberyCommand) {
        commandQueue.add(command)
    }

    fun executeCommands() {
        for (command in commandQueue) {
            command.execute()
        }
        commandQueue.clear()
    }
}

fun main() {
    val gardener = ShrubberyGardener()

    gardener.addCommand(PlantShrubberyCommand("Rose"))
    gardener.addCommand(TrimShrubberyCommand("Rose"))
    gardener.addCommand(PlantShrubberyCommand("Lilac"))

    gardener.executeCommands()
}

In this example, we have a ShrubberyCommand interface that represents the Command. This interface has a single method execute() that concrete command classes must implement.

We then create two concrete command classes, PlantShrubberyCommand and TrimShrubberyCommand, both implementing the ShrubberyCommand interface. Each of these command classes has a shrubbery property and an implementation of the execute() method to perform the specific action.

The ShrubberyGardener class serves as the invoker. It maintains a queue of commands and provides methods to add commands to the queue and execute them in order. In this example, the executeCommands() method iterates through the queue, executes each command, and clears the queue afterward.

In the main function, we create a ShrubberyGardener object and add a few commands to its queue. We then call the executeCommands() method to execute all the commands in the queue.

The Command pattern allows us to encapsulate requests and their associated data in command objects. This can help simplify code, enable reusability of commands, and make it easy to add new commands to the system.

Interpreter

The Interpreter pattern is a behavioral design pattern that defines a representation for a language's grammar and provides an interpreter to evaluate expressions in the language. This pattern is useful when you need to implement a simple language, perform calculations, or process structured text data.

// Expression interface
interface ShrubberyExpression {
    fun interpret(context: String): Boolean
}

// Terminal expression
class ShrubberyTerminalExpression(private val data: String) : ShrubberyExpression {
    override fun interpret(context: String): Boolean {
        return context.contains(data)
    }
}

// Or expression
class ShrubberyOrExpression(private val expr1: ShrubberyExpression, private val expr2: ShrubberyExpression) : ShrubberyExpression {
    override fun interpret(context: String): Boolean {
        return expr1.interpret(context) || expr2.interpret(context)
    }
}

// And expression
class ShrubberyAndExpression(private val expr1: ShrubberyExpression, private val expr2: ShrubberyExpression) : ShrubberyExpression {
    override fun interpret(context: String): Boolean {
        return expr1.interpret(context) && expr2.interpret(context)
    }
}

fun main() {
    // Create terminal expressions
    val roseExpression = ShrubberyTerminalExpression("Rose")
    val lilacExpression = ShrubberyTerminalExpression("Lilac")

    // Create combined expressions
    val roseOrLilacExpression = ShrubberyOrExpression(roseExpression, lilacExpression)
    val roseAndLilacExpression = ShrubberyAndExpression(roseExpression, lilacExpression)

    // Interpret expressions
    println("Garden has Rose or Lilac: ${roseOrLilacExpression.interpret("Rose, Lilac")}")
    println("Garden has Rose and Lilac: ${roseAndLilacExpression.interpret("Rose, Lilac")}")
}

In this example, we define a ShrubberyExpression interface that represents the abstract syntax tree of our simple language. This interface has a single method, interpret(), which takes a context (in this case, a String) and returns a Boolean value.

We then create ShrubberyTerminalExpression, which is a terminal expression representing specific data in our language (in this case, the name of a shrubbery). It implements the interpret() method by checking if the context contains the specific data.

We also create two non-terminal expressions, ShrubberyOrExpression and ShrubberyAndExpression. Each of these classes takes two ShrubberyExpression objects and implements the interpret() method by performing the appropriate logical operation (OR or AND) on the results of the interpret() method calls on the two expressions.

In the main function, we create instances of the terminal and non-terminal expressions and test their interpretations.

The Interpreter pattern is suitable when you have a well-defined grammar for a simple language, and the performance of the interpreter is not critical. In cases where the language is more complex or performance is crucial, you may want to consider using other techniques such as parsing or compilation.

Iterator

The Iterator pattern is a behavioral design pattern that provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. The pattern defines an interface for traversing a collection of elements and decouples the client code from the internal structure of the collection.

In Kotlin, the Iterator pattern is implemented by default in the standard library for many collections, such as lists and sets.

// Aggregate interface
interface ShrubberyCollection {
    fun createIterator(): ShrubberyIterator
}

// Iterator interface
interface ShrubberyIterator {
    fun hasNext(): Boolean
    fun next(): String
}

// Concrete aggregate
class ShrubberyList(private val shrubberies: List<String>) : ShrubberyCollection {
    override fun createIterator(): ShrubberyIterator {
        return ShrubberyListIterator(shrubberies)
    }
}

// Concrete iterator
class ShrubberyListIterator(private val shrubberies: List<String>) : ShrubberyIterator {
    private var index = 0

    override fun hasNext(): Boolean {
        return index < shrubberies.size
    }

    override fun next(): String {
        if (!hasNext()) throw NoSuchElementException()
        return shrubberies[index++]
    }
}

fun main() {
    val shrubberyList = ShrubberyList(listOf("Rose", "Lilac", "Tulip"))

    val iterator = shrubberyList.createIterator()
    while (iterator.hasNext()) {
        val shrubbery = iterator.next()
        println(shrubbery)
    }
}

In this example, we define a ShrubberyCollection interface that represents the aggregate object. This interface has a single method, createIterator(), which returns a ShrubberyIterator object.

We then create a ShrubberyIterator interface, which defines two methods: hasNext() and next(). The hasNext() method checks if there is a next element in the collection, and the next() method returns the next element.

Next, we create a concrete aggregate, ShrubberyList, which implements the ShrubberyCollection interface. It holds a list of shrubberies and creates a ShrubberyListIterator when the createIterator() method is called.

The ShrubberyListIterator is a concrete iterator that implements the ShrubberyIterator interface. It takes the list of shrubberies as a parameter and provides the hasNext() and next() methods to traverse the list.

In the main function, we create an instance of ShrubberyList with a list of shrubberies, create an iterator using the createIterator() method, and then use the iterator to traverse the shrubberies and print them.

In Kotlin, you can use built-in iterators for most collections, so you don't usually need to implement the Iterator pattern from scratch like in this example. However, this example demonstrates the core concept of the pattern and how you can implement it if needed for a custom collection.

Mediator

The Mediator pattern is a behavioral design pattern that defines an object that encapsulates how a set of objects interact. This pattern is used to promote loose coupling between objects by keeping them from referring to each other explicitly, and it lets you vary their interaction independently.

// Mediator interface
interface ShrubberyMediator {
    fun notify(sender: ShrubberyComponent, event: String)
}

// Abstract component
abstract class ShrubberyComponent(val mediator: ShrubberyMediator) {
    fun changed(event: String) {
        mediator.notify(this, event)
    }
}

// Concrete components
class ShrubberyPicker(mediator: ShrubberyMediator) : ShrubberyComponent(mediator) {
    fun pickShrubbery(shrubbery: String) {
        println("Shrubbery picked: $shrubbery")
        changed("PICKED")
    }
}

class ShrubberyProcessor(mediator: ShrubberyMediator) : ShrubberyComponent(mediator) {
    fun processShrubbery() {
        println("Processing shrubbery...")
        changed("PROCESSED")
    }
}

// Concrete mediator
class ShrubberyManager : ShrubberyMediator {
    private val picker = ShrubberyPicker(this)
    private val processor = ShrubberyProcessor(this)

    override fun notify(sender: ShrubberyComponent, event: String) {
        when (event) {
            "PICKED" -> processor.processShrubbery()
            "PROCESSED" -> println("Shrubbery processed!")
        }
    }

    fun start() {
        picker.pickShrubbery("Rose")
    }
}

fun main() {
    val shrubberyManager = ShrubberyManager()
    shrubberyManager.start()
}

In this example, we define a ShrubberyMediator interface, which represents the mediator. The interface has a single method, notify(), which is called when a component has an event that may require interaction with other components.

We create an abstract ShrubberyComponent class that represents the components that will interact through the mediator. The class takes a ShrubberyMediator as a parameter and has a changed() method that notifies the mediator of an event.

We create two concrete components, ShrubberyPicker and ShrubberyProcessor. These components inherit from ShrubberyComponent and implement their own behavior. The ShrubberyPicker has a pickShrubbery() method that picks a shrubbery and notifies the mediator, while the ShrubberyProcessor has a processShrubbery() method that processes a shrubbery and also notifies the mediator.

We then create a concrete mediator, ShrubberyManager, which implements the ShrubberyMediator interface. The manager holds instances of the ShrubberyPicker and ShrubberyProcessor and implements the notify() method to handle their interactions. In this example, when the picker picks a shrubbery, the processor processes it.

Finally, in the main function, we create an instance of ShrubberyManager and call the start() method, which starts the picking and processing of a shrubbery. The interactions between the picker and the processor are handled by the mediator, keeping them loosely coupled.

Memento

The Memento pattern is a behavioral design pattern that provides the ability to restore an object to its previous state, which is useful for implementing undo/redo functionality or creating snapshots of the object's state.

// Memento class
data class ShrubberyMemento(val height: Double)

// Originator class
class Shrubbery(var height: Double) {
    fun save(): ShrubberyMemento {
        return ShrubberyMemento(height)
    }

    fun restore(memento: ShrubberyMemento) {
        height = memento.height
    }
}

// Caretaker class
class ShrubberyCaretaker {
    private val mementos = mutableListOf<ShrubberyMemento>()

    fun saveState(shrubbery: Shrubbery) {
        mementos.add(shrubbery.save())
    }

    fun restoreState(shrubbery: Shrubbery, index: Int) {
        if (index < mementos.size) {
            shrubbery.restore(mementos[index])
        }
    }
}

fun main() {
    val shrubbery = Shrubbery(2.0)
    val caretaker = ShrubberyCaretaker()

    caretaker.saveState(shrubbery)
    println("Initial height: ${shrubbery.height}")

    shrubbery.height = 3.5
    caretaker.saveState(shrubbery)
    println("Updated height: ${shrubbery.height}")

    caretaker.restoreState(shrubbery, 0)
    println("Restored height: ${shrubbery.height}")
}

In this example, we define a ShrubberyMemento class that stores the state of a Shrubbery object. The Shrubbery class represents the originator, which has a mutable height property. It has two methods, save() and restore(), which are responsible for saving the current state and restoring a previous state from a memento, respectively.

The ShrubberyCaretaker class is responsible for managing the mementos. It stores the mementos in a list and provides methods saveState() and restoreState() to save and restore the state of a Shrubbery object.

In the main function, we create a Shrubbery object and a ShrubberyCaretaker. We then save the initial state of the shrubbery and modify its height. After that, we save the updated state and finally restore the initial state using the caretaker. The output of the program demonstrates the changes and restoration of the shrubbery's height.

Observer

The Observer pattern is a behavioral design pattern that defines a one-to-many dependency between objects. When one object (the subject) changes its state, all its dependent objects (the observers) are notified and updated automatically. This pattern is useful when you need to maintain consistency between related objects without making them tightly coupled.

// Observer interface
interface ShrubberyObserver {
    fun update(newHeight: Double)
}

// Concrete Observer class
class ShrubberyDisplay : ShrubberyObserver {
    private var height: Double = 0.0

    override fun update(newHeight: Double) {
        height = newHeight
        display()
    }

    fun display() {
        println("Shrubbery height: $height")
    }
}

// Subject class
class Shrubbery {
    private val observers = mutableListOf<ShrubberyObserver>()
    var height: Double = 0.0
        set(value) {
            field = value
            notifyObservers()
        }

    fun addObserver(observer: ShrubberyObserver) {
        observers.add(observer)
    }

    fun removeObserver(observer: ShrubberyObserver) {
        observers.remove(observer)
    }

    private fun notifyObservers() {
        for (observer in observers) {
            observer.update(height)
        }
    }
}

fun main() {
    val shrubbery = Shrubbery()
    val display = ShrubberyDisplay()

    shrubbery.addObserver(display)

    shrubbery.height = 2.0
    shrubbery.height = 3.5
}

In this example, we define a ShrubberyObserver interface that represents the observer. The ShrubberyDisplay class is a concrete observer that implements the ShrubberyObserver interface. It has a height property and two methods: update() to update its state when notified and display() to display the current height.

The Shrubbery class represents the subject. It maintains a list of observers and a height property. When the height property is changed, the notifyObservers() method is called, which iterates through the list of observers and calls their update() methods with the new height value.

In the main function, we create a Shrubbery object and a ShrubberyDisplay object. We then register the display as an observer of the shrubbery. When we change the height of the shrubbery, the display is notified and updates its state to reflect the new height.

State

The State pattern is a behavioral design pattern that allows an object to change its behavior when its internal state changes. Instead of using conditionals to manage state-dependent behavior, the State pattern delegates this behavior to separate state objects.

// State interface
interface ShrubberyState {
    fun grow(shrubbery: Shrubbery)
}

// Concrete State classes
class SmallShrubberyState : ShrubberyState {
    override fun grow(shrubbery: Shrubbery) {
        println("Growing from small to medium")
        shrubbery.currentState = MediumShrubberyState()
    }
}

class MediumShrubberyState : ShrubberyState {
    override fun grow(shrubbery: Shrubbery) {
        println("Growing from medium to large")
        shrubbery.currentState = LargeShrubberyState()
    }
}

class LargeShrubberyState : ShrubberyState {
    override fun grow(shrubbery: Shrubbery) {
        println("Cannot grow further, already large")
    }
}

// Context class
class Shrubbery {
    var currentState: ShrubberyState = SmallShrubberyState()

    fun grow() {
        currentState.grow(this)
    }
}

fun main() {
    val shrubbery = Shrubbery()

    shrubbery.grow() // Growing from small to medium
    shrubbery.grow() // Growing from medium to large
    shrubbery.grow() // Cannot grow further, already large
}

In this example, we define a ShrubberyState interface that represents the state. It has a single method, grow(), which takes a Shrubbery object as a parameter. We also define three concrete state classes that implement the ShrubberyState interface: SmallShrubberyState, MediumShrubberyState, and LargeShrubberyState. Each concrete state class defines the grow() method to change the shrubbery's state and print a message.

The Shrubbery class represents the context. It maintains a reference to the current state object through the currentState property. It also provides a grow() method, which delegates the growing behavior to the current state object.

In the main function, we create a Shrubbery object with an initial state of SmallShrubberyState. We then call the grow() method three times. Each time the method is called, the behavior changes based on the current state of the shrubbery.

Strategy

The Strategy pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. The Strategy pattern lets the algorithm vary independently from clients that use it.

// Strategy interface
interface PruningStrategy {
    fun prune(): String
}

// Concrete Strategy classes
class LightPruningStrategy : PruningStrategy {
    override fun prune(): String {
        return "Light pruning"
    }
}

class ModeratePruningStrategy : PruningStrategy {
    override fun prune(): String {
        return "Moderate pruning"
    }
}

class HeavyPruningStrategy : PruningStrategy {
    override fun prune(): String {
        return "Heavy pruning"
    }
}

// Context class
class Shrubbery(private var pruningStrategy: PruningStrategy) {
    fun setPruningStrategy(strategy: PruningStrategy) {
        pruningStrategy = strategy
    }

    fun performPruning(): String {
        return pruningStrategy.prune()
    }
}

fun main() {
    val shrubbery = Shrubbery(LightPruningStrategy())
    println(shrubbery.performPruning()) // Light pruning

    shrubbery.setPruningStrategy(ModeratePruningStrategy())
    println(shrubbery.performPruning()) // Moderate pruning

    shrubbery.setPruningStrategy(HeavyPruningStrategy())
    println(shrubbery.performPruning()) // Heavy pruning
}

In this example, we define a PruningStrategy interface that represents the strategy. It has a single method, prune(), which returns a String describing the pruning process. We also define three concrete strategy classes that implement the PruningStrategy interface: LightPruningStrategy, ModeratePruningStrategy, and HeavyPruningStrategy. Each concrete strategy class defines the prune() method with a specific behavior.

The Shrubbery class represents the context. It maintains a reference to the current strategy object through the pruningStrategy property. It provides a setPruningStrategy() method to change the strategy at runtime and a performPruning() method, which delegates the pruning behavior to the current strategy object.

In the main function, we create a Shrubbery object with an initial strategy of LightPruningStrategy. We then call the performPruning() method to print the pruning process. After that, we change the strategy using the setPruningStrategy() method and call performPruning() again to demonstrate how the behavior changes based on the current strategy.

Template Method

The Template Method pattern is a behavioral design pattern that defines the skeleton of an algorithm in a base class but lets subclasses override specific steps of the algorithm without changing its structure.

// Abstract base class with the template method
abstract class ShrubberyCare {

    fun care() {
        prepareTools()
        prune()
        water()
        cleanUp()
    }

    protected open fun prepareTools() {
        println("Preparing tools for shrubbery care.")
    }

    protected abstract fun prune()

    protected open fun water() {
        println("Watering the shrubbery.")
    }

    protected open fun cleanUp() {
        println("Cleaning up after shrubbery care.")
    }
}

// Concrete classes
class LightCareShrubbery : ShrubberyCare() {
    override fun prune() {
        println("Performing light pruning.")
    }
}

class HeavyCareShrubbery : ShrubberyCare() {
    override fun prune() {
        println("Performing heavy pruning.")
    }

    override fun cleanUp() {
        println("Cleaning up thoroughly after heavy shrubbery care.")
    }
}

fun main() {
    val lightCareShrubbery = LightCareShrubbery()
    val heavyCareShrubbery = HeavyCareShrubbery()

    println("Light care:")
    lightCareShrubbery.care()

    println("\nHeavy care:")
    heavyCareShrubbery.care()
}

In this example, we define an abstract base class ShrubberyCare that represents the template method. The care() method is the template method that consists of several steps: prepareTools(), prune(), water(), and cleanUp(). Some of these steps are defined as abstract methods, like prune(), which means subclasses must provide an implementation for them. Other steps have a default implementation but are marked as open, allowing subclasses to override them if needed.

We define two concrete subclasses, LightCareShrubbery and HeavyCareShrubbery, which inherit from ShrubberyCare. Both subclasses provide their own implementation of the prune() method. The HeavyCareShrubbery class also overrides the cleanUp() method to provide a different cleanup behavior.

In the main function, we create instances of LightCareShrubbery and HeavyCareShrubbery and call their care() methods. This demonstrates how the template method ensures that the algorithm's structure remains the same while allowing subclasses to provide their own implementations of specific steps.

Visitor

The Visitor pattern is a behavioral design pattern that allows you to separate algorithms from the objects on which they operate. It lets you define a new operation without changing the classes of the objects you work with.

// Element interface
interface Shrubbery {
    fun accept(visitor: ShrubberyVisitor)
}

// Concrete elements
class OakShrubbery : Shrubbery {
    fun oakSpecificOperation() {
        println("Performing an operation specific to Oak shrubbery.")
    }

    override fun accept(visitor: ShrubberyVisitor) {
        visitor.visitOakShrubbery(this)
    }
}

class PineShrubbery : Shrubbery {
    fun pineSpecificOperation() {
        println("Performing an operation specific to Pine shrubbery.")
    }

    override fun accept(visitor: ShrubberyVisitor) {
        visitor.visitPineShrubbery(this)
    }
}

// Visitor interface
interface ShrubberyVisitor {
    fun visitOakShrubbery(oakShrubbery: OakShrubbery)
    fun visitPineShrubbery(pineShrubbery: PineShrubbery)
}

// Concrete visitor
class ShrubberyCareVisitor : ShrubberyVisitor {
    override fun visitOakShrubbery(oakShrubbery: OakShrubbery) {
        println("Caring for an Oak shrubbery.")
        oakShrubbery.oakSpecificOperation()
    }

    override fun visitPineShrubbery(pineShrubbery: PineShrubbery) {
        println("Caring for a Pine shrubbery.")
        pineShrubbery.pineSpecificOperation()
    }
}

fun main() {
    val shrubberies: List<Shrubbery> = listOf(OakShrubbery(), PineShrubbery())
    val visitor = ShrubberyCareVisitor()

    for (shrubbery in shrubberies) {
        shrubbery.accept(visitor)
    }
}

In this example, we have an Shrubbery interface representing the element interface with an accept method. We also have two concrete elements, OakShrubbery and PineShrubbery, which implement the Shrubbery interface and their own specific operations.

The ShrubberyVisitor interface is the visitor interface with methods for each type of concrete element. We have a ShrubberyCareVisitor class implementing the ShrubberyVisitor interface, which defines how to care for different types of shrubberies.

In the main function, we create a list of Shrubbery objects and a ShrubberyCareVisitor object. We iterate through the list of shrubberies and call their accept method with the visitor. This allows the visitor to perform operations on the different types of shrubberies without modifying their classes.

The Visitor pattern is particularly useful when you want to perform various operations on a set of objects with different classes and don't want to pollute their classes with methods unrelated to their primary responsibilities.

Conclusion

In conclusion, software design patterns are reusable solutions to common problems that arise in software design. They provide a shared language and best practices that can improve the efficiency and maintainability of your code. Design patterns can be classified into three main categories: creational, structural, and behavioral.

Creational patterns focus on the process of object creation, providing mechanisms to create objects in a flexible and efficient manner. Examples include the Abstract Factory, Builder, Factory Method, Object Pool, Prototype, and Singleton patterns.

Structural patterns deal with the composition of classes and objects to form larger structures. They help ensure that the architecture of your software is scalable and robust. Examples of structural patterns include the Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy patterns.

Behavioral patterns define the ways in which objects interact and communicate with one another. They help to improve the flexibility of communication between objects and to better manage complex interactions. Examples of behavioral patterns are the Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, and Visitor patterns.

By understanding and applying design patterns, developers can create more flexible, reusable, and maintainable software. It's essential to remember that design patterns should not be treated as one-size-fits-all solutions; instead, they should be considered as a toolbox of techniques that can be adapted to the specific requirements of your projects. Always keep in mind the context and trade-offs of each pattern, and use them judiciously to enhance the quality and readability of your code.

Subscribe to rohp

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe