All streams
Search
Write a publication
Pull to refresh
31
0
Антон Куранов @Throwable

Пользователь

Send message

Наверное единственный оставшийся серьезный юзер у свинга -- это Jetbrains :) Он же по совместительству и основной контрибьютор.

Вообще нынче модно делать локальные веб приложения и паковать их в электрон. Если нужна обязательно Java, взять какой-нибудь Vaadin или Kotlin Multiplatform...

Что такое “микросервис” технически и чем он отличается принципиально от “монолита”?

Хороший вопрос, не так ли?

У меня есть даже лучше вопрос: чем микросервисная архитектура принципиально отличается от сервисной (SOA), расхайпованной 10-15 лет назад? У меня только один ответ -- это то же самое, только собранное на коленке, реализованное на другом стеке, и игнорирующее всякие индустриальные стандарты.

Мартин Фаулер в 2012 году говорил о так называемой “слабой связности”. И “слабая связность” становится в микросервсиной архитектуре центральным понятием.

К сожалению, центральным понятием в микросервисной архитектуре на сегодняшний день являются именно все эти ваши докеры, кубернетисы, клауды, эджайлы, девопсы и прочие религиозные атрибуты. Сомневаюсь, что Мартин Фаулер в 2012 году имел именно это ввиду, говоря о слабой связанности.

Базы данных. “Слабая связность” в микросервисном подходе позволяет каждому отдельному микросервису иметь свою базу данных. А это значит, что её во-первых нужно спроектировать, а во вторых доходчиво для разработчика описать. 

...а это значит, что ввиду отсутствия ACID у вас сразу теряется консистентность операций, и вам необходимо дополнительно искать искусственные способы обеспечения целостности данных, плюс по каждому предоставить доказательство, что в каждом данном сценарии целостность соблюдается. И на это будет тратиться львиная доля усилий архитектора решения.

Лет 5 назад было абсолютно нормальным говорить бизнесу о том, что ему надо. Теперь это не так. Бизнес теперь диктует ИТ, что ему нужно и сам предоставляет аналитику. 

Это наверное в какой-то параллельной вселенной. Бизнес до сих пор требует одну большую кнопку: "сгенерировать прибыль" или "сделать красиво". На элементарные вопросы типа "что делать, если случиться то-то..." у бизнеса один ответ -- "мне лень об этом думать", или "ну вы там придумайте сами что-нибудь, я же плачу вам за это". Вся аналитика, которую я видел за свою карьеру в лучшем случае покрывает наивный самый оптимистичный сценарий, оставляя за кадром кучу кейсов. И что-то я не заметил, что ситуация сильно поменалась.

Вот теперь стало понятно, что нагромождение микросервисов плохо масштабируется и очевидно, микросервисы это не всегда хорошо. Так когда же нам стоит их использовать?

Тут можно добавить еще и организационный мотив: если над компонентом трудится более 5 разрабочиков, становится очень сложным координировать их работу, слияние веток, релизный цикл и т.д. Поэтому при возможности лучше разделить функциональные части и на каждую назначить свою небольшую команду.

И вот в этой единой точке входа мы складываем схемы (xsd или json). И вот по этим схемам должна осуществляться валидация данных.

И как это соответствует принципу слабой связанности иметь единый реестр сервисов? Это что-то типа UDDI из SOA?

Да, после пандемии рынок сильно меняется -- очень много офферов от зарубежных компаний. Однако, компании тоже не дураки -- предлагают условия исходя из твоего ПМЖ, особенно если компания имеет представительство в стране.

С налогами по законодательству, если проживаешь больше полугода, то должен платить налоги в месте проживания. Налоговая будет следить за деньгами, поступающими на счет в местном банке, с которых нужно будет декларировать прибыль. Если есть резиденство с разрешением на работу, то можно оформиться как ЧП (autonomo), сделать себе официальную зарплату, и государство будет доить тебя конскими налогами при минимальной отдаче.

Главное отличие испанцев (и европейцев в целом) от жителей развивающихся стран — в том, что они не гонятся за доходами и сверхдоходами. Местных низких зарплат достаточно для комфортной жизни.

