Изграждане на Scala приложение

Scala

Вече познаваме Scala доста добре

Вече всички можем да се наречен функционални магьосници :)

Моделиране на домейни

  • в последните лекции бяхме малко по-абстрактни
  • но най-силно ползата от ФП се проличава когато приложим изразните му средства в конкретни домейни
  • Днес и следващия път ще опитаме да изградим цялостно конкретно приложение
    • Функционално
    • с HTTP, JSON, SQL база
    • с бизнес домейн (уеб магазин)

Избор на IO ефект
(асинхронност и конурентност)

Първите три предоставят имплементация на Cats Effect type class-овете

JSON библиотеки

  • circe - базира се на cats
  • play-json - първоначално част от Play Framework, извадена в последствие. Използва jackson.
  • json4s - “This project aims to provide a single AST to be used by other scala json libraries”

Circe

A JSON library for Scala powered by Cats

JSON types

Circe workflow

Encoding
Data Model -> Encoder -> Json -> String


Decoding
String -> Parser -> Json -> HCursor -> Decoder -> Data Model

Encoders

trait Encoder[A]:
  def apply(a: A): Json

Encoder examples

json/examples/IdCard

Decoding

String -> Parser -> Json -> HCursor -> Decoder -> Data Model

trait Parser:
  def parse(input: String): Either[ParsingFailure, Json]
  def decode[A: Decoder](input: String): Either[Error, A]


trait Decoder[A]:
  def apply(c: HCursor): Decoder.Result[A]

Добавяне на circe към проект

val circeVersion = "0.14.1"

libraryDependencies ++= Seq(
  "io.circe" %% "circe-core",
  "io.circe" %% "circe-generic",
  "io.circe" %% "circe-parser"
).map(_ % circeVersion)
  • circe-core - core data type and type classes
  • circe-generic - uses Shapeless to auto-generate Decoder/Encoder for case classes.
  • circe-parser - Parser type class for decoding JSON

HTTP библиотеки

  • http4s - based on cats-effect and fs2 (streaming)
  • akka-http - full server- and client-side HTTP stack on top of akka-actor and akka-stream
  • Play Framework - build on Akka
  • Scalatra - influenced on Ruby’s Sinatra. “microframework”

http4s

Http applications are just a Kleisli function from a streaming request to a polymorphic effect of a streaming response. So what’s the problem?

Нека разгледаме как е изграден http4s

Започваме просто


case class Request(
    method: Method,
    uri: Uri,
    body: EntityBody,
    headers: Headers,
    ...
)

case class Response(
    status: Status,
    body: EntityBody,
    headers: Headers,
    ...
)

type HttpApp = Request => IO[Response]

type HttpRoutes = Request => IO[Option[Response]]
object HttpRoutes:
  def of(pf: PartialFunction[Request, IO[Response]]): HttpRoutes = pf.lift
type Http[F[_]] = Request => F[Response]
type HttpApp = Http[IO]

type HttpRoutes = Http[IO[Option[_]]]

type Http[F[_]] = Request => F[Response]
type HttpApp = Http[IO]

type OptionIO[A] = IO[Option[A]]
type HttpRoutes = Http[OptionIO]
type HttpRoutes = Http[[A] =>> IO[Option[A]]]

OptionT

OptionT[F[_], A] is a light wrapper on an F[Option[A]]

import cats.data.OptionT

type Http[F[_]] = Request => F[Response]
type HttpApp = Http[IO]

type OptionTIO[A] = OptionT[IO, A]
type HttpRoutes = Http[OptionTIO] 

Какво имаме накрая

type Http[F[_]] = Request => F[Response]

type HttpApp = Http[IO]

type HttpRoutes = Http[OptionT[IO, _]]
object HttpRoutes:
  def of(pf: PartialFunction[Request, IO[Response]]): HttpRoutes =
    req => OptionT(pf.lift(req).sequence)

Всъщност е малко по-различно

Kleisli

type Http[F[_]] = Kleisli[F, Request, Response]

Освен това - IO не е фиксирано

type HttpApp[F[_]] = Http[F]

