Part 3 of Obsolete Design Patterns. For this post I’m going to use to Scala 3. You can go here for the code translated to Java.

Strategy Pattern Link to heading

This pattern allows you to create a family of algorithms as a collection of classes so you can interchange the algorithms at runtime as needed.

Imagine you want to support 3 different payment types in your application, credit card, paypal, and bitcoin. You could use the strategy pattern to do this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
trait PaymentStrategy:
  def execute(amount: Double): Unit

class CreditCardPayment(private val cardNumber: String) 
    extends PaymentStrategy:
  def execute(amount: Double) = 
    println(s"paid $amount using the cc $cardNumber")

class PaypalPayment(private val email: String) 
    extends PaymentStrategy:
  def execute(amount: Double) = 
    println(s"paid $amount using paypal $email")

class BitcoinPayment(private val walletAddress: String) 
    extends PaymentStrategy:
  def execute(amount: Double) = 
    println(s"paid $amount using the wallet $walletAddress")

class PaymentContext:
  private var strategy: PaymentStrategy

  def setStrategy(strategy: PaymentStrategy) =
    this.strategy = strategy

  def executeStrategy(double amount) =
    strategy.execute(amount)

// usage - the `DetailsFromUser` functions are of type `() => String`
val paymentContext = PaymentContext()
paymentContext.setStrategy(someValidatedUserInput match
  case "credit card" => CreditCardPayment(DetailsFromUser.creditCard())
  case "paypal" => PaypalPayment(DetailsFromUser.paypal())
  case "bitcoin" => BitcoinPayment(DetailsFromUser.bitcoin())
  case _ => throw RuntimeException("user input invalid")
)

paymentContext.executeStrategy(400)

You can now pass around the paymentContext and execute values against the strategy. You can also change the strategy of the context as needed, and it will change for all users of the context (assuming you’ve been passing around the same reference).

The FP approach Link to heading

A strategy interface is really just a glorified function type. The implementations importantly have two stages of application, the constructor application and the method application. In FP you instead curry functions or use multiple argument lists to separate stages of application. This makes a PaymentStrategy analogous to String => Double => Unit.

Another interesting bit is the PaymentContext. Its purpose in the strategy pattern is to allow payment strategies to be changed across a large part of the codebase at the same time, so long as they all have reference to the context.

Generally it’s discouraged to share this kind of mutable state freely within FP, normally you’d just pass around the immutable, partially applied payment function directly. However, there can be some cases where there is a change in information at runtime and you want that change propogated out across large parts of your codebase, and maybe even across threads! Sharing mutable state across threads is notoriously difficult in imperative environments, requiring manual synchronization, locks, compare-and-swaps, etc. The below Scala will be thread safe by default and is easily parallelizable since we’re using an effect system, cats-effect.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def creditCardPayment(cardNumber: String)(amount: Double): IO[Unit] =
  IO.println(s"paid $amount using the cc $cardNumber")

def paypalPayment(email: String)(amount: Double): IO[Unit] =
  IO.println(s"paid $amount using paypal user $email")

def bitcoinPayment(walletAddress: String)(amount: Double): IO[Unit] =
  IO.println(s"paid $amount using the wallet $walletAddress")

// usage - the `DetailsFromUser` functions are of type `IO[String]`
for 
  payment <- someValidatedUserInput match
    case "credit card" => DetailsFromUser.creditCard.map(creditCardPayment)
    case "paypal" => DetailsFromUser.paypal.map(paypalPayment)
    case "bitcoin" => DetailsFromUser.bitcoin.map(bitcoinPayment) 
    case _ => IO.raiseError(RuntimeException("user input invalid"))

  _ <- payment(400)
  paymentRef <- Ref[IO].of(payment) // safe, concurrent mutable ref
yield ()

Like mentioned before, if you can achieve what you want by just passing around payment, and not making/using a paymentRef, it’s ideal to do that. Though for feature-parity with the strategy pattern I wanted to showcase how you would make a mutable ref if needed (that is better than the original because it’s a nonblocking, lightweight concurrent mutable ref).

Digression on function application Link to heading

Firstly, here is generalized version of the OO approach to partial application:

class SomePayment(private val userDetails: String) extends PaymentStrategy:
  def execute(amount: Double) = ???

SomePayment("yeetus") // first stage of application
  .execute(400) // second stage of application

and the FP approach to partial application:

def somePayment(userDetails: String)(amount: Double) = ???

somePayment("yeetus") // first stage of application
  (400) // second stage of application

In the FP paradigm, it would be very easy to add more stages of application to the above code. Say another part of the app handles providing details about an organization the user is a part of:

def somePayment(userDetails: String)(orgDetails: String)(amount: Double) = ???

val p1 = somePayment("yeetus") // first stage of application
val p2 = p1("yacht") // second stage of application
p2(400) // third stage of application

Doing this in OO is less straightforward:

class SomePaymentUserDetails(private val userDetails: String):
  def execute(orgDetails: String) = SomePayment(userDetails, orgDetails)

class SomePayment(
    private val userDetails: String, 
    private val orgDetails: String
) extends PaymentStrategy:
  def execute(amount: Double) = ???

val p1 = SomePaymentUserDetails("yeetus") // first stage of application
val p2 = p1.execute("yacht") // second stage of application
p2.execute(400) // third stage of application

However you could make the use-site literally identical by using different names:

class somePayment(private val userDetails: String):
  def apply(orgDetails: String) =
    SomePaymentWithUserAndOrg(userDetails, orgDetails)

class SomePaymentWithUserAndOrg(
    private val userDetails: String, 
    private val orgDetails: String
) extends PaymentStrategy:
  def apply(amount: Double) = ???

// note the usage is identical to the FP version
val p1 = somePayment("yeetus") // first stage of application
val p2 = p1("yacht") // second stage of application
p2(400) // third stage of application