Тем не менее в IT одни из самых низких зарплат по Европе. Выше установленной ставки нигде не прыгнуть, работай ты хоть за семерых. Поэтому большинство испанцев мечтает устроиться в большие компании, которые помимо стабильности могут предложить некоторые социальные бонусы и не сильно напрягать с обязанностями. С другой стороны низкие зарплаты в IT компенсируются низкой производительностью и качеством специалистов, т.к. реально хорошие профессионалы легко эмигрируют в другие страны, где их труд оценивается в выше 2-5 раз.

/sarcasm mode/ Наше дело маленькое: сказали светильник -- значит светильник. Верим, вопросов не задаем.

У меня одного впечатление, что способ изготовления этих артефактов больше похож на лепку, нежели на резьбу? Нет характерных резких граней, все переходы плавные, плоские части как-будто согнуты внутрь...

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

Хайп раздувает не тот кто создает технологию. 

Именно те, кто создает технологию и раздувают хайп вокруг нее. Более того, сама технология создается ради хайпа, а не наоборот. Сегодня все модные тенденции идут от гигантов индустрии, которые фактически диктуют моду и технологии. И делают это с конкретной коммерческой целью -- продвижение своих облачных платформ. А мода в свою очередь бездумно подхватывается адептами и создает эффект снежного кома.

Ценность определяет социум, людям нравиться значит все ок.

Ценность на 100% определяет маркетинг, задача которого максимально оправдать завышенную цену товара или сервиса при минимальных издержках на производство оного. Успешный товар -- это не лучший или качественный, а наиболее коммерчески оправданный. Конечные же потребители всегда ограниченны, а зачастую безальтернативны в своем выборе, а посему зачастую вынуждены хавать и плеваться разрекламированным низкокачественным ширпотребом.

Наконец-то человек со здоровой критикой. Подписываюсь под каждым пунктом! Блокчейн ведет к мировому энергетическому кризису. Material design - примитивное некрасивое и неудобное говнище: кнопка должна быть похожа на кнопку, а не быть раскрашеной полоской.

По поводу Agile: из изначально правильной идеи не мешать разработчику делать до конца свое дело сколотили целую религию с адептами, клириками и евангелистами, позиционируя ее как благодать и серебряную пулю от всех напастей. А на еретические претензии типа "вот мы внедрили эджайл, но сани быстрее не едут" всегда осаждают, что эджайл у вас неправильный, сами вы постоянно грешите и потому невидать вам благодати.

С DevOps-ом та же ситуация. Да, неплохо бы если разработчик понимал всю цепочку доставки и выполнения их кода. Однако это быстро переросло в целый фетиш из паравоза тулзов, где девопсеры меряются ЧСВ у кого их больше задействовано в проекте. И чем их больше, тем весомее становиться роль девопсера в проекте, при всем при том, что все это никак не добавляет value к самому проекту.

Я бы еще к хейту добавил Микросервисы. Распили свой монолит на микросервисы и будет тебе счастье! На самом деле получаешь лишь геморрой с кодовой базой, деплойментом, сборкой, координацией изменений и пр. А самый лютый дебилизм -- это неосознанный отказ от транзакций и ACID. То есть консистентность данных, которая шла из коробки, теперь заменяется на саги и прочие неработающие велосипеды. К сожалению, на сегодняшний день уже мало кто вообще понимает концепт транзакций.

Я думаю самый главный хейт, который лежит в основе всех новых IT-веяний -- это Культ Карго, религиозное мышление, отсутствие элементарной критики и постоянный поиск панацеи от всех проблем и серебряной пули от всех напастей. Поэтому очередной распиаренный термин быстро подхватывается миллионами и становится божественным откровением. То, что он имеет все признаки религии вы можете убедиться например по неаргументированным нападкам адептов в коментариях, сразу же переходящих на личности. Настоящий айтишник всегда должен критически относиться к своим средствам и методам разработки, уметь ими пользоваться, понимать их преимущества и недостатки, и оценивать их исключительно с позиции личной пользы.

А все потому что не надо быть швейцарким ножом, ставя в приоритет количество фич над удобством и безопасностью.

А ничего удивительного. В Logback мы внезапно столкнулись с багом, который спровоцировал почти детективную историю поиска отказа в системе. Баг стоит в трекере ещё с 2014 года, но до сих пор не закрыт.

Автоматизируйте все что можно.

