Prosta aplikacja w Scali bez frameworka/kontenera DI z użyciem HA/Ports-Adapters i Testcontainers

1

W ramach eksperymentu i nauki FP oraz na własne potrzeby, piszę prostą aplikację w Scali do zarządzania linkami i folderami na wzór zakładek w Chrome/Firefoxie.

Pisze ją bez użycia żadnego frameworka/DI oraz całej magii, którą zapewnia framework np. Spring w Javie, stawiając na Hexagonal Architecture/Porty i Adaptery oraz na zasady Clean Code/SOLID/ i zasad CCP/ADP do komponentów itd

Użyłem następujących bibliotek:

  • REST API/HTTP server: Akka HTTP
  • Testy: Scala Test + Testcontainers + Docker
  • Konfiguracja: Typesafe Config
  • Baza danych: MongoDB + Scala driver do Mongo

Kod znajduje się tu: https://gist.github.com/vrgavth/a81f8f66d86182edcbc3b6715ac0f5c2
Oczywiście jest jeszcze do refaktoryzacji, więc wybaczcie :D Chodzi mi głównie o to, czy warto pisać w taki sposób.

Moje pytania są takie:

  1. Czy taki kod Scali bez użycia DI z ręcznym tworzeniem operatorem new spotkaliście może kiedyś w aplikacjach webowych/mikroserwisach na produkcji w swoich projektach?

  2. Czy raczej jesteście bardziej za użyciem lekkich frameworków typu Lagom/Play lub innych dla Scali zamiast ręcznego DI za pomocą new i SOLID? (Pisałem taki kod bez DI ale w Javie w projektach związanych z Big Data :D)

  3. Czy test integracyjny do LinkControllerSpec, który napisałem z pomocą Testcontainers jest ok według konwecnji pisania testów w Scali?

  4. Jakie potencjalne zalety i wady takiego rozwiązania widzicie w takim kodzie poza następującymi (moim zdaniem)

  • Czytelny kod i pełna kontrola nad nim
  • Przewidywalność w działaniu (brak magii związanej np. z beanami w Springu zwłaszcza w adnotacjach @Component)
  • Łatwe debugowanie (brak konieczności debugowania kodu frameworka np. Springa)

Wołam oczywiście doświadczonych kolegów ze Scali: @jarekr000000 @KamilAdam @Wibowit

Z góry dzięki!

3

Ja i doświadczony Scalowiec :D chociaż to i tak bliższe prawdy niż nazwanie mnie doświadczonym Rustowcem w innym wątku xD

MongoLink(new ObjectId(), url: String, title: String, description: String
Ta linia się w ogóle kompiluje? Po co te typy powtórzone przy wywołaniu?

W mainie te typy też są nadmiarowe przy zmiennych. Ogólnie dla zmiennych lokalnych i pół prywatnych nie podaje się typów jawnie jak nie trzeba.

case class CreateLinkRequest(url: String, title: String, description: String)

I jeszcze dwie inne masz takie jednolinijkowe klasy. Ja nie lubię robić takich jednolinijkowych plików i piszę to wewnątrz jednego object DTO {} lub object API {}. No chyba że podejrzewasz iż tym klasom przybędzie metod

3
Baldr napisał(a):
  1. Czy taki kod Scali bez użycia DI z ręcznym tworzeniem operatorem new spotkaliście może kiedyś w aplikacjach webowych/mikroserwisach na produkcji w swoich projektach?

Tak, stosujemy właśnie takie DI bez magii.

  1. Czy raczej jesteście bardziej za użyciem lekkich frameworków typu Lagom/Play lub innych dla Scali zamiast ręcznego DI za pomocą new i SOLID? (Pisałem taki kod bez DI ale w Javie w projektach związanych z Big Data :D)

Rozwój Lagom i Play powoli jest przekazywany w ręce społeczności (bądź przechodzą one w tryb utrzymania), a Lightbend skupia się na Akce, która ma już sporo rzeczy analogicznych do tych z Playa i Lagoma. Polecam poczytać np. https://discuss.lightbend.com/t/on-the-future-of-akka-lightbend/8997/2

  1. Czy test integracyjny do LinkControllerSpec, który napisałem z pomocą Testcontainers jest ok według konwecnji pisania testów w Scali?

W sensie który aspekt mam ocenić? Na mongodb container się nie znam. Styl given-when-then sam wybrałeś, więc jeśli lubisz taki to ok.

Problem jest w kodzie poniżej, bo masz wyciek:

  override def withFixture(test: OneArgTest): Outcome = {
    val databaseName: String = "linkskap"
    val mongoConnectionDetails: MongoConnectionDetails = MongoConnectionDetails(container.replicaSetUrl, databaseName)
    val mongoConnectionProvider: MongoConnectionProvider = new MongoConnectionProvider(mongoConnectionDetails)
    val linkPersistenceAdapter: LinkPersistenceAdapter = new LinkMongoAdapter(mongoConnectionProvider)
    val linkFacade: LinkFacade = new LinkFacade(linkPersistenceAdapter)
    val fixture: FixtureParam = FixtureParam(new LinkController(linkFacade))
    super.withFixture(test.toNoArgTest(fixture))
  }

Nie zamykasz połączenia. super. nie jest potrzebne na końcu (bo wywołujesz przeciążoną metodę, a nie metodę z nadklasy). Coś takiego więc trzeba zrobić:

  override def withFixture(test: OneArgTest): Outcome = {
    val databaseName: String = "linkskap"
    val mongoConnectionDetails: MongoConnectionDetails = MongoConnectionDetails(container.replicaSetUrl, databaseName)
    val mongoConnectionProvider: MongoConnectionProvider = new MongoConnectionProvider(mongoConnectionDetails)
    val linkPersistenceAdapter: LinkPersistenceAdapter = new LinkMongoAdapter(mongoConnectionProvider)
    val linkFacade: LinkFacade = new LinkFacade(linkPersistenceAdapter)
    val fixture: FixtureParam = FixtureParam(new LinkController(linkFacade))
    try {
      withFixture(test.toNoArgTest(fixture))
    } finally {
      klientOrazPołączenieDoMongosa.close()
    }
  }

Analogicznie trzeba zrobić w

  before {
    val mongoClient: MongoClient = MongoClient(container.replicaSetUrl)
    val mongoDatabase: MongoDatabase = mongoClient.getDatabase("linkskap")
    mongoDatabase.createCollection("links")
  }

i wyjdzie:

  before {
    val mongoClient: MongoClient = MongoClient(container.replicaSetUrl)
    try {
      val mongoDatabase: MongoDatabase = mongoClient.getDatabase("linkskap")
      mongoDatabase.createCollection("links")
    } finally {
      mongoClient.close()
    }
  }

Trochę dziwnie też wygląda hardkodowanie "linkskap" w BaseIntegrationSpec - tutaj bym się raczej spodziewał parametru niż sztywnej wartości. Ale może tak ma być.

Kolejna sprawa to to, że możesz stworzyć klasę bazową zamiast traita. Podobno szybciej się wtedy kompiluje, aczkolwiek nie mierzyłem:

https://www.scalatest.org/user_guide/defining_base_classes
Instead of duplicating code by mixing the same traits together repeatedly, we recommend you create abstract base classes for your project that mix together the features you use the most. For example, you might create a UnitSpec class (not trait, for speedier compiles) for unit tests that looks like (...)

0

Wielkie dzięki za pomoc i wyjaśnienie oraz review!

KamilAdam napisał(a):

Ja i doświadczony Scalowiec :D chociaż to i tak bliższe prawdy niż nazwanie mnie doświadczonym Rustowcem w innym wątku xD

Spoko, na bank masz większe doświadczenie niż ja :D Rusta uczyłem się trochę z rok temu i mega język! Niestety mam jeszcze do przeczytania kilka cegieł po 700-800 str od Tanenbauma: do architektur Structured Computer Organization i Modern Operating Systems i jak tylko je przeczytam, to od razu wracam do Rusta na 100%, bo jest to świetny język, a znajomość FP na pewno w nim też da efekty :D

KamilAdam napisał(a):

W mainie te typy też są nadmiarowe przy zmiennych. Ogólnie dla zmiennych lokalnych i pół prywatnych nie podaje się typów jawnie jak nie trzeba.

Właśnie, nauczyłem się żeby nie korzystać z Type Inference, dlatego zawsze wszędzie dodaję typy dla czytelności, żeby wiedzieć z jakim typem mam od razu do czyniania np. w Intellij. Czyli jednak korzystacie z możliwości Type Inference w Scali i nie piszecie ich tam gdzie nie trzeba?

case class CreateLinkRequest(url: String, title: String, description: String)

I jeszcze dwie inne masz takie jednolinijkowe klasy. Ja nie lubię robić takich jednolinijkowych plików i piszę to wewnątrz jednego object DTO {} lub object API {}. No chyba że podejrzewasz iż tym klasom przybędzie metod

O bardzo dobre spostrzeżenie! Te jednolinikowe klasy to niestety jeszcze nawyk z Javy: jedna klasa = jeden plik, nie bij proszę :(. Ale będę pamiętał o enkaspulacji do wspólnego obiektu, bo dużo lepiej to będzie wyglądać.

@Wibowit Dzięki bardzo za review i wskazówki. Ten kod i tak właśnie nadawał się do refaktoryzacji, ale nie zamknięcie połączenia dopiero teraz zauważyłem :D


Czyli podsumowując, śmiało mogę stosować tą metodę z manualnym DI w pisaniu w Scali apek webowych/mikroserwisów itp i nie przejmować się frameworkiem?
Mam jeszcze jedno pytanie co do podejścia bez DI i stosowania bibliotek. Jeśli macie taką sytuację, że potrzebujecie typowej funkcjonalności zapewnianej przez framework np. obsługi error message przy walidacji w wielu językach czyli i18n w REST API, to szukacie biblioteki, która zapewnia obsługę i18n, czy śmiało można importować taki moduł wielojęzykowości z jakiegoś frameworka np Lagom?
1

Właśnie, nauczyłem się żeby nie korzystać z Type Inference, dlatego zawsze wszędzie dodaję typy dla czytelności, żeby wiedzieć z jakim typem mam od razu do czyniania np. w Intellij. Czyli jednak korzystacie z możliwości Type Inference w Scali i nie piszecie ich tam gdzie nie trzeba?

W intelliju jest skrót alt+= do pokazywania typu. Względnie ctrl+q by pokazać okienko z dokumentacją (będzie puste jeśli dana rzecz jest bez dokumentacji).

IntelliJ ma ustawienia do stylu kodu w Scali i możesz tam sobie poustawiać styl. Jeśli np. nie podasz jawnego typu w publicznym polu to IntelliJ podświetli taką definicję jako problem w kodzie (ale nie będzie blokował kompilacji ani niczego innego).

screenshot-20211113200341.png

Jak dla mnie to te ustawienia co widać na obrazku są OK i się ich generalnie trzymam. Czasem dodaję typy w dodatkowych miejscach, jeśli wnioskowanie typów wylicza typy, których nie chcę. Dla przykładu czasem dla kodu typu option.fold(Left("aaa"))(stringi) dodaję typ, bo kompilator wnioskuje Left z pierwszego parametru, zamiast Either. Podając typ jawnie dostaję to co chcę: option.fold[Either[String, Int]](Left("aaa"))(parseInt) albo option.fold(Left("aaa"): Either[String, Int])(stringi). Przykład jest poglądowy, być może w nowych wersjach Scali akurat ten problem (z foldem) jest naprawiony. Czasem też dodaję typy do np. lokalnych zmiennych jeśli np. kod jest skomplikowany, a ja go intensywnie przerabiam. Dzięki jawnym typom wcześniej widzę, że coś poszło nie tak przy przerabianiu kodu.

Czyli podsumowując, śmiało mogę stosować tą metodę z manualnym DI w pisaniu w Scali apek webowych/mikroserwisów itp i nie przejmować się frameworkiem?

Nie używałem Lagoma, więc nie wiem czy jakiś konkretny mechanizm DI jest tam wymuszany. Używamy gołej Akki, a ta nie ma żadnego mechanizmu do wstrzykiwania.

Mam jeszcze jedno pytanie co do podejścia bez DI i stosowania bibliotek. Jeśli macie taką sytuację, że potrzebujecie typowej funkcjonalności zapewnianej przez framework np. obsługi error message przy walidacji w wielu językach czyli i18n w REST API, to szukacie biblioteki, która zapewnia obsługę i18n, czy śmiało można importować taki moduł wielojęzykowości z jakiegoś frameworka np Lagom?

Różnie. Ważne by nie wpakować się w jakąś bibliotekę, którą rozwija pojedynczy ziomek po godzinach, bo może mu się odechcieć utrzymywania tejże biblioteki i zostaniesz na lodzie z czymś przestarzałym, a w Scali jest mniejszy nacisk na kompatybilność wsteczną niż np. w świecie Javy.

2

W Scali jest trochę tych mechanizmów do kulturalnego DI.
Swego czasu był popularny cake pattern, i kilka podobnych patentów.

Jest też trochę rozwiązań opartych o makra (np. macwire - zręsztą autorem jest Adam Warski).

Jakkolwiek z tablicy popularności, które widziałem co roku na Scalarze wychodziło, że konstruktory rzondzo i co roku coraz więcej ludzi przechodzi na takie proste rozwiązanie.
(a dawno, dawno temu scalowcy używali dziwów typu play, a nawet spring).

0

Wiekie dzięki Wam wszystkim jeszcze raz za pomoc z kodem i ze zrozumieniem jak to jest z całym DI w Scali :D

Zrefaktoryzowałem kod wg wskazówek. Jak już skończę pisać cały backend aplikacji, to podzielę się z Wami, na pewno będzie można coś jeszcze poprawić ;)

1 użytkowników online, w tym zalogowanych: 0, gości: 1