Но вернемся к Scala. Я смотрел как устроены Framework-и Play и Spray. Заметил такой тренд, что они все заточены на архитектуру в стиле Акторов(актеров) для обеспечения Highload. Это конечно все правильно и перспективно. Но почему-то погоня за этим сделала кодинг проектов несколько чуть более сложным. Получилось что если у тебя обычный не Highload-проект, то тебе совсем не упали Play и Spray и альтернатив нет для реализации одного из преимуществ Scala, писать меньше букв чем в Java. Особенно смотришь в сторону Spring boot, Spring Data и тд. Там все мило, коротко и красиво. А в Scala библиотеки в актор-стиле похожи на первые версии J2EE по параметру удобства использования.
Начал я изучение Scala с прочтения книги Хорстман К. — Scala для нетерпеливых (есть русское издание). Потом был перерыв в несколько месяцев во время которого я обтачивал свой Framework на Java и с сомнением вспоминал о плюшках из Scala. Но в итоге я все-таки решился и начал писать библиотеку на Scala. Я заметил два нюанса:
- Когда пишешь на Scala несколько дней (например весь Weekend), то возвращаясь в свой коммерческий проект на Java начинаешь выть, как много букв надо писать и как не удобно, а вот на Scala это куда проще.
- Вызывая API написанное в Java из Scala приходиться «приседать» и многие удобства Scala сводятся на нет (ну не все, но всё что нового с Java 6-7-8: лямды, мульти-параметры методов и тд). По этой причине и надо писать на Scala обвертки вокруг стандартной библиотеки или Framework-ов на Java, что бы ими было пользоваться комфортно и удобно
Сначала, я свою библиотеку на Java обвертывал на Scala (работа с Json). Потом где явно уперся в архитектурные особенности в глубине своей Java-реализации с Jetty, переписал этот участок с Java на Scala. И там и там я получил колоссальный опыт. Я лично убедился, что писать код на Scala действительно короче и быстрей (особенно когда запоминаешь синтаксис языка). И что можно без проблем весь наработанный багаж существующих Java библиотек и Framework-ов использовать в своем Scala-проекте. Я уж молчу о магии Scala, которая позволяет делать DSL (Предметно-ориентированный язык). Вспомним причину возникновения ООП (Объе?ктно-ориенти?рованное программи?рование), это желание сделать код читабельным и понимабельным на уровне человеко-понятных выражений. Scala это позволяет сделать еще на более высоком уровне. Например можно описывать свои управляющие структуры похожие на if, for, switch и тд (применил это в ORM для транзакций).
В итоге код фреймворка на гитхабе github.com/evgenyigumnov/scala-common
Пример веб-сервиса использующего этот фреймворк на гитхабе github.com/evgenyigumnov/example-scala
Структура примера:
./:
build.sbt
./javascript:
user.js
./pages:
index.html
layout.html
login.html
./sql:
1.sql
./locale:
messages_en.properties
./src/main/scala/com/igumnov/scala-2.11/example:
ExampleUser.scala
SiteServer.scala
build.sbt
name := "example-scala"
version := "1.0"
scalaVersion := "2.11.7"
libraryDependencies += "com.igumnov.scala" % "scala-common_2.11" % "0.5" // Подключаем наш фреймворк
libraryDependencies += "com.h2database" % "h2" % "1.4.187" // подключаем БД
// подключаем Bootstrap, AnglularJS и тд из webjars проекта
libraryDependencies += "org.webjars" % "angular-ui-bootstrap" % "0.12.0"
libraryDependencies += "org.webjars" % "angularjs" % "1.3.8"
libraryDependencies += "org.webjars" % "bootstrap" % "3.3.1"
SiteServer.scala
package com.igumnov.scala.example
import java.util.Calendar
import com.igumnov.scala._
import com.igumnov.scala.webserver.User
object SiteServer {
def main(args: Array[String]) {
// Создаем пул коннекций к БД (максимум 3 коннекта)
ORM.connectionPool("org.h2.Driver", "jdbc:h2:mem:test", "SA", "", 1, 3)
// Накатываем на базу объявления таблиц или оно это пропускает если уже делало
ORM.applyDDL("sql")
// Размер пула нитей для вебсервера
WebServer.setPoolSize(5,10)
// Задаем начальные параметры веб-сервера
WebServer.init("localhost", 8989)
// Определям откуда брать обьекты с пользователями
WebServer.loginService((name) => {
val user = ORM.findOne[ExampleUser](name)
if (user.isDefined) {
Option(new User(user.get.userName, user.get.userPassword, Array[String]("user_role")))
} else {
Option(null)
}
})
// Говорим что у нас включена безопасность которая должна работать по URL-ам
WebServer.securityPages("/login", "/login?error=1", "/logout")
// Ограничиваем доступ только для пользователям с ролью user_role
WebServer.addRestrictRule("/*", Array("user_role"))
// Даем доступ для всех к статическому контенту
WebServer.addAllowRule("/static/*")
// Указываем откуда брать этот статический контент из classpath от webjars
WebServer.addClassPathHandler("/static", "META-INF/resources/webjars")
// Даем доступ для всех к нашим Java Script-ам
WebServer.addAllowRule("/js/*")
// Указываем в какой папке на винте лежат наши Java Script
WebServer.addStaticContentHandler("/js", "javascript")
// Определяем каким образом серверу вычислять какой язык (в примере захардкожен всего один единственный язык)
// И указываем в каком файле для этого языка лежат ключи - значения
WebServer.locale(Map("en" -> "locale/messages_en.properties"), (rq,rs)=>{
"en"
})
// Указываем в какой папке на винте лежат шаблоны страниц
WebServer.templates("pages",0)
// Добавляем контроллер по урл "/", который добавляет в модель текущее время и говорит, что нужно отобразить index.html
WebServer.addController("/", (rq, rs,model) => {
model += "time" -> Calendar.getInstance.getTime
"index"
})
// Добавляем контроллер по урл "/login", который говорит, что нужно отобразить login.html
WebServer.addController("/login", (rq, rs,model) => {
"login"
})
// Добавляем REST-контроллер по урл "/rest/user" и указываем что могут методом POST/PUT прислать JSON-объект типа ExampleUser
WebServer.addRestController[ExampleUser]("/rest/user", (rq, rs, obj) => {
rq.getMethod match {
case "GET" => { // Прилетел GET запрос
ORM.findAll[ExampleUser]() // Извлекаем список пользователей
}
case "POST" => { // Прилетел POST запрос
val user = obj.get
user.userPassword = WebServer.crypthPassword(user.userName, user.userPassword)
ORM.insert(obj.get) // Вставляем его в БД
}
case "DELETE" => { // Прилетел DELETE запрос
val user = ORM.findOne[ExampleUser](rq.getParameter("userName"))
// Если юзер demo не даем удалять
if(user.get.userName == "demo") throw new Exception("You cant delete demo user")
ORM.delete(user.get)
user.get
}
}
})
// Для того что бы эксепшены в рест сервисе выдавались в виде JSON возвращаем ошибку в виде обьекта Error
WebServer.addRestErrorHandler((rq, rs, e) => {
object Error{
var message:String =_
}
Error.message = e.getMessage
Error
})
val users = ORM.findAll[ExampleUser] // Берем из БД всех пользователей
if(users.size==0) { // В таблице с пользователями пусто
val user = new ExampleUser
user.userName="demo"
user.userPassword=WebServer.crypthPassword(user.userName, "demo")
ORM.insert(user) // Добавляем demo/demo пользователя в БД
}
// Если до этого места кода дошло управление и ничего не вывалилось по Exception, то стартуем веб-сервер :)
WebServer.start
}
}
ExampleUser.scala
// Данный класс используется для JSON сериализации и десериализации и также для меппинга в БД
package com.igumnov.scala.example
import com.igumnov.scala.orm.Id
class ExampleUser {
@Id(autoIncremental = false)
var userName: String = _
var userPassword: String = _
}
1.sql
# Создаем таблицу в БД где будем хранить через ORM объекты типа ExampleUser.class
CREATE TABLE ExampleUser (userName VARCHAR(255) PRIMARY KEY, userPassword VARCHAR(255))
login.html
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<!-- Указываем что нужно использовать декоратор layout из layout.html -->
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorator="layout">
<body>
<!-- Объявляем наш контент блок который будет подставлен в layout.html -->
<div layout:fragment="content">
<form name="form" action="/j_security_check" method="POST">
<div class="modal-header">
<h3 class="modal-title" th:text="#{login.title}"></h3> <!-- Берем название окна из ленг файла -->
</div>
<div class="modal-body">
<div class="form-group">
<input type="text" name="j_username" class="form-control" value="" placeholder="Login"/>
</div>
<div class="form-group">
<input type="password" name="j_password" class="form-control" placeholder="Password"/>
</div>
<div class="form-group">
<button type="submit" id="login" class="btn btn-primary">OK</button>
</div>
</div>
</form>
</div>
</body>
</html>
index.html
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<!-- Указываем что нужно использовать декоратор layout из layout.html -->
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorator="layout">
<body>
<!-- Объявляем наш контент блок который будет подставлен в layout.html -->
<div layout:fragment="content">
<!-- Подключаем наш контроллер на AngularJS-->
<script src="/js/user.js"></script>
<h1 th:text="${time}"></h1> // Выводим текущее время переданное в модель
<!-- Обозначаем область действия нашего контроллера UserCtrl -->
<div ng-controller="UserCtrl">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Password</th>
<th></th>
</tr>
</thead>
<tbody>
<!-- В цикле заполняем таблицу пользователями -->
<tr ng-repeat="user in users">
<td>{{user.userName}}</td>
<td>{{user.userPassword}}</td>
<!-- По клику на крестик вызываем функцию на контроллере для удаления пользователя -->
<td><a href="#"><span class="glyphicon glyphicon-remove" tooltip="Delete" ng-click="deleteUser(user)"/></a></td>
</tr>
</tbody>
</table>
<div ng-model="user">
<!-- Форма добавления пользователя -->
<div class="form-group">
<input type="text" class="form-control" ng-model="user.userName" placeholder="Login"/>
</div>
<div class="form-group">
<input type="password" class="form-control" ng-model="user.userPassword" placeholder="Password"/>
</div>
<div class="form-group">
<!-- По клику на кнопке вызываем функцию в контроллере добавляющую пользователя -->
<button class="btn btn-primary" ng-click="addUser(user)">Add</button>
</div>
</div>
</div>
</div>
</body>
</html>
layout.html
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<!-- Область действия нашего приложения на AngularJS -->
<html ng-app="com.igumnov.common.example">
<head>
<title>Title</title>
<link rel="stylesheet" href="/static/bootstrap/3.3.1/css/bootstrap.min.css" />
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<script src="/static/angularjs/1.3.8/angular.min.js"></script>
<script src="/static/angularjs/1.3.8/angular-resource.min.js"></script>
<script src="/static/angular-ui-bootstrap/0.12.0/ui-bootstrap-tpls.min.js"></script>
<div class="container">
<!-- Сюда будет вставляться контентный блок -->
<div layout:fragment="content"></div>
</div>
</body>
</html>
user.js
angular.module('com.igumnov.common.example', ['ui.bootstrap', 'ngResource'])
.factory('User', ['$resource', function ($resource) { // Объявляем REST-ресурс User
return $resource('/rest/user', {}, {
list: { // Список юзеров
method: 'GET',
cache: false,
isArray: true // Результат вызова массив
},
add: { // Добавляем юзера
method: 'POST',
cache: false,
isArray: false // Результат вызова один объект
},
delete: { // Удаляем юзера
method: 'DELETE',
cache: false,
isArray: false // Результат вызова один объект
}
});
}])
.controller('UserCtrl', function ($scope, User) { // Обьявляем наш контроллер UserCtrl
$scope.users = User.list({}); // Заполняем список пользователя при инициализации контроллера
$scope.addUser = function (user) { // Функция добавления пользователя
User.add({},user,function (data) { // Дергаем REST-интерфейс
$scope.users = User.list({}); // В случае успеха, перезаполняем список пользователей
}, function (err) {
alert(err.data.message); // В случае ошибки, выводим ошибку
});
}
$scope.deleteUser = function (user) { // Функция удаления пользователя
User.delete({"userName" : user.userName},user,function (data) { // Дергаем REST-интерфейс
$scope.users = User.list({}); // В случае успеха, перезаполняем список пользователей
}, function (err) {
alert(err.data.message); // В случае ошибки, выводим ошибку
});
}
});
messages_en.properties
login.title=Login
В заключении могу сказать, что Scala буду внедрять в коммерческих проектах написанных на Java. Не собираюсь там ничего переписывать из сделанного на Java, но новые модули, точно буду писать на Scala. Причина такого решения: на Scala быстрее, удобнее, чище код. В общем это эффективно.
PS Так как я новичок в Scala, с радостью готов выслушать критику по своему коду. Нужен фидбек, чтобы понять что делаю не так.
Комментарии (24)
aparamonov
29.07.2015 17:23+1Неидиоматично. Модель должна быть иммутабельной. ОРМ железно работает на рефлекшне — совсем не очень, все в мире скалы стараются этого избегать. Конфигурация сервера задается статически… тестировать пробовали? А если и пробовали, то получилось с костылями, верно?
Если реально хочется заняться велосипедостроением — скопируйте мега проект типа slick — заодно всю функциональщину по полочкам разберете.
Идеальное для меня решение — найти работу на скале. Если не хочется/не получается — контрибьютте в опен соурс — там полно скала проектов и все будут счастливы, и Вам дадут подробный фидбек, и польза обществу.
По мне так Ваши статьи меня бы жестко напрягли как потенциального работодателя — слишком велосипедисто и плохой код.igumnov Автор
29.07.2015 17:42+1Да — я еще тот говнокодер, но я учусь и показываю результат… Я не ищу работодателя. Просто учусь.
ShadowsMind
29.07.2015 18:45Вот бы кто-нибудь из серьезных дядек взял на себя и довел до production ready Spring Scala… Эх мечты, мечты…
solver
29.07.2015 18:49Так его и не доводят, потому что он нафиг не нужен…
ShadowsMind
29.07.2015 19:22+1Просто слишком разные модели и подходы. Spring не далеко от JavaEE ушел, с километром атотаций и всех этих Java магий через рефлекшн, кодогенерацию и т.д. А в Scala как бы постоянно намекают, что анотации и рефлекшн это не ок.
Суть в том, что для больших проектов альтернатив Spring'у на Java нет(всякие Gouce не предлагать — в них и 10% инфраструктуры спринга нет… ), а на Scala мне совершенно не нравятся фрэймворки, вот и приходится писать на привычном Spring'е, только с приятным синтаксисом Scala ). Может быть я просто еще не привык, еще даже пол года нет как на Scala стал писать, глядишь скоро наступит фп головного мозга и стану юзать всякие pray, spray etc.igumnov Автор
29.07.2015 19:27Ну вот я тоже планирую в своем коммерческом проекте на Spring Boot, Spring Data, Spring MVC новые модули писать на Scala…
solver
29.07.2015 19:52+1Тут дело не в ФП, оно ортогонально спрингу.
Достаточно сломать себя один раз, поломать это привычное и потому кажущееся удобным использование спринга. И попробовать scala-way. Не потому что он идеален или еще какие тупые фанатские приблуды.
Просто посмотреть как можно делать приложения по другому, на тех же play, spray, akka, slick.
Увидеть разницу. И спринг уже точно не особо захочется.
Хотя, лично я, не вижу ничего такого в использовании связки spring+scala.
Это всего лишь инструменты.
Просто я уже побывал на той стороне… и знаю, что есть инструменты лучше.
fogone
29.07.2015 18:09Получилось что если у тебя обычный не Highload-проект, то тебе совсем не упали Play и Spray и альтернатив нет для реализации одного из преимуществ Scala, писать меньше букв чем в Java. Особенно смотришь в сторону Spring boot, Spring Data и тд. Там все мило, коротко и красиво.
Если только в этом дело, то попробуйте kotlin. И писать меньше и оборачивать ничего не надо, можно использовать любимые фреймворки.
btd
29.07.2015 19:38+3Качество кода у вас конечно (если честно то для скалы это ж*па)…
1. Берем
val user = ORM.findOne[ExampleUser](name) if (user.isDefined) { Option(new User(user.get.userName, user.get.userPassword, Array[String]("user_role"))) } else { Option(null) }
2. ???
3. Profit
val user = ORM.findOne[ExampleUser](name) user.map(u => new User(u.userName, u.userPassword, Array("user_role")))
Про God объект, WebServer и говорить нечего.igumnov Автор
29.07.2015 19:42те если там пусто map не сработает? удобно — сильно давно читал книгу — видать забыл что так можно…
про WebServer что не так-то? расскажите — учимся же…btd
29.07.2015 19:45scala> Option(null)// можно сразу писать None res27: Option[Null] = None scala> Option(null).map(_ => 10) res28: Option[Int] = None scala> Option("abc").map(_ => 10) res29: Option[Int] = Some(10)
igumnov Автор
29.07.2015 20:06-2я почему-то cli от Scala не юзаю. Наверное надо ввести в практику что бы простые вещи смотреть и проверять.
igumnov Автор
30.07.2015 14:50В итоге начал читать вот эти два источника
twitter.github.io/scala_school/ru
twitter.github.io/effectivescala/index-ru.html
они довольно сжато подают информацию и когда есть минимальный опыт кодинга и прочтенная книжка по Scala очень легко воспринимается сейчас информация
в итоге вижу что я в своей либе писал на Scala но в стиле Java -)))igumnov Автор
30.07.2015 15:56Кстати вот щас сам переделал
Было
if (params.isDefined) { val messageParameters: Array[AnyRef] = params.get.toArray resolver.resolveMessage(null, messageKey, messageParameters).getResolvedMessage } else { resolver.resolveMessage(null, messageKey, null).getResolvedMessage }
стало
val messageParameters: Array[AnyRef] = params.getOrElse(List[String]()).toArray resolver.resolveMessage(null, messageKey, messageParameters).getResolvedMessage
some_x
30.07.2015 08:57+1Я не специалисит по java, я специалист по C#. Но тем не менее заглавная картинка удивила.
В яве же есть лямбды(стрелочные функции)? А extension методы?
Что тогда мешает в яве реализовать аналогичный метод flatMap, для класса List или интерфейса который он реализует?igumnov Автор
30.07.2015 10:53Да все верно — картинка древняя. Просто в свое время не было и Scala еще более привлекательно выглядела для Java разработчиков.
Googolplex
30.07.2015 11:23+4В джаве нет экстеншн-методов, но в восьмёрке добавили дефолтные методы для интерфейсов, через которые на существующих интерфейсах коллекций добавили операции для работы со стримами. Поэтому, начиная с восьмёрки, flatMap в джаве есть, он даже называется так же:
orders.stream().flatMap(o -> o.getProducts().stream()).collect(Collectors.toList())
Но это всё равно более громоздко, чем в скале. И вообще, библиотека стримов в джаве менее практичная, чем в скале — там даже нормального foldLeft нет.
ShadowsMind
Ничего не имею против ваших трудов, но лучше юзать готовые решения от серьезных поставщиков, имхо. Как показывает практика свои подделки очень редко бывают лучше аналогов, а проблемы багов и скудность функционала будут сводить на нет продуктивность.
igumnov Автор
Согласен. Но прикольно же велосипед изобретать -) Особенно в учебных целях…