ВСТУПИТЕЛЬНАЯ ЛЕКЦИЯ. ИСТОРИЯ РАЗВИТИЯ. 6 страница

Тип языка Последовательные системы Параллельные системы
Алгебраический Larch Lotos
Основанный на моделях Z, VDM, B CSP, сети Петри

 

13.2. Специфицирование интерфейсов

 

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

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

 

Рис. 13.4. Объекты интерфейсов подсистем

 

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

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

Структура спецификации объекта показана на рис. 13.5 и состоит из четырех компонентов.

• Введение, где объявляется класс (sort) объектов. Класс — это общее название для множества объектов. Он обычно реализуется как тип данных. Введение может также включать объявление импорта (imports), где указываются имена спецификаций, определяющие другие классы. Импортирование спецификаций делает эти классы доступными для использования.

• Описательная часть, в которой неформально описываются операции, ассоциированные с классом. Это делает формальную спецификацию более простой для понимания. Формальная спецификация дополняет это описание, обеспечивая однозначный синтаксис и семантику операций.

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

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

 

Рис. 13.5. Структура алгебраической спецификации

 

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

1. Структурирование спецификации. Представление неформальной спецификации интерфейса в виде множества абстрактных типов данных или объектных классов. Также неформально определяются операции, ассоциированные с каждым классом.

2. Именование спецификаций. Задаются имена для каждой спецификации абстрактного типа, определяются параметры спецификаций (если они необходимы) и имена определяемых классов.

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

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

5. Определение синтаксиса операций. Определение синтаксиса и параметров для каждой операции. Это часть сигнатуры формальной спецификации.

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

Для пояснения методики алгебраической спецификации, рассмотрим простую структуру данных связанного списка, спецификация которого показана на рис. 13.6.

Предположим, что первый этап разработки спецификации списка, а именно структурирование спецификации, выполнен. Имя спецификации и имя класса может быть одним и тем же, хотя полезно проводить различие между ними, используя какое-либо соглашение. Например, я использую заглавные буквы для имени спецификации (LIST) и прописные буквы с первой заглавной буквой для имени класса (List). Поскольку списки могут содержать элементы разных типов, спецификация имеет общий параметр Elem (Элемент). Тип Elem может представлять целое число, строку, список и т.д.

 

Рис. 13.6. Спецификация связанного списка

 

В общем случае для каждого абстрактного типа данных набор необходимых операций должен содержать операцию по созданию нового экземпляра этого типа данных и операцию по конструированию экземпляра данного типа из имеющихся элементов (в нашем примере это операции Create и Cons). В случае применения списков также должны быть операции для получения значения первого элемента списка (в нашем примере операция Head), операция, которая изменяет список путем удаления его первого элемента (Tail), и операция подсчета количества элементов в списке (Length).

При определении синтаксиса этих операций следует установить, какие для них необходимы параметры и каковы должны быть результаты операций. В общем случае входные параметры принадлежат или определяемому классу (в данном случае класс List) или более общему (родовому) классу. Результаты операций также принадлежат тем же классам или некоторому другому, например Integer или Boolean; операция Length возвращает целое число. Декларация импорта, объявляющая использование спецификации целых чисел, должна быть включена в спецификацию. Аксиомы, определяющие семантику абстрактного типа данных, написаны с использованием ранее определенных операций.

Операции над абстрактным типом данных обычно относятся к одному из двух классов.

1. Операции конструирования, которые создают или изменяют объекты класса. Обычно их называют Create (Создать), Update (Изменить), Add (Добавить) или, как в нашем случае. Cons (Конструирование).

2. Операции проверки, которые возвращают атрибуты класса. Обычно им дают имена, соответствующие именам атрибута, или имена, подобные Eval (Значение), Get (Получить) и т.п.

Хорошим эмпирическим правилом для написания алгебраической спецификации является создание аксиом для каждой операции конструирования с применением всех операций проверки. Это означает, что если есть m операций конструирования и n операций проверки, то должно быть определено mxn аксиом.

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

