Last active
September 4, 2018 21:00
-
-
Save mttkay/01ae7141e1a2c8b28c56d746a3782968 to your computer and use it in GitHub Desktop.
Passing functions, not objects as collaborators
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Coffee | |
class Order | |
class CoffeeMaker { | |
def startCoffee(): Unit = ??? | |
def isCoffeeReady: Boolean = ??? | |
def dispenseCoffee: Coffee = ??? | |
} | |
val coffeeMaker = new CoffeeMaker | |
// Object oriented solution | |
// Note that here, the waiter only needs 2 out of the 3 methods defined | |
// by CoffeeMachine. This exposes unnecessary complexity to a dependent | |
// object, which we need to account for in refactorings or unit tests. | |
class OOWaiter(coffeeMaker: CoffeeMaker) { | |
def serveCoffee: Option[Coffee] = | |
if (coffeeMaker.isCoffeeReady) | |
Some(coffeeMaker.dispenseCoffee) | |
else | |
None | |
} | |
// Note how we _must_ pass a CoffeeMaker instance to the waiter; it will | |
// never work with any other construct, unless we come up with convoluted | |
// interface hierarchies to generalize this, which we all know ends in pain. | |
// Favor composition over inheritance. | |
val ooWaiter = new OOWaiter(coffeeMaker) | |
// Functional solution | |
// This solution is superior in basically every way: we explicitly declare | |
// _only_ those dependencies we really need: | |
// 1 - a way to know that coffee is available, and | |
// 2 - a way to obtain it (we completely abstract away how though) | |
class FPWaiter(isCoffeeReady: () => Boolean, | |
obtainCoffee: () => Coffee) { | |
def serveCoffee: Option[Coffee] = | |
if (isCoffeeReady()) | |
Some(obtainCoffee()) | |
else | |
None | |
} | |
// We can still rely just on the CoffeeMaker; however, the dependent object (the waiter) | |
// will have zero knowledge of that fact. | |
val fpWaiter = new FPWaiter(coffeeMaker.isCoffeeReady _, coffeeMaker.dispenseCoffee _) | |
// Suppose now we hire an intern whose job it is to operate the coffee machine, and | |
// dispense coffee into a pot when it's done. | |
object Intern { | |
def dispenseCoffee: Coffee = ??? | |
} | |
// We can now rely on a different way to obtain coffee, namely through the coffee pot | |
// instead of from the machine directly, without changing a single line of code in | |
// the waiter class. | |
val fpLazyWaiter = new FPWaiter(coffeeMaker.isCoffeeReady _, Intern.dispenseCoffee _) | |
// Assume now we're writing a unit test. It is extremly easy to stub out this behavior: | |
def waiterUnderTest(coffeeReady: Boolean) = new FPWaiter( | |
() => coffeeReady, () => new Coffee | |
) | |
val coffeeServed: Option[Coffee] = waiterUnderTest(coffeeReady = true).serveCoffee | |
// Given coffeeReady == true | |
// Then coffeeServed == Some(coffee) | |
val noCoffeeServed: Option[Coffee] = waiterUnderTest(coffeeReady = false).serveCoffee | |
// Given coffeeReady == false | |
// Then coffeeServed == None |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment