Как стать автором
Обновить

Java/Scala: самая лаконичная трехзвенная архитектура в моем моднейшем To-Do List

Java *Анализ и проектирование систем *SQL *Scala * *

Видео-вариант:

Когда-то давно, в 2015 году, я опубликовал на Хабре статью про Java программу, если вкратце "Как я на коленке сделал свое 1С:Предприятие, с блек-джеком и шлюхами".

Вот на что это было похоже (каскад форм создается автоматически по классам Entities с помощью reflection):

Но это была, во-первых, двухзвенка, а во-вторых, потребовала от меня столько лапшекода, что после кодирования я надолго погрузился в депрессию. И до сих пор считаю, что при прототипировании, ручное вколачивание Entities, еще и в параллель со скриптами liquibase - дурацкая работа, в то время как "тупые 1Сники" (по мнению Настоящих Программистов), редактируют структуру БД вообще не лазя в код и не напрягаясь. Теперь же я готов представить вам вариант производственного процесса, который не отпугнет перебежчика с 1С на Java.

Ключевое преимущество у 1С:Предприятия - заучив десяток хоткеев ты можешь налабать учетную систему, вообще не прикасаясь к мышке, просто добавляя элементы в дерево объектов конфигуратора.

С вашего позволения, процитирую свою же статью:

"Проводя тренинги для РП, я ... с нуля делал простейший контур складского учета за 4 минуты 35 секунд ..., включая ввод в базу тестового примера из двух документов."

Даже сложнейшие 10+ уровневые SQL запросы в 1С создаются без кодирования, выбором списка полей в специальном мастере. Не говоря обо всем остальном. Поэтому тогда, в 2015, я был глубоко разочарован Enities классами, hibernate и прочими прелестями Java.

И вот, допустим, джуниор с 1Сным бэкграундом решил податься за длинным рублем в мир Java/Scala (псс, если что, у нас в банке имеется пара вакансий, пишите в личку).

Но его мозг, избалованный 1Сом, сопротивляется - еще вчера ему не нужно было делать кучу дурацкой работы по набиванию Pojo/Hibernate классов на клаве, и сколько такого волка не корми высоким окладом, он все равно рано или поздно постарается слинять в лес из этого царства BDSM.

Я попытаюсь доказать, что и в мире Java/Scala есть инструменты, которые позволяют создавать полноценные трехзвенные (Bloody Enterprise) приложения, прикладывая минимум усилий, и используя максимум шикарных помощников.

Для начала, запрототипируем БД. Для этого можно использовать практически любую СУБД и любой редактор. В моем случае это PostgreSQL и DBeaver, но если лень устанавливать СУБД, рекомендую использовать H2 database. Создаем там базу, и таблицы по своей задаче. Я создал таблицу tasks для классического to-do list примера.

Кто же создаст для нас дурацкие java Pojo классы? Никто, но скаловские case классы сущностей создаст команда sbt slickCodeGenTask. Вот что она натворила (хотя смотреть не обязательно, вряд ли она накосячит):

package com.todolist.shared
// AUTO-GENERATED Slick data model
/** Stand-alone Slick data model for immediate use */
object Tables extends {
  val profile = slick.jdbc.PostgresProfile
} with Tables

/** Slick data model trait for extension, choice of backend or usage in the cake pattern. (Make sure to initialize this late.) */
trait Tables {
  val profile: slick.jdbc.JdbcProfile
  import profile.api._
  import slick.model.ForeignKeyAction
  // NOTE: GetResult mappers for plain SQL are only generated for tables where Slick knows how to map the types of all columns.
  import slick.jdbc.{GetResult => GR}

  /** DDL for all tables. Call .create to execute. */
  lazy val schema: profile.SchemaDescription = Tasks.schema
  @deprecated("Use .schema instead of .ddl", "3.0")
  def ddl = schema

  /** Entity class storing rows of table Tasks
   *  @param id Database column id SqlType(serial), AutoInc, PrimaryKey
   *  @param text Database column text SqlType(varchar)
   *  @param done Database column done SqlType(bool) */
  case class TasksRow(id: Int, text: String, done: Boolean)
  /** GetResult implicit for fetching TasksRow objects using plain SQL queries */
  implicit def GetResultTasksRow(implicit e0: GR[Int], e1: GR[String], e2: GR[Boolean]): GR[TasksRow] = GR{
    prs => import prs._
    TasksRow.tupled((<<[Int], <<[String], <<[Boolean]))
  }
  /** Table description of table tasks. Objects of this class serve as prototypes for rows in queries. */
  class Tasks(_tableTag: Tag) extends profile.api.Table[TasksRow](_tableTag, "tasks") {
    def * = (id, text, done) <> (TasksRow.tupled, TasksRow.unapply)
    /** Maps whole row to an option. Useful for outer joins. */
    def ? = ((Rep.Some(id), Rep.Some(text), Rep.Some(done))).shaped.<>({r=>import r._; _1.map(_=> TasksRow.tupled((_1.get, _2.get, _3.get)))}, (_:Any) =>  throw new Exception("Inserting into ? projection not supported."))

    /** Database column id SqlType(serial), AutoInc, PrimaryKey */
    val id: Rep[Int] = column[Int]("id", O.AutoInc, O.PrimaryKey)
    /** Database column text SqlType(varchar) */
    val text: Rep[String] = column[String]("text")
    /** Database column done SqlType(bool) */
    val done: Rep[Boolean] = column[Boolean]("done")
  }
  /** Collection-like TableQuery object for table Tasks */
  lazy val Tasks = new TableQuery(tag => new Tasks(tag))
}