В спецификации списка операциями конструирования являются Create, Cons и Tail, которые создают списки. Операциями проверки являются Head и Length, которые используются для получения значений атрибутов списка. Операция Tail не является примитивной конструкцией, поэтому для нее можно не определять аксиомы с использованием операций Head и Length, но в таком случае Tail необходимо определить посредством примитивных конструкций.

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

Иногда проще понять рекурсивные преобразования, используя короткий пример. Предположим, что есть список [5, 7], где элемент 5 — начало (вершина) списка, а элемент 7 — конец списка. Операция Cons([5, 7], 9) должна возвратить список [5, 7, 9], а операция Tail, примененная к этому списку, должна возвратить список [7, 9]. Приведем последовательность рекурсивных преобразований, приводящую к этому результату.

 

Здесь систематически использовались аксиомы для Tail, что привело к ожидаемому результату. Аксиому для операции Head можно проверить подобным способом.

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

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

1. Enter (Ввод). Добавляет самолет (представляемый идентификатором) в воздушное пространство на указанной высоте. На этой высоте или на расстоянии ближе 300 метров от него не должно быть другого самолета.

2. Leave (Выход). Удаляет указанный самолет из контролируемого сектора. Она применяется, если самолет перемещается в соседний сектор.

3. Move (Перемещение). Перемещает самолет с одной высоты на другую. Опять проверяются условия безопасности — расстояние между самолетами должно быть не менее 300 метров.

4. Lookup (Просмотр). Определяет текущую высоту самолета в секторе.

Определение этих операций упростится, если определены также другие операции.

1. Create (Создать). Стандартная операция для любого абстрактного типа данных. Она создает пустой экземпляр данного типа. В нашем случае эта операция задает сектор, в котором отсутствуют самолеты.

2. Put (Поместить). Более простая версия операции Enter. Она добавляет новый самолет в сектор без проверки ограничений.

3. In-space (Проверка пространства). Возвращает значение истины, если указанный самолет находится в контролируемом секторе, в противном случае ее значение ложно.

4. Occupied (Занятый). Возвращает значение истины, если внутри 300-метровой зоны по высоте есть самолет, в противном случае ее значение ложно.

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

По существу, основными операциями конструирования являются Create и Put, которые использованы в спецификациях других операций. Occupied и In-space являются операциями проверки, которые определяют использование операций Create и Put. Как выполняются эти и другие операции, легко понять из спецификации.

 

Рис. 13.7. Спецификация класса Sector, представляющего сектор контролируемого воздушного пространства


Лекция 14. Спецификация поведения систем

 

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

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

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

Формальные спецификации могут быть трудными для чтения и громоздкими, особенно если используются большие математические формулы. Это замедляет разработку ПО. Разработчики метода Z обратили внимание на эту проблему. В этом методе спецификации представляются как неформальный текст, дополненный формальными описаниями. Формальная часть спецификации состоит из небольших простых описаний (называемых схемами), которые визуально отделяются от остального текста спецификации (рис. 14.1). Схемы используются для введения переменных состояний и определения ограничений и операций над состояниями. В методе также предусмотрены операции, выполняемые над схемами, в частности для построения, переименования и сокрытия схем.

 

Рис. 14.1. Структура Z схемы

 

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

Для иллюстрации применения метода Z в разработке спецификации критической системы рассмотрим упрощенный пример системы инсулинового насоса, используемой диабетиками. У диабетиков нарушен естественный метаболизм сахара, им требуются инъекции инсулина — гормона, необходимого для метаболизма глюкозы. Данная система контролирует уровень сахара в крови больного и, если требуется, производит автоматическую инъекцию инсулина.

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

Рис. 14.2. Блок схема инсулинового насоса

 

1. Набор игл. Подсоединены к насосу, используются для инъекции инсулина в тело диабетика.

2. Датчик. Измеряет уровень сахара в крови больного. В формальной спецификации операция получения данных от датчика названа reading?.

3. Насос. Передает инсулин из резервуара к набору игл. В формальной спецификации величина дозы инсулина названа dose.

4. Управляющее устройство. Управляет объектами системы. Имеет переключатель «Включено — Выключено», кнопку отмены и кнопку установки дозы инсулина. Для упрощения формальной спецификации эти элементы управления не описаны.

5. Устройство аварийной сигнализации. Подает звуковой сигнал при возникновении проблем. В спецификации сигнал на входе устройства аварийной сигнализации обозначен как alarm!.

6. Индикаторы. Имеется два индикатора: один показывает последний анализ уровня сахара в крови, другой — информацию, выдаваемую системой пользователю. Эти индикаторы в формальной спецификации обозначаются displayl! и display2!.

7. Часы. Обеспечивают управляющее устройство значением текущего времени.

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

Основная схема Insulin-pump (инсулиновый насос), которая моделирует состояния инсулинового насоса, показана на рис. 14.3. Схема разбита на две части. В верхней части объявляются имена и типы, в нижней приведены условия, которые должны всегда выполняться.

 

Рис. 14.3. Z схема инсулинового насоса

 

Состояние системы моделируется посредством ряда переменных. В соответствии с соглашениями, принятыми в методе Z, имена, заканчивающиеся знаком «?», используются для представления входных данных, а имена, заканчивающиеся знаком «!» представляют выходные данные. В схеме инсулинового насоса объявлены следующие имена.

1. reading?. Это неотрицательное целое число, которое представляет данные от датчика, определяющего количество сахара в крови. Это входная величина.

2. dose, cumulative_dose. Это также натуральные числа, представляющие соответственно дозу инсулина и суммарную дозу инсулина, введенную за определенный период времени.

3. r0, r1, r2. Представляют последние три значения, полученные от датчика, и используются для вычисления изменения сахара в крови.

4. capacity. Натуральное число, представляющее объем инсулина в хранилище (резервуаре).

5. alarm!. Эта выходная величина сообщает об аварийных ситуациях в системе.

6. pump!. Это натуральное число, представляющее управляющие сигналы, посылаемые к физическому насосу.

7. display1!, display2!. Эти выходные величины строкового типа представляют два текстовых индикатора. Один индикатор используется для отображения текстовых сообщений, другой — для показа введенной дозы инсулина.

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

1. Доза должна быть меньше или равна количеству инсулина в резервуаре.

2. Однократная доза не должна превышать 5 единиц инсулина, а общая доза, введенная за определенный промежуток времени, — 50 единиц инсулина. Это условия безопасности.

3. display1!. Показывает сообщения о состоянии инсулинового резервуара. Если резервуар содержит 40 или больше единиц инсулина, сообщений нет. Когда в резервуаре инсулина от 10 до 40 единиц, на дисплее отображается предупреждение, если же в резервуаре меньше 10 единиц, звучит звуковой сигнал и отображается соответствующее предупреждение.

Работа инсулинового насоса заключается в определении каждые 10 минут количества сахара в крови пациента и введении инсулина, если уровень сахара увеличивается (это очень упрощенное описание). Вводимое количество инсулина вычисляется согласно схеме DOSAGE (Дозировка), которая приведена на рис. 14.4. Из этой схемы видно, что на вводимую дозу влияет множество различных факторов.

 

Рис. 14.4. Z схема вычисления дозы

 

На рис. 14.4 показано часто используемое средство метода Z, а именно дельта схема. Если имя схемы включено в раздел описаний, то это равносильно включению всех имен данной схемы, а ее условия включаются в предикатную часть. В данном случае в схему DOSAGE включаются имена и условия схемы lnsulin_Pump. Если имени схемы предшествует символ «Δ», то вводится новый набор величин, имена которых совпадают с именами данной схемы, но к ним добавляется символ «'» (апостроф). Так обозначаются значения переменных состояний, измененные после выполнения операции. Например, если операция изменяет значение переменной val, то после операции эта переменная будет обозначаться val’. Дельта-схема DOSAGE вводит имена capacity', cumulative_dose’ и т.д.