И да, и нет. Во-первых, автоматические инструменты достаточно "костные" -- вылезает стопицот случаев, где реально такое написание оправдано, однако нет возможности объяснить это аналайзеру. А во-вторых, они реагируют лишь на примитивные случаи подобно потенциальным ошибкам, оставляя за кадром более глобальные вещи как общий дизайн, стиль и читаемость кода, что например для меня является гораздо более важным аспектом, нежели наличие потенциальных ошибок, которые легко покрываются тестами.

Круто! У меня были подозрения, что весь процесс не достигает полного паралелизма выполнения либо по причине ограничения пулов, либо сам сайт не дает делать 100 запросов с одного хоста и ставит их в очередь. Интересно было бы посмотреть действительно ли все выполняется параллельно.

Насчет долгого SSL хендшейка есть предположение, что на сокете не выставлен TCP_NODELAY, однако я не нашел как его можно сконфигурировать в Java 9 HttpClient -- там настройки сильно ограничены.

Почему разница между Java и JavaScript почти трёхкратная?

Первое, что бросается в глаза -- это кондовый способ замера производительности:

        var start = System.currentTimeMillis();
        var contents = requestManyUrls(urls).get();
        var time = System.currentTimeMillis() - start;

и что самое основное -- отсутствие предварительного "прогрева". Это является причиной почему:

Если повторно запустить тот же код, общий размер будет близким, но не точно таким же. Предполагаю, что содержимое некоторых ссылок динамично.

То есть то, что аффтар тут намерял -- это работа http-клиента + класс лоадинг + динамическая оптимизация + создание новых статических инстансов -- т.е. сразу работу половины JVM.

Во-вторых, CompletableFuture.supplyAsync()использует дефолтный ForkJoinPool.commonPool(), поведение которого определяется внешними параметрами, железом и вендором Java-рантайма. В большинстве случаев размер дефолтного пула выбирается равеным количеству CPU core. То есть для 100 параллельных запросов из блокирующих операций использовалось всего 4-8 треда.

Далее:

Параллельные HTTP-запросы при помощи современного HttpClient

Формально работа HttpClient принципиально ничем не отличается от примера выше, за исключением того, что вместо ForkJoinPool.commonPool() используется java.util.concurrent.Executors.newCachedThreadPool(). В итоге то, что замерил аффтар -- это время выполнения запросов + время создания новых тредов для параллельной обработки. Поэтому если бы автор повторил тест сразу после первого прогона -- он был бы "сильно потрясен, весьма удивлен и крайне обескуражен".

Огромный код с современным HttpClient выглядит пугающе

                .map(url -> URI.create(url))
                .map(uri -> HttpRequest.newBuilder(uri))
                .map(reqBuilder -> reqBuilder.build())
                .map(request -> client.sendAsync(request, BodyHandlers.ofString()))

Ну да, особенно если искусственно навтыкать цепочку ненужных преобразований :)))

Почему разница между Java и JavaScript почти трёхкратная?

Потому что у аффтара изначально таковой была задача.

Код на JavaScript сначала выполняет один за другим 105 HTTP-запросов. Когда приходит ответ, движок JavaScript помещает в очередь задач небольшой обратный вызов. После получения всех ответов единственный поток по очереди обрабатывает их.

Ну то есть все рассчитано на то, что все 105 запросов и ответов должны обязательно поместиться в память, что никто не будет скачивать и закачивать гигабайтные запросы/ответы, что все данные для отправки уже доступны во время вызова, и что каждый из 105 обработчиков отработает моментально, чтобы никаки не задерживать других.

В Java это работает совершенно иначе. Создаётся множество потоков, каждый из которых отправляет один HTTP-запрос.

Не в Java, а конкретно в Java 11 HttpClient. В TCP стеке для отсылки требуется: открыть сокет, записать запрос и прочитать ответ. В классической синхронной модели все три операции блокирующие, и поэтому на каждый запрос требуется тред, который их последовательно обрабатывает, учитывая что открытие сокета немоментально, буфер данных для отсылки ограничен и может быть много меньше размера запроса, ответ также может приходить порциями, а обработка никак не должна препятствовать и интерферировать с другими запросами.

