Java EE: структура кода крупного корпоративного проекта

Крупный корпоративный проект на Java EE - структура и особенностиМы рассмотрели варианты структуры корпоративного проекта, теперь подробнее изучим конкретную структуру проекта. Предполагая, что мы моделируем корпоративную систему небольшого размера с незначительной функциональностью, преобразуем задачи проекта в кодовые структуры.

Мы уже знаем, что такое вертикальные и горизонтальные структуры модулей. Именно это нужно в первую очередь изучить при структурировании проекта.



 

Ситуация в корпоративных проектах

Типичный корпоративный проект обычно состоит из трех технически обоснован­ных слоевуровней представления, бизнес-логики и данных. Это означает, что проект имеет горизонтальную структуру в виде трех подмодулей, или пакетов.

Идея состоит в том, чтобы разделить по уровням данные, бизнес-логику и пред­ставления. Таким образом, функциональность нижнего уровня не может зависеть от более высокого — только наоборот. На уровне бизнес-логики не может быть задействована функциональность уровня представления, а уровень представ­ления может использовать функционал бизнес-логики. То же самое верно для уровня данных, который не может зависеть от уровня бизнес-логики.

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

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

Мы были свидетелями горячих дискуссий между высококвалифицирован­ными архитекторами о необходимости организации и разделения обязанностей в соответствии с техническими задачами. Однако у этого подхода есть ряд недостатков.

 

Структурирование по горизонтали и по вертикали

Чистый код — это такой код, который должны понимать люди, а не машины. То же самое касается области разработки и разделения ответственности. Мы хотим по­строить структуру, по которой инженеры легко разобрались бы в том, что собой представляет проект.

Проблема структурирования по техническим признакам на высоких уровнях абстракции заключается в том, что при этом назначение и область применения программного обеспечения искажаются и скрываются на более низких уровнях абстракции. Когда человек, незнакомый с проектом, смотрит на структуру кода, первое, что он видит, — это три технических уровня (иногда их названия и ко­личество могут быть иными). С одной стороны, это будет выглядеть знакомо, но с другой — ничего не скажет о фактической области применения программного продукта.

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

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

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

 

Структура, продиктованная бизнес-логикой

Роберт Мартин, он же дядюшка Боб, однажды описал «кричащую» архитектуру, которая должна сообщить инженеру прежде всего о том, что представляет собой корпоративный проект в целом. Чтобы, глядя на чертежи зданий, видя структу­ру и детализированный интерьер, сразу можно было сказать: вот это дом, это библио тека, а это — железнодорожная станция. То же самое справедливо и для программных систем. Взглянув на структуру проекта, программист должен сразу понять: это система учета, это система инвентаризации книжного магазина, а это — система управления заказами. Так ли это в большинстве существующих проектов? Или же, глядя на самый высокий уровень модулей и пакетов, мы можем лишь сказать: это приложение Spring, у этой системы есть уровень представления, бизнес-логики и данных, а в этой системе используется кэш Hazelcast?

Техническая реализация, безусловно, важна для нас, программистов. Однако первое, на что мы обращаем внимание, — бизнес-логика. Соответственно, эти аспекты должны отражаться в структуре проекта и модулей.

Но самое главное, это означает, что предметная область будет отражена в структуре приложения. Уже имена пакетов верхнего уровня иерархии должны давать представление о том, для чего предназначено это программное обеспече­ние. Поэтому разделение на уровни должно определяться прежде всего задачами бизнеса и только потом — деталями реализации.

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

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

Уточним терминологию: модулями мы сейчас называем бизнес-модули, реа­лизованные в виде пакетов и подпакетов Java, а не модули проекта. Поэтому термином «модуль» описывается скорее концепция, чем строгая техническая реализация.

 

Рациональное проектирование модулей

А теперь перейдем к практике: как построить структуру модулей разумного раз­мера?

Поскольку на первом месте у нас требования бизнеса, лучше всего начать с об­зора всех задач и сценариев использования приложения. Это можно сделать во время мозгового штурма, в идеале при участии специалистов по бизнесу, если это не было предпринято раньше. Каковы задачи приложения? Какие бизнес-сценарии следует реализовать? Какой подходящий функционал можно применить? Ответив на эти вопросы, вы составите представление о том, какие модули, скорее всего, будут присутствовать в проекте, не концентрируясь на внешних системах, деталях реализации или фреймворках.

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

Например, приложение для интернет-магазина может состоять из модулей, отвечающих за пользователей, рекомендации, статьи, платежи и доставку. Их основная структура может выглядеть так, как показано на рис. 1.

