Strategy Design Pattern in Scala

1. Definition

The Strategy Design Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

2. Problem Statement

Imagine you have multiple ways of performing a particular operation, and you want to switch between them dynamically. Using traditional methods, you might end up with a messy code full of conditionals. Instead, a more flexible and cleaner way to manage these multiple algorithms is needed.

3. Solution

The Strategy Pattern suggests defining a family of algorithms, encapsulating each one in a separate class, and making them interchangeable. Typically, this pattern involves a context class that holds a reference to a strategy object and delegates the task to the strategy object.

4. Real-World Use Cases

1. Selecting different compression algorithms for file storage.

2. Various payment methods in an e-commerce system.

3. Different rendering techniques in a graphics editor.

5. Implementation Steps

1. Define a Strategy interface that encapsulates the behavior that varies.

2. Implement concrete Strategy classes that implement the Strategy interface.

3. Define a Context class that delegates to a Strategy object.

6. Implementation in Scala Programming

// Step 1: Define the Strategy trait
trait SortingStrategy {
  def sort(data: List[Int]): List[Int]
}
// Step 2: Implement concrete Strategy classes
class BubbleSort extends SortingStrategy {
  def sort(data: List[Int]): List[Int] = {
    // A simplistic bubble sort implementation
    def bubbleSort(arr: List[Int]): List[Int] = {
      if (arr.length <= 1) arr
      else {
        val (head, tail) = arr.splitAt(2)
        if (head(0) > head(1)) bubbleSort(head.reverse ::: bubbleSort(tail))
        else head ::: bubbleSort(tail)
      }
    }
    bubbleSort(data)
  }
}
class QuickSort extends SortingStrategy {
  def sort(data: List[Int]): List[Int] = {
    // A basic quick sort implementation
    if (data.length <= 1) data
    else {
      val pivot = data.head
      val (less, greater) = data.tail.partition(_ < pivot)
      sort(less) ::: pivot :: sort(greater)
    }
  }
}
// Step 3: Define the Context class
class NumberList(strategy: SortingStrategy) {
  def sort(data: List[Int]): List[Int] = strategy.sort(data)
}
// Client and Tester
object StrategyClient extends App {
  val data = List(3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5)
  val bubbleSorted = new NumberList(new BubbleSort).sort(data)
  val quickSorted = new NumberList(new QuickSort).sort(data)
  println(s"Bubble Sorted: $bubbleSorted")
  println(s"Quick Sorted: $quickSorted")
}

Output:

Bubble Sorted: List(1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9)
Quick Sorted: List(1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9)

Explanation:

1. The Strategy trait SortingStrategy provides a contract that all concrete strategies must follow.

2. The concrete strategy classes BubbleSort and QuickSort provide different implementations for sorting algorithms.

3. The context NumberList class is designed to work with any strategy that implements the SortingStrategy trait. When the context needs to perform the sort operation, it delegates the task to the encapsulated strategy object.

4. The client code demonstrates the flexibility of the Strategy pattern. Depending on the need, the client can switch between bubble sort and quick sort dynamically.

7. When to use?

Use the Strategy pattern when:

1. Many related classes differ only in their behavior. Strategies provide a way to configure a class with one of many behaviors.

2. You need different variants of an algorithm.

3. An algorithm uses data that clients shouldn't know about. Use the Strategy pattern to avoid exposing complex, algorithm-specific data structures.

4. A class defines many behaviors, and these appear as multiple conditional statements in its operations. Instead of many conditionals, move related conditional branches into their own Strategy class.


Comments