А вот в Java как раз есть способ реализовать низкоуровнево полную асинхронную реализацию, используя Selectors и Channels. Кроме того, есть прекрасный стек Netty в котором это уже реализовано, равно как и куча надстроек к нему, напр. полностью асинхронный https://github.com/timboudreau/netty-http-client Ну и напоследок стоит упомянуть, что начиная с версии 17 в JVM добавлен project Loom, с виртуальными тредами, которые позволяют использовать классическую синхронную модель, не заботясь о масштабировании, и которая "под капотом" выполняется асинхронно и неблокирующе. Только реальных тредов будет не один, а сколько душе угодно.

P.S. забыл упомянуть: www.bbc.com наверняка контролирует количество параллельных запросов с одного хоста, поэтому все тесты со 100 реквестами можно сразу выкинуть в корзину.

5. На порядки меньше уровень миграции как внешней, так и внутренней.

Если нужен замороченный flow с одновременной поддержкой старой и новой моделей:

  • создаете новую таблицу

  • мигрируете данные со старой

  • синхронизацию изменений реализуете при помощи триггеров

  • следующий апдейт дропает старую таблу и триггеры

Легко проследить, что такое определение не противоречит математической идеологии.

Легко заметить, что противоречит:

симметричным (для любых xy выполняется: если x = y, то y = x)

String a = "Test";
String b = null;
a.equals(b)	// false
b.equals(a)	// NPE

Строгую математическую семантику соблюдает Objects.equals(). И именно его нужно было использовать с самого начала для оператора "==". Однако создателям Java казалось очень важным каждый обращать внимание пользователей на то, как они реализовали equals(), и что сравнивать строки при помощи "==" в их языке хоть и можно, но будет неправильно. Даром что ссылочное сравнение используется в одном случае из ста. Но уж теперь ничего не поделаешь.

Вопреки заявленному разработчиками, интеграция с джавой и ее экосистемой далеко не бесшовная. Цена за "удобство" -- общее усложнение билда (соответственно увеличивающаяся "хрупкость"), наличие подводных камней и лишнего геморроя практически с любой библиотекой. К слову, null -- это одна из самых распространенных, но наиболее легко отлаживаемых ошибок. Поэтому формальный смысл данной затеи на мой взгляд очень сомнителен. Котлин оптимально использовать в "чистом" проекте по возможности с нативной котлиновской экосистемой (пусть и небольшой), либо с джавовыми библиотеками, где заявлена поддержка Kotlin.

Синхроны или асинхроны в каком смысле?

Видимо, с "потерей контекста" как SwingUtilities.invokeLater(...) или setTimeout(() => ...). Большинство реализаций событийной модели никак не декларируют данное поведение, из-за чего приходится сначала исследовать их поведение.

Гарантируется, всегда в порядке очередности в которой произошла подписка

Вопросы больше риторические. А теперь представьте, что реализацию делал другой человек, и нет возможности с ним связаться. Согласно моему опыту никто и никогда не декларирует поведение обзерверов, даже в именитых фреймворках и библиотеках. Читая документацию, нет возможности на 100% разобраться с гарантиями, поэтому практически приходится их тестить и влезать в исходники.

Если мы говорим про Spring то для этого достаточно добавить аннотация `@Order` с цифрой.

И параллельно ознакомить всех разработчиков в проекте с глобальной политикой упорядочивания observer-ов, а также открыть репозиторий, где бы каждый сначала резервировал индекс для своих нужд. Это антипаттерн "Magic Number". Проблема в том, что корректность поведения приложения будет зависеть от взаимного расположения всех (в общем случае) транзакционных обзерверов и обзерверов бизнес кода, контроль над которым ложится на плечи разработчиков.

конкретный пример с кодом, который будет демонстрировать проблему, которую по вашему мнению нельзя будет решить с помощью наблюдателей

Я не утверждаю, что есть проблема, которую нельзя решить с помощью наблюдателей. Я лишь говрою, что без наблюдателей можно просто не иметь многих проблем.

Прошу выбирать выражения. Если вам не нравится решение, это можно выразить другим способом или вы считаете такую риторику уместной?

Видите ли, форма ведения дискуссии совершенно никак не влияет на суть излагаемого и лишь усиливает стилистически эмоциональный окрас контраргументов. Что же касается сути статьи, то по моему скромному мнению, жесткое следование принципам любой отдельно взятой парадигмы отрицательно влияет на архитектуру вцелом и финальный код, кроме того приведенные кейсы мне кажутся слишком искусственными наивными, которые оставляют неосвещенным огромное количество связанны с ними проблем, которые обязательно возникнут в реальных системах, что не могло не вызвать у меня бурную эмоциональную реакцию.