Структура интернет-магазина

Рис. 1. Структура интернет-магазина

Идентифицированные модули представляют собой базовые пакеты Java соз­даваемого приложения.

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

Далее в моем блоге будут показаны аналогичные причины и методологии проектиро­вания систем с распределенными приложениями. В частности, мы рассмотрим принципы проблемно-ориентированного проектирования взаимосвязанных контекстов и карты контекстов.

 

Реализация пакетных структур

Предположим, что базовая структура Java-пакетов создана. Как теперь реали­зовать внутреннюю структуру пакетов? Другими словами, какие подпакеты использовать?

 

Содержимое пакетов

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

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

Следующей не менее важной задачей является создание функциональности, реализующей различные сценарии использования. Обычно они отличаются от технических конечных точек тем, что не содержат коммуникационной логики. Бизнес-сценарии являются точкой входа в логику предметной области. Они реа­лизуются как управляемые компоненты, обычно как Stateless Sessions Beans — другими словами, EJB или управляемые компоненты CDI.

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

Следующий тип объектов — это все классы, которые обычно принято счи­тать моделями содержимого, такого как сущности, объекты-значения и объекты переноса данных. Эти классы описывают сущности из предметной области, но также могут и должны реализовывать бизнес-логику. Примерами являются сущностные объекты, которыми оперирует база данных, другие POJO-объекты и перечисления. 

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

 

Горизонтальное структурирование пакетов

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

Например, в пакете users это означало бы предусмотреть такие подпакеты, как controller, business (или core), model, data и client. Придерживаясь этого подхода, далее разделяем функционал внутри пакета users по техническим кате­гориям. Действуя последовательно, мы добьемся того, что все остальные модули и пакеты в проекте будут иметь аналогичную структуру в зависимости от их содержимого. Эта идея похожа на реализацию трехуровневой архитектуры, но только внутри модулей, соответствующих предметной области.

Один из подпакетов, например controller, будет считаться технической точкой входа. Этот пакет будет содержать коммуникационные конечные точки, инициирующие логику сценариев использования, и служить точкой входа извне приложения. На рис. 2 приведена структура горизонтально упорядоченного пакета users.

На рис. 3 эта структура реализована в виде Java-пакетов.

структура горизонтально упорядоченного пакета  

Рис. 2. Структура горизонтально упорядоченного пакета users

структура пакета на языке Java

Рис. 3. Структура пакета users на языке Java

 

Плоская структура модулей

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

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

На рис. 4 показана структура рассмотренного в примере пакета users.

Структура пакета users

Рис. 4. Структура пакета users

Преимущество такого подхода заключается в том, что он хорошо поддержива­ется языком Java. По умолчанию классы и методы Java имеют видимость только в пределах пакета. С учетом этого обстоятельства размещение всех классов в од­ном пакете улучшает инкапсуляцию и видимость. Компоненты, к которым жела­тельно обеспечить доступ извне пакета, становятся общедоступными. Все классы и методы, которые должны быть доступны только внутри пакета, имеют такую видимость по умолчанию. Таким образом, в пакете инкапсулируются все вну­тренние задачи.

 

Подход «Сущность — управление — граница»

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

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

Айвар Джекобсон (Ivar Jacobson) сформулировал термин «сущность — управ­ление — граница» (Entity Control Boundary, ECB), описывающий показанный на рис. 5 способ организации модулей.

сущность — управление — граница

Рис. 5. Модуль users в модели "сущность — управление — граница"

Пакеты

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

В корпоративном приложении Java классы пограничного пакета реализова­ны в виде управляемых объектов. Как уже отмечалось, в данном случае обычно применяется EJB.

Если логика в контуре становится слишком сложной и трудноуправляемой в рамках одного класса, то ее передают задействованным в контуре делегатам.

 

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

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

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

Основа нашего продукта — сущности и объекты-значения. В совокупности с объектами передачи данных они образуют модель модулей предметной обла­сти — объектов, которыми обычно оперирует бизнес-сценарий. Они объединены в пакет сущностей в соответствии с шаблоном ECB.

А как быть с компонентами, связанными с презентацией, и сквозными за­дачами, такими как перехватчики или фреймворки со связующей логикой? К счастью, в современных проектах Java EE все требуемые связующие функции спрятаны в пределах фреймворка. Всего несколько необхо­димых вещей, таких как самонастройка JAX-RS с классом активатора приложения, помещаются в корневой пакет проекта или в специальный пакет platform. То же самое касается и сквозных задач, таких как технически обусловленные методы- перехватчики, которые не привязаны к определенному модулю, а относятся к при­ложению в целом. Обычно таких классов не очень много, имеет смысл выделить их в особый пакет. Опасность добавления пакета platform заключается в том, что программистам, естественно, захочется добавить туда и другие компоненты. Однако он предназначен лишь для нескольких классов платформы — все остальное должно находиться в соответствующих пакетах бизнес-логики.