Схемы, моделирующие выходные данные инсулинового насоса, показаны на рис. 14.5. Это модели индикаторов и устройства аварийной сигнализации. Здесь также используется дельта-схема. Из схемы DISPLAY видно, что индикатор display2! показывает вычисленную дозу (Nat_to_String — функция преобразования), индикатор display1! отображает или предупреждающее сообщение, или ОК. В схеме ALARM показаны условия, когда активизируется аварийная сигнализация. Она включается, если уровень сахара в крови очень низкий (меньше 3) или слишком высокий (больше 30).

 

Рис. 14.5. Схема выходных данных

 

Предикаты во всех Z-схемах должны быть .согласованы, т.е. не должно быть условий в одной схеме, которые противоречат предикатам в другой схеме. Если в спецификации замечены противоречия, можно применить различные математические методы анализа Z-спецификации. Рассматривая представленные четыре Z-схемы, вы могли заметить противоречия, которые сознательно введены, но которые могли бы появиться в реальной спецификации. В общей схеме инсулинового насоса lnsulin_Pump установлено, что индикатор display1! должен показывать состояние резервуара инсулина. Однако в схеме DISPLAY этот индикатор должен показывать уровень сахара в крови. Здесь противоречие, которое следует разрешить при обсуждении системы с медицинскими экспертами и потенциальными пользователями.

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

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


Лекция 15.Архитектурное проектирование

 

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

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

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

1. Структурирование системы. Программная система структурируется в виде совокупности относительно независимых подсистем. Также определяются взаимодействия между подсистемами.

2. Моделирование управления. Разрабатывается базовая модель управления взаимоотношениями между частями системы.

3. Модульная декомпозиция. Каждая определенная на первом этапе подсистема разбивается на отдельные модули. Здесь определяются типы модулей и типы их взаимосвязей.

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

Четких различий между подсистемами и модулями нет, но, будут полезными следующие определения.

1. Подсистема — это система (т.е. удовлетворяет «классическому» определению «система»), операции (методы) которой не зависят от сервисов, предоставляемых другими подсистемами. Подсистемы состоят из модулей и имеют определенные интерфейсы, с помощью которых взаимодействуют с другими подсистемами.

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

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

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

2. Динамическая модель процессов, в которой представлена организация процессов во время работы системы.

3. Интерфейсная модель, которая определяет сервисы, предоставляемые каждой подсистемой через общий интерфейс.

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

Ряд исследователей при описании архитектуры систем предлагают использовать специальные языки описания архитектур. В них основными архитектурными элементами являются компоненты и коннекторы (объединяющие звенья); эти языки также предлагают принципы и правила построения архитектур. Однако, как и другие специализированные языки, они имеют один недостаток, а именно: все они понятны только освоившим их специалистам и почти не используются на практике. Фактически использование языков описания архитектур только усложняет анализ систем. Поэтому, для описания архитектур лучше использовать неформальные модели и системы нотации, подобные предлагаемой, например, унифицированный язык моделирования UML.

Архитектура системы может строиться в соответствии с определенной архитектурной моделью. Очень важно знать эти модели, их недостатки, преимущества и возможности применения.

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

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

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

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

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

4. Надежность. В этом случае следует разработать архитектуру с включением избыточных компонентов, чтобы можно было заменять и обновлять их, не прерывая работу системы.

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

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








Дата добавления: 2015-03-23; просмотров: 1038;


Поиск по сайту:

При помощи поиска вы сможете найти нужную вам информацию.

Поделитесь с друзьями:

Если вам перенёс пользу информационный материал, или помог в учебе – поделитесь этим сайтом с друзьями и знакомыми.
helpiks.org - Хелпикс.Орг - 2014-2024 год. Материал сайта представляется для ознакомительного и учебного использования. | Поддержка
Генерация страницы за: 0.038 сек.