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

Изучение Spring Framework 100% через практику. Активные туториалы и язык разметки KML. Проект KciTasks (beta)

Время на прочтение 15 мин
Количество просмотров 102K


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

В результате, мы получаем целенаправленное обучение, но БЕЗ ступоров, проблем непонимания вида «а где это использовать», а также без скучного и пассивного чтения теории. Совсем.

В рамках статьи представлено 5 учебных задачек, которые покрывают Spring Jdbc/Transactions на 50% от необходимых для сертификации знаний. Главная задача статьи — массово протестировать саму идею, а также вместе с вами разработать большое количество уникальных задачек по всем темам.

Для начала немного о себе. Меня зовут Ярослав, и я работаю на позиции Middle Java Developer в компании EPAM Systems. Одним из моих хобби является создание обучающих систем (диплом бакалавра, диплом магистра). За последние 4 года я перепробовал более сотни самых разнообразных подходов к обучению различных областей знаний (включая Java/Spring), и создал более двадцати программ-прототипов для проверки этих подходов. Большинство из них не принесло какой-либо супер-пользы, и поэтому я продолжаю работу в этом направлении.

Эта статья посвящена одной из моих идей, которая, теоретически, может взлететь. Надо её массово протестировать, в этом и заключается задача данной статьи. Забегая вперёд, рекомендую вам зайти и посмотреть веб-страницу с задачками, чтобы понимать, о чём идёт речь (https://kciray8.github.io/KciTasks/App/src/).

Я перфекционист и хочу, чтобы обучение было близким к идеальному. В моём понимании это такое обучение, которое не вызывает негативных эмоций, затягивает и приносит удовольствие. К сожалению, изучение многих современных (и высокооплачиваемых) технологий вызывает трудности. Spring Framework традиционно считается одним из самых сложных в изучении, и его изучение происходит крайне неэффективно.

Как обычно изучают Spring


В изучении фреймворков есть 2 крайности. Первая — когда человек работает в компании и просто делает те задачи, которые даёт заказчик. Это медленный, очень медленный способ развития, но по нему идут большинство разработчиков. Логично делать только то, что просят, особенно когда за это платят, не так ли? Однако, обучение «на работе» лишь кажется эффективным. Большинство задач, которые решают современные программисты, заключается в сопровождении систем. Всё уже написано до нас, нужно лишь фиксить баги\править конфиги, и дорабатывать систему напильником. В этом нет ничего плохого, но вот обучение самим технологиям происходит крайне медленно. Конечно, рано или поздно вам придётся раскопать документацию спринга и вы запомните всё необходимое, но на это уйдут годы. Может быть, стоит попробовать сначала «накрутить» знания и опыт, а потом уже брать практические задачки посложнее (и за существенно больший оклад, разумеется)? (С Java это точно работает, можно за 2-3 месяца целенаправленно изучить Java SE и это могут засчитывать за год-другой опыта. Много знакомых с универа, кто так делал).

Вторая крайность — это целенаправленное обучение. Это быстрый, но ОЧЕНЬ тяжёлый способ. Он заключается в том, что человек продвигается по книге (или курсу), а потом пытается применить это на практике и как-то запомнить. И если с Java SE такой подход ещё кое-как работает, то со спрингом всё глухо и туго. Даже в самых лучших книгах зачастую не объясняется, где конкретно применять те или иные особенности, приходится догонять это самому. Но самое обидное тут — это забывание информации, добытой таким тяжёлым трудом. Одна из проблем обучения — это отсутствие эффективного повторения. Если вы изучали спринг классическим способом (например, читали книгу и пробовали код на практике), то на это были потрачены огромные усилия, но БЕЗ возможности восстановления. Простое перечитывание книги через 1-2 года не вернёт вам забытой информации (которую вы получили через практику, параллельную с прочтением). Возникает некая дилемма — как же сделать так, чтобы было много практики, но при этом программист «направлялся» в нужные области?

Проблемы обучения


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

Для начала, выберем материалы, по которым стоит обучаться. Главное требование к ним — ограниченный набор информации (только то, что нужно для сертификации/собеседования/практики), а также последовательная и логически взаимосвязанная подача этой информации. По данным критериям, наилучшим образом подходят книги (Спринг в действии, Учебное пособие по сертификации от Юлианы и т.д.) Они очень хорошо продуманы и оформлены (на мой взгляд, куда приятнее и подробнее, чем видеокурсы по спрингу на udemy). Казалось бы, читай себе, вникай, пробуй, экспериментируй — и будут знания! Но не тут-то было.

Дело в том, что сам процесс чтения книги и разбора теории в ней очень плохо состыкован с процессом апробации этой теории на практике. Он не естественный. Какой бы ни была книга идеальной, она остаётся книгой. Она по своей природе не предназначена для обучения программированию. Программист, в конечном итоге, должен набивать хорошие привычки по сознательному использованию тех или иных технологий фреймворка. Между «Я прочитал и понял» и «Я умею это применять и знаю где» образуется огромная пропасть. Чтобы преодолеть её, приходится вложить немалые усилия. Скажу честно — я написал довольно много веб-приложений на Spring, однако всё равно испытывал множество трудностей при прочтении глав книги «Спринг в действии». На данный момент я детально разобрал около 30% из обеих книг, и готов к сертификации Spring 5 примерно на 60%.

Аналогичные проблемы я наблюдал при изучении Android SDK в 2014 году. Разгадка тут довольно простая — каждый фреймворк вводит какие-то свои новые концепции, которые довольно трудно понять и начать думать через них (если использовать традиционные способы обучения).

Процесс связывания представленной в книге информации с привычками — очень тяжёлый и не эффективный. Он часто вызывает множество негативных эмоций (когда что-то не работает или не запускается), заставляет много гуглить и решать проблемы. Неужели нельзя сделать некоторые «пути (trails)», по которым можно будет провести разработчика, показать ему правильные решения, да так, чтобы он сам их делал? Ведь опытные разработчики прекрасно знают о таких путях. Для них это что-то очевидное. Но тут возникает проблема в форме передачи знаний. Как мы уже выяснили, книги не подходят для этой цели.

Ещё одна проблема, на которую хотелось бы обратить внимание — это проблема повторения. Даже если мы преодолеем все трудности и широко прокачаем знания спринга, со временем они будут угасать (не считая того небольшого процента, который получилось связать с текущими задачами). Человеку свойственно забывать, и с этим ничего не поделать. Единственное, что мы можем сделать — это попробовать оптимизировать повторения. Когда я учился в бакалавриате, я возлагал большие надежды на теорию интервального повторения и хранение знаний в виде флеш-карт (Хабростатья про диплом бакалавра). Однако, флеш-карты слишком независимы друг от друга (не подходят для хранения связанных знаний о спринге). Даже для изучения Java их эффективность средняя. Да и они тоже не естественны, ведь программист должен повторять через практику.

Я многие годы ломал голову над тем, как сделать обучение 100% завязанным на практику (как следствие — не скучное и с высоким КПД). Сейчас я коротко расскажу о вариантах, которые были перепробованы. Для начала, я пытался найти набор подробных и учебных ТЗ, которые бы, кроме задач давали бы технические наводки (разработать такой-то REST Api используя такие-то классы). Ничего дельного я не нашёл, и потратил кучу времени. Да, я видел отдельные авторские курсы по некоторым частям спринга, но полного покрытия сертификации нигде не собрать. Да и «книжная» проблема этих туториалов остаётся (хоть и некоторые авторы пытались сделать пошаговые руководства, они всё равно имеют недостатки). P.S.: Если у вас есть что-то вроде «Учебных ТЗ» по спрингу, обязательно отправьте их мне на kciray8@gmail.com.

Одна из проблем книг и туториалов — их очень скучно и нудно читать. Это ещё больнее, чем гуглить баги. Я не хочу читать, я хочу кодить! Что, если использовать книгу только для наводки на темы и названия классов, а потом уже самостоятельно (через эксперименты и гугл) догонять всё остальное? А уже потом и главу перечитывать, пересиливая себя и собирая оставшиеся крупицы знаний. Собственно, я так и изучал спринг. Не с начала главы (унылое введение), а с середины, пытаясь за что-то ухватиться и экспериментировать вокруг этого. IDE очень помогает в этом с помощью автозаполнения, просмотра JavaDoc и исходников, удобной отладки. Я бы назвал это «изучение с помощью экспериментов с API». Я даже развил ряд особых методик вокруг этого метода, но там всё равно остаются некоторые фундаментальные проблемы.

А именно, проблема «ступоров» никуда не девается. Она вызывает по-прежнему много боли, хоть это уже более естественно и приближенно к практике (в реальных проектах придётся много таких ступоров решать, прокачать навык будет полезно). На самом деле, на этой методике можно вполне себе дойти до конечной цели (сертификация). Но это будет требовать очень много усилий, в 3-4 раза больших, чем если бы идти по накатанной дорожке. Да и проблема повторения информации всё равно остаётся. И хочется сделать что-то более приближенное к идеальному. Вдруг моя новая методика окажется полезным большому количеству людей и изменит мир? И в конце 2017 года мне пришла в голову такая идея, да так, что захотелось её сразу реализовать и изначально я делал очень большие ставки на успех.

Давайте подумаем, как происходит обучение по классическому (большей частью пассивному) туториалу. Большинство туториалов по Spring просто отвратительны по своей структуре (включая гайды на spring.io). Самый большой их недостаток, который я просто терпеть не могу — это линейность. Многие авторы «вываливают» большие куски кода, которые нужно копипастить к себе. Было бы правильнее начинать с простого примера (минимально возможной демонстрации, которую можно запустить и поэкспериментировать), и потом накручивать на него разные навороты. Принцип «от простого к сложному» — золотой закон обучения! Но нет ведь. Каждый автор считает нужным накрутить информации в 2-4 раза больше чем нужно, по кускам это разбирать и только потом запускать.

К примеру, откроем руководство по поднятию SOAP-сервиса на спринге (https://spring.io/guides/gs/producing-web-service, недавно на работе понадобилось). Они тут и spring-boot прикрутили, и wsdl4j с процессом генерации Java-классов с использованием gradle, и целый in-memory репозиторий CountryRepository (хотя простой строки «Hello world» мне хватило бы). И только в самом конце объяснили, как с помощью curl запустить всю эту систему. Нет, я конечно всё понимаю — авторы хотели дать наглядный пример «всё в одном» и разобрать его. Но, с точки зрения понимания информации, такой подход не годится.

Вы любите «читать» скучный разбор и копипастить куски кода (каждый по пол страницы)? Я это терпеть не могу. Хочу вот получать опыт экспериментальным путём, и всё тут.

Идея активных туториалов (KciTasks)


Что, если сделать некое подобие виртуального учителя, который просто даёт маленькие указания типа "сделай то и это", а в случае, если программист ошибся или забыл — учитель просто даёт фрагмент кода. По своей сути, кси-таски и являются таковыми. Суть их в том, что у нас есть маленький набор инструкций, к каждой из которых есть ответ и он под спойлером (скрыт). Разницу между ними и обычным туториалом можно увидеть на следующем графике:



Концепция проекта с чистого листа


Перед тем, как продолжить разбор активных туториалов, хотелось бы рассказать об одной важной концепции, на которой они базируются. Эту концепцию вы можете использовать совместно с любой методикой обучения, но почему-то о ней редко пишут или упоминают где-то. Так вот, суть её в том, что тренировочные проекты нужно делать с нуля. Никаких start.spring.io, каждый день заходите в File->new Project->Hello world и на нём базируете ВСЕ ваши проекты, включая веб. И все maven-зависимости тоже забиваете по-новому. Благодаря этому вы запомните зависимости между спринг-модулями, зачем нужен каждый из них и т.д. На практике это очень сильно пригождается, когда есть какие-то проблемы с зависимостями.

Практическая направленность


Любите ли вы XML? Авторы обеих книг по Spring соглашаются, что рано или поздно XML станет пережитком прошлого. Однако, они сами приводят большинство решений в двух вариантах (XML+Аннотации). Я не любил XML до тех пор, пока не устроился в большую компанию. Сейчас это просто часть работы. Слишком много готовых решений сделано, которые просто пронизаны XML и переписать их без него — потратить огромные деньги и получить несравнимо мало. Никто не будет этого делать. Поэтому, я старался чередовать XML/Аннотации в моих задачках, что и вам рекомендую. Если правильно обучаться (по описанной в статье методике), то XML не вызывает проблем, а, напротив, помогает взглянуть с другой стороны на некоторые решения и лучше их запомнить. Написание XML кода (с автозаполнениями и подсказками, с помощью IDE) также приятно, как и написание Java кода.

Приближаемся к идеалу (0.5% копипасты, 5% чтения)


Каждая инструкция в активном туториале должна быть выполнимой БЕЗ копипасты. Современные IDE позволяют умножить её на ноль. Да, даже beans.xml со всеми его приблудами, даже dependencies — всё можно сделать внутри IDE. Это намного приятнее, чем бездумно копировать код. Как я уже сказал, я хочу сделать обучение приятным и это одно из проявлений.

Каждая инструкция в кси-таске заставляет вас немножко подумать и что-то вспомнить. В этом и заключается «активность» такого туториала. Этот процесс намного приятнее, чем чтение или копирование кода. Тут нужно поддерживать баланс — инструкция не должна быть слишком тупой (иначе это будет не так приятно), и не должна быть слишком сложной (что повлечет за собой большие куски кода и проблемы, аналогичные с туториалами). Я нигде не видел подобных разработок, хоть и повидал много разных систем обучения.

Специальный язык для активных туториалов (KML)


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

Пример 1 — Создание бина JdbcTemplate


=Create a @@JdbcTemplate@@ as a @@@Bean@@ with @@DataSource@@ injected into it
+Main.java
@Bean
JdbcTemplate getJdbcTemplate(DataSource dataSource){
    return new JdbcTemplate(dataSource);
}



Пример 2 — Создание файла schema.sql


=Create a file ~~schema.sql~~ with a DDL-statement for creating a new table ##Product## (name, price)
+schema.sql
CREATE TABLE Product(name VARCHAR(100), price DOUBLE)



Полный пример таски (JdbcTemplate2.kml)
###name=Jdbc template and datasource part 2
###full = JdbcTemplate2.zip
=Create a spring-based modern application
==Create a Java project from scratch using your favorite IDE
+<<MainEmpty
==Add ##maven## support to it
==Annotate class @@Main@@ as a configuration (with automatic scan)
+<<spring-context
+<<MainConfiguration
==Create a ##run## method inside the @@Main@@ class
+Main.java
void run(){

}
==Create a new context and call @@Main.run()@@
+Main.java
public static void main(String[] args){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
    context.getBean(Main.class).run();
}
=Add Apache Derby as a @@@Bean@@ using builder (embedded)
+<<derby
+<<spring-jdbc
+Main.java
@Bean
public DataSource dataSource(){
    return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.DERBY).build();
}
=Create @@JdbcTemplate@@ as a @@@Bean@@ and inject @@DataSource@@ into it
+Main.java
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
    return new JdbcTemplate(dataSource);
}
=Retrieve the url from ##JdbcTemplate## and print it to the console
+Main.java
@Autowired
JdbcTemplate jdbcTemplate;
+Main.java
void run(){
    String url = jdbcTemplate.execute((ConnectionCallback<String>) con -> con.getMetaData().getURL());
    System.out.println(url);
}
=Run the application and ensure that the url contains "derby"
+Output
jdbc:derby:memory:testdb
=Create a file ~~schema.sql~~ with a DDL-statement for creating a new table ##Product## (name, price)
+schema.sql
CREATE TABLE Product(name VARCHAR(100), price DOUBLE)
=Create a file ~~test-data.sql~~ with a few DML-statements that insert distinct products
+test-data.sql
INSERT INTO Product VALUES('Milk', 3.5)
INSERT INTO Product VALUES('Eggs', 6)
=Inject ~~schema.sql~~ and ~~test-data.sql~~ into the ##Main## class as spring-resources
+Main.java
@Value("schema.sql")
Resource schema;