type HttpRoutes[F[_]] = Http[OptionT[F, _]]
object HttpRoutes:
  def of[F[_]: Defer: Applicative](pf: PartialFunction[Request[F], F[Response[F]]]): HttpRoutes[F] =
    Kleisli(req => OptionT(Defer[F].defer(pf.lift(req).sequence)))

Examples

middlewares

http client

Структуриране на кода

Зависимости

Чистите функции често рефирират към други конкретни функции.

def countHttpResourceWords(url: String): IO[Long] =
  HttpUtils.get(url).map(_.body).map(countWords)

Често обаче бихме желали да се абстрахираме от конкретната имплементация на тези функции

Зависимости

Във ФП това може да постигнем като функцията се подава като параметър:

def countHttpResourceWords(retrieveUrl: String => HttpResponse)(url: String): IO[Long] =
  retrieveUrl(url).map(_.body).map(countWords)

val asyncHttpCountHttpResourceWords = countHttpResourceWords(AsyncHttpClient.get) _

Dependency injection

  • Този подход наричаме със сложното име “Dependency injection”
  • Вид inversion of control – функцията вече не създава/реферира изрично конкретна зависимост, ами я приема като параметър

Dependency injection чрез ООП модулност

trait HttpClient {
  def get(url: String): IO[HttpResponse]
  def post(url: String)(entity: Entity): IO[HttpResponse]
}
class ResourceProcessor(httpClient: HttpClient) {
  def countHttpResourceWords(url: String): IO[Long] =
    httpClient.get(url).map(_.body).map(countWords)
}
val httpClient = new AsyncHttpClient
val resourceProcessor = new ResourceProcessor(httpClient)

resourceProcessor.countHttpResourceWords("http://example.org")

Защо dependency injection?

  • Less coupling
    • не сме зависими от конкретна имплементация
    • винаги можем да я сменим с друга
    • зависими сме единствено от конкретен интерфейс
  • позволява тестване
  • зависимостите на всеки компонент стават по-явни

Кой навързва зависимостите?

  • По време на изпълнение – популярно в Java света (Guice, Spring)
  • По време на компилация

Compile-time dependency injection – демо

Thin cake pattern

Още информация

DI in Scala guide

Безопасно боравене с Resource-и

Тоест функционален вариант на using

Ефектни модули. Dependency Injection чрез Resource

Как да определим кои да са модулите

  • инфраструктурни/библиотечни – занимаващи се с конкретна библиотека или конкретна част от инфраструктурата на приложението
  • домейн модули – за всеки домейн/поддомейн

Уеб магазин

Домейни:

  • Управление на потребителите (регистрация, информация за потребителите, аутентикация)
  • Инвентар (продукти, наличност и т.н.)
  • Магазин и поръчки

Подход:

  • Всеки от тях може да бъде отделен модул, развиващ се сравнително самостоятелно
  • Една промяна обикновено засяга един модул, който в бъдеще може да бъде отделен и в отделен service
  • Напълно естествено разделение

Разделение по слоеве?

  • Например model, repository, service, controller
  • Почти винаги не работи добре за по-големи приложения
  • Една промяна често засяга няколко модула/пакета
  • Компоненти с по-силна връзка помежду си остават по-отдалечени
  • Понякога може да е полезно като второстепенно разделение

Глобални ресурси?

  • Много често приложенията съдържат ресурси, които трябва да се инициализират (при стартиране) и зачистват (при спиране)
    • thread pools
    • връзки към базата
    • уеб сървър
  • Масово в JVM приложение се среща това да не се прави
  • При подхода за DI, който разгледахме, това изисква известно количество mutability
  • По-късно ще разгледам как лесно може да постигнем безопасност чрез Resource

Ефектни модули. Dependency Injection чрез Resource

Потоци

Fs2 – Демо

Връзка със SQL база от данни

Doobie

  • стъпва върху Cats и Cats Effect
  • вътрешно използва JDBC

Doobie – Демо

Doobie – сериализация/десериализация към/от заявка

Anatomy of a Doobie query

Въпроси :)?