Last active
December 17, 2021 22:42
-
-
Save lgtout/2be831462785671845c283696e9fdc0f to your computer and use it in GitHub Desktop.
Eliminate indirection due to interfaces while preserving stubbing and enforcing a narrow API surface
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
@file:Suppress("UNREACHABLE_CODE") | |
package com.lagostout.extensions | |
interface SystemUnderTest { | |
companion object | |
} | |
//fun SystemUnderTest.f1(): String = "f1" + f2() // Test this first without testing f2 | |
//fun SystemUnderTest.f2(): String = "f2" // Test this second without testing f1 | |
interface C { | |
fun SystemUnderTest.f1(): String | |
fun SystemUnderTest.f2(): String | |
} | |
// Unfortunately, for testC3 to pass, we need to own C1, so we can make it "open". | |
open class C1 : C { | |
override fun SystemUnderTest.f1() = "f1" + f2() | |
override fun SystemUnderTest.f2() = "f2" // Pretend this is a slow call e.g. over HTTP. | |
} | |
//val c2 = object : ... | |
class C2(c1: C1) : C by c1 { | |
override fun SystemUnderTest.f2() = "f2-mocked" | |
} | |
class C3 : C1() { | |
override fun SystemUnderTest.f2() = "f2-mocked" | |
} | |
fun main() { | |
// testC2() | |
// testC3() | |
testD2() | |
// testD5() | |
} | |
fun testC2() { | |
val sut: SystemUnderTest = object : SystemUnderTest {} | |
val c1 = C1() | |
val c2 = C2(c1) | |
val r = c2.run { | |
sut.f1() | |
} | |
println(r) // Success!! "f1f2-mocked" | |
} | |
fun testC3() { | |
val sut: SystemUnderTest = object : SystemUnderTest {} | |
val c3 = C3() | |
val r = c3.run { | |
sut.f1() | |
} | |
println(r) // Fail!! "f1f2" | |
} | |
interface D1 { | |
fun SystemUnderTest.f1() = "f1" + f2() | |
// 1. Can we mock/stub this? | |
// 2. Can we exclude it from a public API? | |
fun SystemUnderTest.f2() = "f2" | |
} | |
object D2 : D1 { | |
override fun SystemUnderTest.f2() = "f2-mocked" | |
} | |
fun testD2() { | |
val sut: SystemUnderTest = object : SystemUnderTest {} | |
val d2 = D2 | |
val r = d2.run { | |
sut.f1() | |
} | |
println(r) // Success!! "f1f2-mocked" | |
} | |
class D3(private val d1: D1) { | |
private val sut = object : SystemUnderTest {} | |
fun f3() = with (d1) { | |
sut.f1() | |
sut.f2() // F*&%! How can we hide f2? | |
} | |
} | |
interface D4 { | |
fun SystemUnderTest.f1(): String | |
} | |
class D5(private val d4: D4) { | |
private val sut = object : SystemUnderTest {} | |
fun f3() = with (d4) { | |
sut.f1() | |
// sut.f2() // Compiler error!! f2 is hidden! | |
} | |
} | |
// ACHIEVED | |
// We now know how we can mock/stub an extension function without introducing a | |
// layer of indirection that multiplies the surface area of the code and slows | |
// us down as we try to navigate and understand it - Provide the extension function's | |
// implementation directly in the interface, instead of in a concrete implementation | |
// of the interface. | |
// BUT... | |
// By doing this, we've lost the ability to hide from clients of the API, any extension | |
// functions that shouldn't be part of the interface's public API. Can we remedy this? | |
// Option 1: Use delegation to hide extensions that shouldn't be | |
// exposed in the public API | |
class D6(d1: D1) : D4, D1 by d1 | |
fun testD5() { | |
val sut = object : SystemUnderTest {} | |
val d1 = object : D1 {} | |
val d6: D4 = D6(d1) | |
val r = with (d6) { | |
sut.f1() | |
// sut.f2() // Success!! Compiler error!! f2 is hidden! | |
} | |
println(r) | |
} | |
// Option 2: Use member visibility to hide the extension function that should be excluded | |
// from the public API | |
interface D7 { | |
fun SystemUnderTest.f1() = "f1" + f2() | |
// 1. Can we mock/stub this? | |
// 2. Can we exclude it from a public API? | |
// Option 2.a: Make f2 private | |
private fun SystemUnderTest.f2() = "f2" | |
// Option 2.b: Can we use protected? | |
//protected fun SystemUnderTest.f2() = "f2" // F@#$%!! protected cannot be used in an interface! | |
} | |
fun testD7() { | |
val sut = object : SystemUnderTest {} | |
val d1 = object : D7 {} | |
with (d1) { | |
sut.f1() // Okay | |
//sut.f2() // Success!! Compiler error!! f2 is hidden! | |
} | |
} | |
// Oh, oh! We can use private to exclude extension function from public API, but we lose the | |
// ability to stub. | |
object D8 : D7 { | |
// override fun SystemUnderTest.f2() = "f2-mocked" // F@#$%!! Cannot mock/stub f2! Compiler error! | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment