Cześć!
Po pooglądaniu paru wystąpień @jarekr000000 na temat wad beanów, Springa, AOP oraz faktu, że można obejść się bez kontenera DI w pisaniu serwisów, w ramach nauki Scali i FP tworzę sobie prostą apkę z wykorzystaniem Scali, MongoDB, Akka HTTP jako REST API.
Wykorzystuję tu architekturę hexagonalną/porty adaptery oraz brak kontenera DI. Zdaję sobie sprawę, że nie jest konieczna na projekt takiej małej wielkości, ale robię w ramach nauki :D
Mam kilka pytań do doświadczonych kolegów, którzy piszą funkcyjnie @jarekr000000 @KamilAdam @0xmarcin @Wibowit odnośnie dobrych praktyk w tego typu podejściu funkcyjnym oraz już w samej Akkce HTTP, bo dość ciężko znaleźć informacje :D
Z góry przepraszam za nierfaktoryzowany kod i za brak obsługi walidacji, zdefiniowanie aktorów w main component i brak osobnych objectów dla bazy danych itd, ale robię to w ramach nauki, potem będę sprzątał ;)
Nie dodawałem tu również Entity object jak w DDD, bo przy takim CRUD po prostu nie ma sensu :D
Do komuniakcji z Mongo używam asynchronicznego MongoDB Scala Driver, który jest oficjalnym driverem dla Scali: https://mongodb.github.io/mongo-java-driver/4.2/driver-scala/
Pytania:
-
LinkMongoAdapter jest adapterem wychodzącym i komunikuje się z MongoDB. Jak widzicie, używam tutaj funkcji transformWith, która tworzy Future w zależności od tego czy dokument został znaleziony. Czy takie podejście jest ok, czy lepiej może do Future użyć Either, gdzie Left będzie reprezentować błąd a Right zwrócony case class?
Następnie oczywiście zwracam to do LinkFacade a potem do LinkController -
Gdzie powinien być umiesczony error handling? Czy dozwolone jest zrobienie tego w LinkMongoAdapter, czy lepiej zwracać zwykły Future do LinkFacade, a w fasadzie obsługiwać error handling czyli to co robię teraz w LinkMongoAdapter przez transformWith z Success/Failure albo z wykorzystaniem Option/Either?
-
Gdzie w Akka HTTP umieszać defininicje Routes? Obecnie trzymam je w LinkController, co moim zdaniem pasuje według SRP, bo tam są też funkcje obsługujące konkretne Route'y (create i findOne), czy lepiej będzie je trzymać w osobnym Object/klasie i tym samym pliku?
-
Obsługa błędów wejścia np. pusty url, description itd powinna być realizowana standardowo poprzez zwrócenie np. case class Response z polami statusCode, message itp?
Najlepiej umieścić ją w kontrolerze np. LinkController? Pytam bo w Spring Boot używam do tego osobnego kontrolera z adnotacją @ControllerAdvice :D
Z góry dzięki za pomoc! Poniżej kod:
LinkskapApplication (Reprezentuje koncepcję main component z Clean Architecture, gdzie są inicjalizacje wszystkich modułów itd)
object LinkskapApplication {
implicit val system: ActorSystem = ActorSystem()
implicit val executionContext: ExecutionContextExecutor = system.dispatcher
def main(args: Array[String]): Unit = {
val linkPersistenceAdapter: LinkPersistenceAdapter = new LinkMongoAdapter()
val linkFacade: LinkFacade = new LinkFacade(linkPersistenceAdapter)
val linkController: LinkController = new LinkController(linkFacade)
val config: Config = ConfigFactory.load()
val interface: String = config.getString("http.interface")
val port: Int = config.getInt("http.port")
val futureBinding = Http().newServerAt(interface, port).bind(linkController.routes)
StdIn.readLine()
futureBinding
.flatMap(_.unbind())
.onComplete(_ => system.terminate())
}
}
LinkController
class LinkController(val linkFacade: LinkFacade) {
private val logger: Logger = LoggerFactory.getLogger(getClass)
private implicit val createLinkRequestFormat: RootJsonFormat[CreateLinkRequest] = jsonFormat3(CreateLinkRequest)
private implicit val createLinkResponseFormat: RootJsonFormat[CreateLinkResponse] = jsonFormat4(CreateLinkResponse)
private implicit val getLinkResponseFormat: RootJsonFormat[GetLinkResponse] = jsonFormat4(GetLinkResponse)
val routes: Route = {
pathPrefix(LinkController.LINK) {
concat(
pathEnd {
concat(
create()
)
},
path(Segment) { id:String =>
concat(
findOne(id)
)
}
)
}
}
private def create():Route = post {
entity(as[CreateLinkRequest]) { createLinkRequest: CreateLinkRequest =>
onComplete(linkFacade.create(createLinkRequest)) {
case Success(createLinkResponse) => complete(StatusCodes.Created, createLinkResponse)
case Failure(exception) => failWith(exception)
}
}
}
private def findOne(id: String):Route = get {
onComplete(linkFacade.findOne(id)) {
case Success(getLinkResponse) => complete(StatusCodes.OK, getLinkResponse)
case Failure(exception) => failWith(exception)
}
}
}
object LinkController {
val LINK: String = "links"
}
LinkFacade
class LinkFacade(linkPersistenceAdapter: LinkPersistenceAdapter) {
def create(createLinkRequest: CreateLinkRequest): Future[CreateLinkResponse] = {
linkPersistenceAdapter.create(createLinkRequest)
}
def findOne(id: String): Future[GetLinkResponse] = linkPersistenceAdapter.findOne(id)
}
LinkPersistenceAdapter
trait LinkPersistenceAdapter {
def create(createLinkRequest: CreateLinkRequest): Future[CreateLinkResponse]
def findOne(id: String): Future[GetLinkResponse]
}
LinkMongoAdapter
class LinkMongoAdapter extends LinkPersistenceAdapter {
private val codecRegistry = fromRegistries(fromProviders(classOf[MongoLink]), DEFAULT_CODEC_REGISTRY)
private val mongoClient: MongoClient = MongoClient("mongodb://localhost:27017")
private val database: MongoDatabase = mongoClient.getDatabase("linkskap").withCodecRegistry(codecRegistry)
private val collection: MongoCollection[MongoLink] = database.getCollection("links")
override def create(createLinkRequest: CreateLinkRequest): Future[CreateLinkResponse] = {
val createdLink: MongoLink = MongoLink(createLinkRequest.url, createLinkRequest.title, createLinkRequest.description)
collection.insertOne(createdLink).toFuture().transformWith {
case Success(result) =>
val id: String = result.getInsertedId.asObjectId().getValue.toString
Future(CreateLinkResponse(id, createLinkRequest.url, createLinkRequest.title, createLinkRequest.description))
case Failure(exception) => Future.failed(exception)
}
}
override def findOne(id: String): Future[GetLinkResponse] = {
val linkFuture: Future[Seq[MongoLink]] = collection.find(Filters.eq("_id", new ObjectId(id))).toFuture()
linkFuture.transformWith {
case Success(result) =>
val mongoLink: MongoLink = result.head
Future(GetLinkResponse(mongoLink._id.toString, mongoLink.url, mongoLink.title, mongoLink.description))
case Failure(exception) => Future.failed(exception)
}
}
}
MongoLink
case class MongoLink(_id: ObjectId, url: String, title: String, description: String)
object MongoLink {
def apply(url: String, title: String, description: String): MongoLink = {
MongoLink(new ObjectId(), url: String, title: String, description: String)
}
}
DTOsy
CreateLinkRequest
case class CreateLinkRequest(url: String, title: String, description: String)
CreateLinkResponse
case class CreateLinkResponse(id: String, url: String, title: String, description: String)
GetLinkResponse
case class GetLinkResponse(id: String, url: String, title: String, description: String)