Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Smash your adapter monolith with the Connect pattern

Smash your adapter monolith with the Connect pattern

Server-side code is regularly broken down into manageable chunks, based around endpoints. As a result it tends to be nicely factored. But over several projects we noticed that the same was not true of adapters that talk to 3rd party systems. These pieces of code tended to grow uncontrolled, and were not given the same level of attention.

In this talk, I'll be covering a pattern that we discovered to help break down your Adapter Monolith into modular, easily digestible and (most importantly) testable pieces.

This talk is based in Kotlin and takes advantage of language features such as Data Classes, Companion Objects, Operator Overloading and Extension Functions. However the pattern concepts themselves are applicable to any technology choice or programming model.

Ed250c56ee7dac44d90e5fdf712314f2?s=128

David Denton

March 19, 2021
Tweet

Transcript

  1. SMASH YOUR ADAPTER MONOLITH WITH THE CONNECT PATTERN David Denton

  2. HI, I’M DAVID! TRAINER OPEN SOURCE ENGINEERING LEAD SPEAKER

  3. relevant kotlin features • extension functions • kotlin invoke() ==

    java apply() • data classes == immutable records • companion object == static “hook”
  4. /endpoint2 /endpoint1 /endpoint3 Business logic API Adapter API Adapter Data

    access /endpoint4 API Adapter a typical http microservice /endpoint2 /endpoint1 /endpoint3 /endpoint4
  5. fun MySecureApp(): HttpHandler = BearerAuth("my-very-secure-and-secret-bearer-token") .then( routes( echo(), health() )

    ) fun echo() = "/echo" bind POST to { req: Request -> Response(OK).body(req.bodyString()) } fun health() = "/health" bind GET to { req: Request -> Response(OK).body("alive!") } val server = MySecureApp().asServer(Netty(8080)).start() server-side
  6. /endpoint2 /endpoint1 /endpoint3 Business logic API Adapter API Adapter Data

    access /endpoint4 API Adapter a typical http microservice API Adapter API Adapter API Adapter
  7. class GitHubApi(client: HttpHandler) { private val http = SetBaseUriFrom(Uri.of("https://api.github.com")) .then(SetHeader("Accept",

    "application/vnd.github.v3+json")) .then(client) fun getUser(username: String): UserDetails { val response = http(Request(GET, "/users/$username")) return UserDetails(userNameFrom(response), userOrgsFrom(response)) } fun getRepoLatestCommit(owner: String, repo: String) = .... } val gitHub: GitHubApi = GitHubApi(OkHttp()) val user: UserDetails = gitHub.getUser("octocat") the adapter monolith…
  8. introducing connect • split monolithic adapters • testable & composable

    • simple
  9. action Action HTTP Req HTTP Resp Domain Req Domain Resp

  10. interface GitHubApiAction<R> { fun toRequest(): Request fun fromResponse(response: Response): R

    } data class GetUser(val username: String) : GitHubApiAction<UserDetails> { override fun toRequest() = Request(GET, “/users/$username") override fun fromResponse(response: Response) = UserDetails(userNameFrom(response), userOrgsFrom(response)) } data class UserDetails(val name: String, val orgs: List<String>) declaring actions
  11. adapter Adapter Domain Response Action http client

  12. interface GitHubApi { operator fun <R : Any> invoke(action: GitHubApiAction<R>):

    R companion object } define the adapter
  13. fun GitHubApi.Companion.Http(client: HttpHandler) = object : GitHubApi { private val

    http = SetBaseUriFrom(Uri.of(“https://api.github.com")) .then(SetHeader("Accept", "application/vnd.github.v3+json")) .then(client) override fun <R : Any> invoke(action: GitHubApiAction<R>) = action.fromResponse(http(action.toRequest())) } val gitHub: GitHubApi = GitHubApi.Http(OkHttp()) val user: UserDetails = gitHub(GetUser("octocat")) reimplementing the adapter
  14. fun GitHubApi.getUser(username: String): UserDetails = invoke(GetUser(username)) fun GitHubApi.getLatestRepoCommit(owner: String, repo:

    String): Commit = invoke(GetRepoLatestCommit(owner, repo)) val user: UserDetails = gitHub.getUser(“octocat") recreating our api
  15. fun GitHubApi.getLatestUser(org: String, repo: String): UserDetails { val commit =

    getLatestRepoCommit(org, repo) return getUser(commit.author) } val latestUser: UserDetails = gitHub.getLatestUser("http4k", "http4k-connect") composite actions
  16. testing the connect pattern

  17. @Test fun `translates request`() { assertThat(GetUser("foobar").toRequest(), equalTo(Request(GET, "/users/foobar"))) } @Test

    fun `translates response`() { assertThat(GetUser("foobar").fromResponse(Response(OK).body("foobar/admin,mgmt")), equalTo(UserDetails("foobar", listOf("admin", "mgmt")))) } testing actions
  18. @Test fun `get user details`() { val githubApi = mockk<GitHubApi>()

    val userDetails = UserDetails("bob", listOf("http4k")) every { githubApi(any<GetUser>()) } returns userDetails assertThat(githubApi.getUser("bob"), equalTo(userDetails)) } mock the adapter
  19. class StubGitHubApi(private val users: Map<String, UserDetails>) : GitHubApi { override

    fun <R : Any> invoke(action: GitHubApiAction<R>): R = when (action) { is GetUser -> getUser(action, users) as R is GetRepoLatestCommit -> getRepoLatestCommit(action) as R else -> throw UnsupportedOperationException() } } private fun getUser(action: GetUser, users: Map<String, UserDetails>) = users[action.username] private fun getRepoLatestCommit(action: GetRepoLatestCommit) = Commit(action.owner) stub it
  20. class RecordingGitHubApi(private val delegate: GitHubApi) : GitHubApi { val recorded

    = mutableListOf<GitHubApiAction<*>>() override fun <R : Any> invoke(action: GitHubApiAction<R>): R { recorded += action return delegate(action) } } decorate it
  21. vary your programming model

  22. interface GitHubApiAction<R> { fun toRequest(): Request fun fromResponse(response: Response): Result<R,

    Exception> } data class GetUser(val username: String) : GitHubApiAction<UserDetails> { override fun toRequest() = Request(GET, "/users/$username") override fun fromResponse(response: Response) = when { response.status.successful -> Success(UserDetails( userNameFrom(response), userOrgsFrom(response)) ) else -> Failure(RuntimeException("Status: " + response.status)) } } result4k
  23. summary • simple • testable and composable • flexible on

    model • works with any req/resp protocol
  24. • featherweight API adapters • in-memory fakes / storage

  25. THANKS! @daviddenton https://dentondav.id hello@dentondav.id