Abstraction

Packing lower level capabilities into more convenient APIs. See definition.

This is different from abstraction in that abstraction works to perform an operation using the lower level capabilities, but that operation could have been performed by working with the lower level capabilities individually i.e. no new functionality was revealed.

Here’s an example of a repository exposed with several facets, with a higher level NameChanger capability composed out of two lower level ones.

/**
  * Demonstrates exposing capabilities as facets of a repository, so individual elements are exposed.
  *
  * The capabilities use tagless final to show how you can use different effects with capabilities.
  *
  * For example, the Id effect is an identity, so a failure will cause an exception.
  *
  * The Try effect is a disjoint union with Exception, so a failure will return Failure(Exception) as a result.
  */
object RepositoryComposition {
  val ID = UUID.fromString("c31d34e2-5892-4a2d-9fd5-3ce2e0efedf7")

  import ItemRepository._

  val access = new ItemRepository.Access()

  def main(args: Array[String]): Unit = {
    val itemRepository = new ItemRepository()

    changeWithId(itemRepository)
    changeWithTry(itemRepository)
  }

  def changeWithId(itemRepository: ItemRepository): Unit = {
    val idNameChanger = new NameChanger[Id](
      access.finder(itemRepository),
      access.updater(itemRepository),
      _.map(identity)
    )
    val idResult = idNameChanger.changeName(ID, "new name")
    println(s"id result = $idResult")
  }

  def changeWithTry(itemRepository: ItemRepository): Unit = {
    val idFinder = access.finder(itemRepository)
    val tryFinder = new Finder[Try] {
      override def find(id: UUID): Try[Option[Item]] = Try(idFinder.find(id))
    }

    val idUpdater = access.updater(itemRepository)
    val tryUpdater = new Updater[Try] {
      override def update(item: Item): Try[UpdateResult] =
        Try(idUpdater.update(item))
    }

    val tryNameChanger = new NameChanger[Try](tryFinder, tryUpdater, {
      case Success(Some(result)) => result.map(Some(_))
      case Success(None)         => Success(None)
      case Failure(ex)           => Failure(ex)
    })
    val tryResult = tryNameChanger.changeName(ID, "new name")
    println(s"try result = $tryResult")
  }

  class NameChanger[G[_]: Functor](
    finder: Finder[G],
    updater: Updater[G],
    transform: G[Option[G[UpdateResult]]] => G[Option[UpdateResult]]
  ) {
    def changeName(id: UUID, newName: String): G[Option[UpdateResult]] = {
      val saved: G[Option[G[UpdateResult]]] = finder.find(id).map {
        maybeItem: Option[Item] =>
          maybeItem.map { item =>
            updater.update(item.copy(name = newName))
          }
      }
      transform(saved)
    }
  }

  case class Item(id: UUID, name: String)

  class ItemRepository {
    import ItemRepository._

    private val items = Seq(Item(ID, "item name"))

    private def find(id: UUID): Option[Item] = items.find(_.id == id)

    private def update(u: Item): UpdateResult = UpdateResult(s"item $u updated")

    private object capabilities {
      val finder: Finder[Id] = new Finder[Id]() {
        override def find(id: UUID): Id[Option[Item]] =
          ItemRepository.this.find(id)
      }
      val updater: Updater[Id] = new Updater[Id]() {
        override def update(item: Item): Id[UpdateResult] =
          ItemRepository.this.update(item)
      }
    }
  }

  object ItemRepository {
    trait Finder[F[_]] {
      def find(id: UUID): F[Option[Item]]
    }

    trait Updater[F[_]] {
      def update(item: Item): F[UpdateResult]
    }

    case class UpdateResult(message: String)

    class Access {
      def finder(repo: ItemRepository): Finder[Id] = repo.capabilities.finder
      def updater(repo: ItemRepository): Updater[Id] = repo.capabilities.updater
    }

    class TryAccess(access: Access) {
      def finder(repo: ItemRepository): Finder[Try] = new Finder[Try] {
        override def find(id: UUID): Try[Option[Item]] = Try(access.finder(repo).find(id))
      }
      def updater(repo: ItemRepository): Updater[Try] = new Updater[Try] {
        override def update(item: Item): Try[UpdateResult] = Try(access.updater(repo).update(item))
      }
    }
  }

}
Full source at GitHub