После того, как IDEA синхронизирует ваш проект, все сущности, которые подтянулись из SQL схемы, будут подсвечиваться и подсказываться по Ctrl + Space, в т.ч. в любом лямбда-выражении и for comprehension:

Что же в проекте предстоит закодировать нам лично? Всего 2 файла, сервера и клиента. Делать это мы будем, используя фреймворк Akka и концепцию акторов. Очень удобно, что акторы (клиент и сервер) могут относится к разным актор-системам и запускаться в разных концах интернета, обмениваясь сообщениями посредством TCP, UDP (могут и по HTTP/HTTPS, но об этом в другой раз).

Итак, код сервера:

package com.todolist


import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import com.typesafe.config.ConfigFactory

import java.util.concurrent.Executors
import scala.concurrent.ExecutionContext
import scala.util.Success

object Server extends App{
  val config =ConfigFactory.load("application")
  val actorSystem = ActorSystem("serversystem", config)
  val server: ActorRef = actorSystem.actorOf(Props[ToDoListServer],"todolistserver")
}
class ToDoListServer extends Actor{
  import slick.driver.PostgresDriver.api._
  implicit val db = Database.forConfig("my.myDb",Server.config)
  implicit val ec = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(10))

  override def receive: Receive = {
    case x:String if (x.trim.isEmpty) =>
    case x:String =>
      val snd = sender()
          val arr = x.split(' ')
       arr(0) match {
        case "list" =>
          db.run(com.todolist.shared.Tables.Tasks.filter(_.done === false).sortBy(_.id).result)
            .onComplete{ e => e match {
            case Success(e) =>
              val res = e.map(e=>s"${e.id}. ${e.text}").mkString("\n")
              println(res)
              snd ! res
          }}
        case "add" =>
          db.run(com.todolist.shared.Tables.Tasks += com.todolist.shared.Tables.TasksRow(text = arr.drop(1).mkString(" "),done = false,id=0))
        case "done" =>
          val arr1 = arr(1).toInt
          db.run((for { c <- com.todolist.shared.Tables.Tasks  if c.id === arr1 } yield c.done).update(true))
        case _ =>  sender() ! "Unknown command"
      }
  }
}

Как видите, никакого plain SQL - и одновременно, нет необходимости руками мапить структуру БД на case классы Entities.

Код клиента (вообще-то, я изначально скопировал его с какого-то примера из интернета, но так как ввод с клавы внутри актора глючил, от примера по сути ничего не осталось):

package com.todolist

import akka.actor.{Actor, ActorRef, ActorSystem, Props}

object Client {
  case class Connect(server: ActorRef)
  case class Process(cmd: String)
}

class Client extends Actor {
  override def receive: Actor.Receive = {
    case Client.Connect(server) =>
      context become process(server)
  }
  def process(server: ActorRef): Receive = {
    case Client.Process(cmd) =>
      server ! cmd
    case x:String =>
      println(s"$x")
    case _ =>
  }
}

object ClientApp extends App {
  import com.typesafe.config.ConfigFactory
  import scala.concurrent.duration._
  val actorSystem = ActorSystem("client-system", ConfigFactory.load("client"))
  val client: ActorRef = actorSystem.actorOf(Props[Client])
  val serverPath = "akka://serversystem@127.0.0.1:2552/user/todolistserver"
  val serverSelection = actorSystem.actorSelection(serverPath)
  serverSelection.resolveOne(FiniteDuration(10, SECONDS)).foreach { (server: ActorRef) =>
    println(s"Connected to $server")
    client ! Client.Connect(server)
    while (true){
      println("Enter command:")
      scala.io.StdIn.readLine() match {
        case cmd => client ! Client.Process(cmd)
      }
    }
  } (actorSystem.dispatcher)
}

Актор-системы сервера и клиента, если они расположены на одном хосте, не могут висеть не одном и том же порту, поэтому, должны быть сконфигурированы разными файлами application.conf, client.conf. Кроме этого, для sbt придется вырубить jmx, там тоже происходит конфликт портов (либо, используйте разные конфиги и для sbt клиента и сервера).

Для запуска, в одном терминале выполняем:

sbt "runMain com.todolist.Server"

В другом:

sbt "runMain com.todolist.ClientApp"

Проверка:

Enter command: list

5. task2

7. task3

9. task5

Enter command: add task6

Enter command: list

5. task2

7. task3

9. task5

10. task6

Enter command: done 9

Enter command: list

5. task2

7. task3

10. task6

В данном, не побоюсь этого слова, трехзвенном приложении, я лично закодил не более 80 строк; при этом, после создания схемы БД и автоматическом импорте ее в приложение, IDEA подсказывала мне, какие поля имеются у сущностей, и не налепил ли я ошибок. В принципе, это то, что делает конструктор запросов в 1С:Предприятии, поскольку ни строчки plain SQL я не написал. Но, конструктор запросов в 1С может делать только select`ы, все остальное предстоит делать самостоятельно - а тут, как видите, и Insert, и Update.

А вообще-то, в таком же стиле, на Slick вы можете закодить мега запросы для Spark SQL, которые распараллелятся и выполнятся на hadoop кластере из 1000 серверов, а потом обработать это все каким-нибудь XGBoost или Tensorflow. В общем, все только начинается...

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Что бы вы выбрали, если завтра сдавать проект учетной системы, а вы даже не приступали?
36.78% Java + Spring 32
10.34% Scala + Akka 9
24.14% 1C: Предприятие 8 21
3.45% SAP 3
6.9% PHP + WordPress 6
18.39% Этот вызов мне не по зубам 16
Проголосовали 87 пользователей. Воздержались 23 пользователя.
Теги:
Хабы:
Всего голосов 2: ↑0 и ↓2 -2
Просмотры 5.9K
Комментарии Комментарии 3