То что это антипаттерн это только по вашему мнению.

Обращу ваше внимание на сей объективный факт, что это по большей части недостоверно, и например паттерны reactive создавались с целью немного улучшить ситуацию. Основной недостаток обзервера -- это неявная последовательность обработки событий разными обработчиками, которая зависит от порядка инициализации компонентов (а в IoC он всегда неявный), и которая в свою очередь является чуть ли не основной причиной всех глюков упомянутых вами UI. Кроме того, при проектировании паттерна вылезает еще куча неявных соглашений и гарантий, которые должны быть обязательно оговорены в каждом Observable-компоненте.

  1. События синронны или асинхронны?

  2. Гарантируется ли детерминированный порядок выполнения (и какой)?

  3. Гарантируется ли доставка сообщения, если removeListener() или addListener() вызван внутри обработчика? В вашем примере поведение недетерминировано.

  4. Если один из хендлеров кинул исключение, гарантируется ли доставка сообщения другим хендлерам?

  5. Уже упомянутый: гарантируется ли вызов onEnd(), если обработчик обработал onStart(), а другой выкинул исключение?

  6. Связанная проблема корректного shutdown-а: событие может послаться уже остановленному компоненту (проблема неразрешима, если есть циклические зависимости, которые может давать паттерн observer).

  7. А если учесть, что обработчики могут провоцировать повторные события от того же компонента, то здесь образуется еще целое поле недетерминированных поведенческих сценариев:

textField.onValueChange(e -> {
  if (e.getValue().endsWith(", "))
    textField.setValue(e.getValue() + "fck, ");
});

Представим код без использования наблюдателей, с TransactionManager у которого есть 4 метода

public void create(CreateOrderRequest request) {
  // практически реальный код -- вся магия внутри TM, принцип DRY соблюден
  // бизнес код защищен от некорректного использования и забывчивости/неумения программиста
  transactionManager.transactional(() -> {
    // some domain code
  });
}

Теперь рассмотрим тот же пример, но с наблюдателями

Предположим, что мы хотим навесить более одного наблюдателя -- а кто нам мешает, раз уж дизайн позволяет? Например я хочу еще залогировать сие событие в audit таблицу в базе данных. Тогда сразу же встает проблема последовательности инициализации (которая есть неявная): если контекст фреймворком был собран так, что первый обработчик -- это TM, тогда все нормально -- второй запишет в audit таблицу уже в транзакции. Если же нет -- первым вызовется audit и упадет с ошибкой transaction required.

Ладно, вы создаете отдельный AuditService, где делаете свои обзерверы и добавляете тот же обзервер с транзакцией. А теперь мне нужно отослать данные только что созданного заказа (и еще клиента) удаленному rest-логгеру. Я вешаю обработчик на onEnd() и опять проблема с последовательностью вызовов: может случиться, что транзакция уже как бы закрыта, а мне нужно еще читать данные клиента. Или в другой транзакции будем читать?

Теперь позвольте мне сделать review вашего кода:

class OrderService {
  public void create(CreateOrderRequest request) {
    // ремарка: это нужно будет копипейстить в любом компоненте, где требуется транзакция
    try {
      // если список обзерверов может меняетья динамически,
      // то нужно делать дефенсивную копию
      // new ArrayList<>(observers).forEach(...)
      observers.forEach(observer -> observer.onStart());
      
      //some domain code
      
      // для обеспечения детерминированности вызовы onEnd()
      // должны осуществляться в обратном порядке (LIFO)
      // тот, кто первым открыл транзакцию, должен закрыть ее последней
      observers.forEach(observer -> observer.onEnd());
    } catch (Exception e) {
      // абсолютно нелогично вызывать onCreationFailed() для обработчиков,
      // для которых не был вызван onStart() - они вообще ничего не должны знать о событии
      // более того, для некоторых уже успел вызваться onEnd(), зачем им посылать onCreationFailed()?
      // они уже освободили ресурсы и забыли про операцию
      // Забыл добавить: onCreationFailed() тоже может выкинуть исключение,
      // и в этом случае остальные обработчики не получат сообщение, что приведет
      // к утечке ресурсов. Здесь нужно вызывать хендлер в try - catch и продолжать в случае ошибки.
      // А после цепочки вызовов кинуть исключение с той ошибкой.
      observers.forEach(observer -> observer.onCreationFailed(e));
    }
  }
}