@Value("test-data.sql")
Resource testData;
=Create a resource-based database populator and use it to init the database
+Main.java
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScript(schema);
populator.addScript(testData);

DataSourceInitializer initializer = new DataSourceInitializer();
initializer.setDatabasePopulator(populator);
DatabasePopulatorUtils.execute(populator, dataSource);
=Retrive the names of the products to a list and print it
+Main.java
List<String> productNames = jdbcTemplate.queryForList("SELECT name FROM Product", String.class);
System.out.println(productNames);
=Run the application and ensure that the names of the products were printed accordingly with ~~test-data.sql~~
+Output
[Milk, Eggs]
=Create a domain object @@Product@@ (with custom !!toString!!)
+Product.java
public class Product {
    String name;
    Double price;

    @Override
    public String toString() {
        return "Product{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}
=Retrieve a @@List<Product>@@ using a row mapper and print it
+Main.java
List<Product> products = jdbcTemplate.query("SELECT * FROM Product", (RowMapper<Product>) (rs, rowNum) -> {
    Product product = new Product();
    product.name = rs.getString("name");
    product.price = rs.getDouble("price");
    return product;
});
System.out.println(products);
=Create a class with name @@ProductSet@@ that has a list of products as its member
+ProductSet.java
public class ProductSet {
    public List<Product> products = new ArrayList<>();
}
=Retrieve a @@ProductSet@@ using a result set extractor and print its products
+Main.java
ProductSet productSet = jdbcTemplate.query("SELECT * FROM Product", (ResultSetExtractor<ProductSet>) rs->{
    ProductSet set = new ProductSet();
    while (rs.next()){
        Product product = new Product();
        product.name = rs.getString("name");
        product.price = rs.getDouble("price");
        set.products.add(product);
    };
    return set;
});
System.out.println(productSet.products);
=Run the application and ensure that the products was printed correctly (two times)
+Output
[Product{name='Milk', price=3.5}, Product{name='Eggs', price=6.0}]
[Product{name='Milk', price=3.5}, Product{name='Eggs', price=6.0}]


План полного покрытия Core Spring 5.0 Certification


В интернете существует холивар о том, нужны ли сертификаты. Об этом можно написать отдельную статью, но в рамках этой статьи я просто напишу «нужны и как можно больше». Месяц назад вышел Study Guide по Spring 5 (и сам экзамен от pivotal), поэтому имеет смысл ориентироваться на него. Для нас это может служить в качестве плана, или некоторого стандартного набора, который спрашивают на собеседованиях и который может быть полезен для ваших проектов.

По каждой теме нужно сделать 6-8 уникальных задачек, которые затрагивают требуемый объём знаний и подают информацию с разных точек зрения (например, DataSource создаётся разными способами — с помощью билдеров, вручную, с использованием пропертей, через аннотации или XML, автоматически через Boot и т.д.). Потренировавшись, программист надёжно запомнит, что же такое DataSource и как он применяется (вместо того, чтобы «выучить, сдать и забыть», как делают многие). Кроме того, темы часто взаимно пересекаются (Container и AOP используются в Spring Data). Это позволяет очень хорошо углубить основы основ.

Не смотря на максимальную автоматизацию процесса через язык разметки, разработка тасок остаётся весьма трудоёмкой задачей. Особенно, если их делать качественно (демонстрируя всё в простой форме и с правильной перспективы). Например, мне потребовалось около 10 часов, чтобы разработать задачку по уровням изоляции в транзакциях. Казалось бы, нужно всего лишь продемонстрировать разницу между 4-мя уровнями, запустив транзакции параллельно. Но не тут-то было! Для H2 и MySQL не видна разница между некоторыми уровнями, и они по-разному обрабатывают конфликтные ситуации (одни БД возвращают старые копии данных, другие — вводят транзацию в режим ожидания). И только в DerbyDB наглядно можно увидеть разницу между всеми уровнями. Все авторы книг такие умные — копируют теорию, а вот показать её на практике — полноценный пример так и не получилось найти, пришлось самому выводить.

Суть моего плана в том, чтобы объединить мои усилия с вашими и вместе разработать большое количество уникальных задачек по Spring 5. Все, что требуется от вас, это отправить мне на почту *.kml-файлик с задачкой, и zip-архив проекта. Задачки будут доступны бесплатно и без регистрации, для всех, через GitHub-вебсайт. Или вы можете сделать пулл-реквест (если нужно что-то доработать в самом движке). Я верю, что вместе мы сможем создать новый принцип обучения и нести его в массы.
Тема (% от экзамена) Покрытие KciTasks Пользователи (план покрытия)
Container (28%) 10%
AOP (14%) 10%
JDBC (4%) 50%
Transactions (10%) 50% kciray8@gmail.com (+20%)
MVC (10%) 0
Security (6%) 0
REST (6%) 0
JPA Spring Data (4%) 0 kciray8@gmail.com (+50%)
Boot (14%) 0
Testing (4%) 0

Также, хочу обратить внимание на просьбу поддержать проект в виде доната (в шапке сайта баннер). Очень хотелось бы получить компенсацию по затраченным усилиям (а сообщество Java-разработчиков далеко не бедное).

Рекомендации


По каждой теме спринга можно придумать множество уникальных тасок. Например, вы можете давать указания по разработке некоторого REST API через спринг MVC, а потом получать данные с него через RestTemplate и выводить их в консоль. Получается 2 в 1, некоторое замкнутое кольцо. Вообще, простор для творчества тут очень большой, нужно лишь время и желание. И чем больше уникальных задачек, тем лучше! Если вы хотите добавить что-то своё (необычное) — вперёд. Многогранность обучения очень важна. Нужно показать одни и те же знания с большого количества разных ракурсов, тогда они хорошо запомнятся. Главное — соблюдайте баланс, о котором говорилось выше (каждая инструкция требует что-то сделать, но не слишком много). Вместе мы сможем конвертировать знания из книг в форму тасок и сделать большое дело!

P.S.: Если кто-то из читателей проходил экзамен и/или официальный курс от pivotal, напишите мне на kciray8@gmail.com.

Система обучения или система повторения?


Я вижу два пути, по которым вы можете использовать KciTasks. Первый — использовать её для углублённого изучения спринга. На мой взгляд, это должно быть эффективно. Вы просто выполняете инструкцию за инструкцией, сверяясь с решением и корректируя себя. По началу вы будете много «подглядывать» туда, но это нормально. На следующий день попробуйте подглядывать как можно меньше и всё делать самому. Все таски рассчитаны на то, чтобы быть сделанными без разворачивания спойлеров, СОВСЕМ. Вам надо к этому прийти. Обратите внимание, что вы не гуглите (!!!), не читаете доки или книжку. Таски самодостаточны, просто вчитвайтесь в инструкцию и решение к ней, и выводите знания из экспериментов.

Второй путь — использовать KciTasks для повторения. Когда вы изучаете какой-либо фрейм или язык программирования по книге/курсу, вы вкладываете много усилий. И пусть они не пропадут даром — вложите весь полученный опыт в таски, чтобы потом через полгода можно было пройти по проторенной дорожке и всё вспомнить.

Оба путя не протестированы (beta же), поэтому жду ваших отзывов. И чем подробнее, тем лучше. Ведь все люди разные, восприятие разное, и надо посмотреть реакцию сообщества. Надеюсь, что статья окажется полезной.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Взлетит?
78.73% Да 248
21.27% Нет 67
Проголосовали 315 пользователей. Воздержались 115 пользователей.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+13
Комментарии 12
Комментарии Комментарии 12

Публикации

Истории

Работа

Java разработчик
356 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн