mardi 30 juin 2020

Is this Scala / Functional Programming pattern to avoid OOP classes, an official design pattern? Does it have a name?

Classes in OOP couple variables to methods via a scope, thus making refactors painful as we cannot easily move methods from one place into another. The entire dependency graph, with initialisation, of the class must be moved with the method somehow. A common "solution" in OOP is dependency injection frameworks.

E.g. in OOP

class Foo(client: Client) {
  def dumbCouplingOfMethodToScope: Unit = client.send("hello world")
}

In Scala the solution to this is to use implicit parameters, usually called a "Context". E.g.

object Foo {
  def methodNotDependOnScope(client: Client): Unit = client.send("hello world")
}

But a problem occurs when we want to do dynamic dispatch, i.e. be able to pass in multiple implementations of the Context (usually for testing), and when the Context itself depends on another Context. E.g.

trait MethodsNeedContext[Context] {
  def method1(p1: Int)(implicit context: Context): Int
}

object ExampleUsage {
  def callsMethod1(implicit contextWithContext: MethodsNeedContext[???], contextsContext: ???): Unit = {
    contextWithContext.method1(10)(contextsContext)
  }
}

We only know the type ??? at runtime, not at compile time. The OOP way would be to make MethodsNeedContext an abstract class, then we can use an existential type. E.g.

abstract class OOPWay[Context <: ContextTypeBound](context: Context) {
  def method1(p1: Int): Int = context.send("hello world")
}

object ExampleUsage2 {
  def callsMethod1(implicit contextWithContext: OOPWay[_ <: ContextTypeBound]): Unit = {
    contextWithContext.method1(10)
  }
}

But if we wrap the contextWithContext and contextContext into a case class, then add an implicit class then we can solve the problem. Full example below:

trait ContextTypeBound

trait MethodsNeedContext[Context <: ContextTypeBound] {
  def method1(p1: Int)(implicit ctx: Context): Int
  def method2(p2: String)(implicit ctx: Context): String
}

case class MethodsWithContext[T <: ContextTypeBound](methods: MethodsNeedContext[T], context: T)

object Pimps {
  implicit class PimpedMethodsWithContext[T <: ContextTypeBound](methods: MethodsWithContext[T]) {
    def method1(p1: Int): Int = methods.methods.method1(p1)(methods.context)
  }
}

import Pimps.PimpedMethodsWithContext

case class FooContext(foo: Int) extends ContextTypeBound

object FooExample extends MethodsNeedContext[FooContext] {
  def method1(p1: Int)(implicit ctx: FooContext): Int = ???
  def method2(p2: String)(implicit ctx: FooContext): String = ???
}

case class BobContext(foo: Int) extends ContextTypeBound

object BobExample extends MethodsNeedContext[BobContext] {
  def method1(p1: Int)(implicit ctx: BobContext): Int = ???
  def method2(p2: String)(implicit ctx: BobContext): String = ???
}

object ExampleUsage {
  def callsMethod1(implicit contextWithContext: MethodsWithContext[_ <: ContextTypeBound]): Unit = {
    contextWithContext.method1(10)
  }

  callsMethod1(MethodsWithContext(FooExample, FooContext(20)))
  // Does not compile
  callsMethod1(MethodsWithContext(BobExample, FooContext(20)))
}

My question(s) are:

  1. Does this pattern have a name? Is it a standard Scala practice?
  2. Is this a Scala only thing, or do other FP languages have similar mechanisms to do the same thing? If other FP languages have different mechanisms, what are they?

Aucun commentaire:

Enregistrer un commentaire