// Ваш обработчик нереентерабелен (не позволяет корректно работать внутри уже созданной транзакции)
// Как надо было:
class CreateOrderObserverImpl {
  // Нет гарантии, что это prototype, поэтому использует ThreadLocal
  ThreadLocal<Boolean> isManagedByCurrent = new ThreadLocal<>();
  
  public void onStart() {
    if (!transactionManager.hasActive()) {
      transactionManager.begin();
      isManagedByCurrent.set(true);
    }
  }
  
  public void onEnd() {
    // если транзакция создалась выше, мы ее и не должны коммитить
    if (isManagedByCurrent.get()) {
      try {
        transactionManager.commit();
      } finally {
        isManagedByCurrent.clear();
      }
    }
  }
  
  public void onCreationFailed(Exception e) {
    if (isManagedByCurrent.get()) {
      try {
        transactionManager.rollback();
      } finally {
        isManagedByCurrent.clear();
      }
    }
  }
}

Уж извините за мой стиль, но наивно следуя идее "как лучше и правильней", вы на ровном месте уже получили тонны трудноотлаживаемого и трудночитаемого геморроя, к тому же нерабочего.

Правильный TransactionManager обязательно включает метод rollback().

Вообще говоря, код из (2) нужно было бы написать в конструкции try-catch-finally, также при этом используя rollback в случае исключения, но напомню, что примеры намерено упрощены. А еще лучше было бы использовать аннотацию @Transactional из какого-либо фреймворка

Так и надо было делать, а еще проверить, не создана ли уже была транзакция ранее.

Несмотря на то, что для представленного интерфейса TransactionManager можно было бы создать реализацию для большого множества баз данных, данный интерфейс раскрывает детали реализации взаимодействия с базой данных и в этом смысле является чужеродным по отношению к основной логике в изолируемом модуле (core).

С этим согласен, но предлагаемое решение -- реально жесть и мусор. Транзакции -- контекстно зависимая вещь. Чтобы скрыть детали реализации самым лучшим способом будет поставить AOP interceptor вручную или при помощи аннотации фреймворка. Если нет фреймворка -- то в тыщу раз лучше оставить TM как явную зависимость в бизнес коде.

private final List<CreateOrderObserver> observers;

Это очень хреновое решение, хуже может только использование EventBus. Обзерверы -- это антипаттерн, служащий в основном, чтобы слабо связать компоненты и передать событие от более низкого компонента в иерархии к более высокому. Он вводит в код неявные трудноразрешимые динамические зависимости, о которых ничего не известно из контекста кода. Соответственно трудно читать, трудно поддерживать, трудно отлаживать и вообще все плохо. Конкретно здесь не нужна слабая связанность нужно объявить въявную зависимость кода от TM. Тем более, зачем использовать списки, если в контексте есть только единственный хендлер. Делать излишнюю абстракцию для удовлетворения конкретного кейса себе и другим дороже.

observers.forEach(observer -> observer.onStart(context));

Как вседа оптимистичное программирование для идеального мира, где по небу летают розовые пони, а код никогда не ломается. Если свалится первый обработчик -- другие что, не получат сообщения? А должны? Если свалится дальнейший код в теле метода, обработчики не получат end, а транзакция никогда не закроется? А если первый обработчик получил onStart(), а второй свалился, нужно ли будет первому послать onEnd()? И еще куча пессимистичных нюансов, которые превратят данный паттерн в настоящий геморрой. Так что на свалку данный велосипед.

Интересная идея серверного рендеринга, однако к сожалению, годится только для небольших автономных веб страничек. Уже давно пишу на Vaadin бизнес приложения, и идея серверного state management-а на порядки упрощает разработку, позволяя ограничиться только одной платформой/языком и убирая рудиментарную прослойку ввиду API.

Но проблема в том, что в современном фронтэнде сложно ограничиться только библиотекой стилей и виджетов -- так или иначе потребуется интеграция с мощной экосистемой JS. Это было основным и правильным решением для Vaadin Flow: теперь там можно достаточно просто интегрировать JS-библиотеки, при этом не теряя возможности серверного state-management-а.

Information

Rating
Does not participate
Location
Madrid, Испания
Registered
Activity