На рис. 6 приведен пример модуля users с использованием шаблона ECB.

модуль users с использованием шаблона ECB

 Рис. 6. модуль users с использованием шаблона ECB

Доступ к пакетам

В шаблоне ECB далеко не каждое обращение из каждого пакета имеет смысл и, соответственно, считается допустимым. В общем случае логический поток на­чинается с контура и заканчивается на пакете управления или пакете сущностей. Таким образом, у пограничного пакета есть зависимости от пакета управления (если он существует) и пакета сущностей. Использование пограничных пакетов других модулей не допускается и не имеет смысла, так как подобный пакет пред­ставляет собой отдельный бизнес-сценарий. Доступ к другому пограничному пакету означал бы обращение к тому, что должно быть отдельным, автономным бизнес-сценарием. Поэтому пограничные пакеты могут обращаться только вниз по иерархии к элементам управления.

Однако иногда зависимости и вызовы от пограничных пакетов к элементам управления других модулей допустимы и имеют смысл. Программистам нужно следить за тем, чтобы при обращении к компонентам из других модулей правильно выбиралась область транзакций. При доступе к элементам управления из других модулей также нужно проследить, чтобы объекты, с которыми они работают или которые возвращают, принадлежали соответствующим «чужим» модулям. Так бывает в нестандартных бизнес-сценариях, и это не вызывает проблем, если по­заботиться о разделении обязанностей и правильном использовании элементов управления и сущностей.

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

Сущности могут зависеть только от других сущностей. Иногда приходится импортировать элементы управления, например, если есть приемник сущностей JPA или преобразователи типа JSON-B, реализующие сложную логику. Такие технически обоснованные случаи, когда следует разрешить импортировать классы, являются исключением из общего правила. В идеале поддерживающие сущности компоненты, такие как приемники или преобразователи сущностей, должны находиться непосредственно в пакете сущностей. Из-за наличия дру­гих зависимостей и использования делегирования данное правило не всегда выполняется, но это не должно приводить к реализации чрезмерно сложных технических решений.

Все это подводит нас еще к одной важной теме.

 

Не перегружайте архитектуру

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

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

Поэтому не стоит чрезмерно усложнять или перегружать архитектуру. Если существует простой и понятный способ достичь того, что требуется в настоящее время, — просто воспользуйтесь им. Это относится не только к преждевременному рефакторингу, но и к проектированию архитектуры. Если при объединении не­скольких классов в один пакет с понятным названием будет решена поставленная задача и обеспечено создание понятной документации, почему бы не ограничиться этим? Если пограничный класс для бизнес-сценария сам выполняет всю простую логику, зачем создавать пустые делегаты?

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

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

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

 

Резюме

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

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

В самом простом случае отдельный модуль приложения может представлять собой единый Java-пакет. Это удобно, если количество классов в модуле невелико. Для более сложных модулей имеет смысл создать дополнительный иерархиче­ский уровень, используя такой шаблон, как ECB.

Инженерам-программистам следует помнить, что не нужно чрезмерно услож­нять архитектуру программного продукта. Хорошо продуманная и задокументи­рованная структура, безусловно, способствует созданию высококачественного программного обеспечения. Тем не менее всегда есть золотая середина между разумной архитектурой и чрезмерным техническим усложнением.

Рассмотрев основную структуру корпоративных проектов и способы разработ­ки модулей, спустимся на один уровень и разберем, как создаются модули проекта. В следующей статье поговорим о том, что нужно для реализации корпоративных приложений на базе Java EE.

Вас заинтересует / Intresting for you:

Аплеты Java и Интернет
Аплеты Java и Интернет 2614 просмотров Ирина Светлова Sat, 09 Jun 2018, 10:17:34
В чем опасность при использова...
В чем опасность при использова... 1006 просмотров Antoni Mon, 10 May 2021, 07:44:45
Использование потоков в Java
Использование потоков в Java 1445 просмотров Antoni Wed, 12 May 2021, 09:51:36
История языка программирования...
История языка программирования... 6818 просмотров Ирина Светлова Thu, 21 Jun 2018, 18:35:59
Войдите чтобы комментировать