Электронная библиотека » Антон Хританков » » онлайн чтение - страница 3


  • Текст добавлен: 8 ноября 2017, 22:40


Автор книги: Антон Хританков


Жанр: Прочая образовательная литература, Наука и Образование


Возрастные ограничения: +12

сообщить о неприемлемом содержимом

Текущая страница: 3 (всего у книги 11 страниц) [доступный отрывок для чтения: 3 страниц]

Шрифт:
- 100% +

ГЛАВА 2. МЕТОДЫ И ПАТТЕРНЫ ПРОЕКТИРОВАНИЯ

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

В развитии проектирования как дисциплины выделяют несколько этапов, в каждом из которых доминировала одна из парадигм проектирования. В 70-е и 80-е такой была структурная парадигма проектирования. Ее основу составляют нисходящие декомпозиционные методы, итеративно разделяющие систему на функциональные блоки, все более понятные и простые в реализации. Примерами таких методов могут служить метод постепенного уточнения (stepwise refinement) [6], метод структурного анализа и структурного проектирования SSA/SD [6], техника структурного анализа и проектирования SADT33
  Дэвид А. Марка, Клемент Л. МакГоуэн. Методология структурного анализа и проектирования: [Пер. с англ.] / Предисл. Д. Т. Росса. – М.: Фирма «Мета Технология». – 1993. – 240 с.; ил.


[Закрыть]
. Среди восходящих методов структурного проектирования следует отметить метод структурного проектирования Джексона [6].

Преимущественные нотации моделей в этой парадигме – это структурная схема, схема потоков данных, диаграмма сущность-связь, диаграммы IDEF.

В 80-е и 90-е преобладающими стали методы объектно-ориентированного проектирования. Основой методов является выделение общего описания групп объектов и использование этого описания в качестве абстрактного типа данных. Различные эвристики выделения общего описания и процедуры создания модели нашли выражение в различных методах анализа и проектирования. В результате объединения Objectory, OMT и OOAD был создан унифицированный процесс разработки RUP и язык моделирования UML. Во многом благодаря обсуждению принципов проектирования на страницах таких журналов как C++ Report были сформулированы эвристики повышения изменяемости и сопровождаемости систем SOLID [7]. Следует также отметить методы проектирования, основанные на выделении и группировке обязанностей RDD и связанные с ними эвристики назначения обязанностей GRASP [8]. А также методы, построенные на прямом использовании модели предметной области для построения программной системы. Позднее они нашли отражение в книге Эванса по предметно-ориентированному проектированию [5].

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

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

Распространенный подход к накоплению знаний в области проектирования – составление каталога повторно применимых приемов решения часто встречающихся задач проектирования, которое могут быть адаптированы для разных случаев – паттернов проектирования. В конце прошлого века были составлены первые каталоги паттернов. Наиболее известным является набор из двадцати двух паттернов «банды четырех» GoF [3]. В сфере высокоуровневого (архитектурного) проектирования аналогом паттернов выступают архитектурные стили [9], комбинации которых, исходя из требований к системе, составляют первоначальное архитектурное описание.

Результатом процесса проектирования являются отраженные в модели решения по реализации программной системы. Для прогнозирования нефункциональных характеристик системы, в том числе сопровождаемости, переносимости и других, вводят показатели проектировочного решения (метрики дизайна). Показатели используют для количественного описания размера системы, сложности решения, выявления недостатков и потенциально проблемных мест в системе. В данной книге рассматриваются показатели сложности проектировочного решения Чидамбера-Кемерера [10].

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

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

Принципы проектирования классов и интерфейсов SOLID. Аббревиатура SOLID расшифровывается по первым буквам сокращений названий принципов проектирования классов.

Single responsibility principle (SRP), принцип ограничения обязанностей, говорит, что модуль или класс должен иметь только один набор функционально сходных обязанностей.

Open-closed principle (OCP), принцип открытости-закрытости, указывает, что класс или модуль должен быть расширяем (открыт для расширения) без внесения в него изменений (закрыт для изменения).

Liskov substitution principle (LSP), принцип подстановки (описан в статье Барбары Лисков, отсюда название), указывает правило построения иерархии типов так, что любой подтип или дочерний класс подставим вместо базового типа или класса соответственно.

Interface segregation principle (ISP), принцип разделения интерфейса, говорит, что для обозначения разных ролей, которые играет класс в разных взаимодействиях, следует использовать разные интерфейсы.

Dependency inversion principle (DIP), принцип обращения зависимостей, указывает на корректное применение принципа сокрытия информации в объектно-ориентированном подходе к проектированию, когда зависимости направлены от реализации класса к выделенным абстракциям: описаниям типов данных или интерфейса

§4. РАСШИРЕННЫЕ КЛАССЫ И ОБЪЕКТЫ
ОСНОВНЫЕ ПОНЯТИЯ

Квалификатор (qualifier) используется для разделения всех связей ассоциации на подмножества согласно уникальным ключам. Обычно в бинарной ассоциации «один-ко-многим» по ключу выделяют связи «один-к-одному».

Класс ассоциации (association class) используют, когда логическое отношение между объектами обладает сложной структурой или поведением. Класс ассоциации является одновременно и ассоциацией, и классом, поэтому может иметь свойства и операции.

Напомним, что операция – это поведенческая черта класса, которая определяется именем, набором параметров, их типами и кратностями, типом и кратностью возвращаемого значения. В дополнение к этому, можно задать ограничения на реализацию операции: предусловие (precondition), постусловие (postcondition), ограничение возвращаемого значение (body condition).

Если операция не изменяет значения свойств класса, то к операции добавляется украшение запрос (query).

Каждый параметр операции может иметь имя, тип, множественность. Параметру можно задать направление параметра (in, out, inout, return), значение по-умолчанию, ограничение на множественный параметр: упорядоченность значений (ordered), уникальность значений (unique).

Производные свойства класса (derived property) вычисляются на основе других свойств или результатов вызова операций класса или других классов модели. Способ вычисления значений свойства указывается вместе с определением самого свойства, при этом вычисление значений не должно иметь сторонних эффектов и должно быть идемпотентным (повторные вычисления дают тот же результат при неизменности остальной модели). Часто используются производные свойства, значения которых составлены из объединения union множеств значений свойств, выделяющих подмножества subsets в базовых свойствах.

Шаблонные классы (template class) по сути не являются полноценными классами, а только их заготовками, определенными с точностью до значений параметров шаблона. Шаблонным может быть не только класс, но и другие элементы модели. Операция присвоения значения параметру называется связыванием (bind). По смыслу, связывание класса с шаблонным классом с приданием значения параметру аналогично созданию класса из шаблона с указанным значением параметра и наследованием от этого класса.

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

Множество обобщения (generalization set) позволяет логически группировать отношения обобщения. Можно указать, что в данном множестве обобщений приведены все возможные уточнения базового класса, или что совмещение непосредственно уточняющих классов в одном экземпляре или подклассе не допускается.

Супертип (powertype) используется совместно с множеством обобщений. Супертип это классификатор, экземплярами которого являются классы-элементы множества обобщений.

Пакеты (package) позволяют группировать элементы модели под общим именем. Для обращения к элементу пакета необходимо использовать квалифицированное имя, состоящее из имени пакета и имени элемента.

Между пакетами определены отношения доступа «access», которое для элементов пакета делает доступным элементы указанного пакета без необходимости указания квалифицированного имени, и отношение импорта «import», которое аналогично доступу, но делает элементы также доступными при последующем импорте или доступе из другого пакета.

Отношение объединения пакетов (merge) «merge» позволяет объединить в одном элементе определения этого элемента в других пакетах.



Сигналом (signal) называют особый вид классификатора, экземпляром которого является сообшение, передаваемое асинхронно отправителем получателю или группе получателей. Для того, чтобы обрабатывать сигналы, получатель должен быть активным классом (active class), объявлять черту поведения – получение сигнала (reception), с которой может быть связан метод, либо определять собственное поведение, которое обрабатывает поступающие сигналы.


ЗАДАЧИ

4.1. На рис. 11 представлены шаблонные интерфейсы Map и Entry. Интерфейс Map позволяет по ключу типа K получить значение типа V. Интерфейс Entry представляет собой пару значений.

а. Измените модель так, чтобы шаблон Entry использовал параметры шаблона Map.

б. Определите интерфейс Map_StringInteger, который указывает String типом ключа и Integer типом значения в шаблоне Map.

в. Сколько операций содержит интерфейс Map_StringInteger? Ответ поясните.

4.2. Диск Disk содержит несколько папок Folder, которые могут содержать файлы File и папки. Произведения Composition хранятся на дисках в виде файлов.

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

б. Дополните модель, укажите, что произведение может быть картинкой Picture, либо музыкой Music, либо фильмом Movie.

в. Может ли произведение храниться на одном диске в разных файлах? Ответ поясните.

г. (*) Сравните способы реализации в модели хранения произведения в нескольких файлах на одном диске. Приведите примеры на диаграмме экземпляров.



4.3. На заседании Meeting обсуждается discuss не менее одного вопроса Issue. Вопрос может быть посвящен обсуждению артефакта Artifact. В каждом вопросе должно быть указано текстовое название, числовой код и имя автора.

а. Добавьте в модель вопрос по постановлению, отдельный вопрос и сложный вопрос.

б. Укажите, что постановление Resolution связано с вопросом по постановлению, как тема topic, и с несколькими артефактами documents.

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

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

4.4. Интерфейс работы с ассоциативным массивом Map в своем пространстве имен содержит интерфейс работы с элементом массива Entry. При этом реализации интерфейса Map включают несколько реализаций интерфейса Entry.

а. Добавьте в модель класс HashMap, реализующий ассоциативный массив с помощью хэш-таблицы, и класс HashEntry, реализующий интерфейс работы с элементом массива.

б. Пусть в классе HashMap определена частная операция изменения размера resize. При каких условиях данная операция будет доступна классу HashEntry?

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

4.5. Класс CPluginsSettings хранит пути к директориям, в которых могут находиться подключаемые модули. Класс управления подключаемыми модулями CPluginManager связан с CPluginsSettings. Модули входят в состав динамической библиотеки, содержащей доступные извне функцию получения количества подключаемых модулей GetPluginsCount и функцию GetPlugin с параметром типа Integer, которая возвращает экземпляр модуля IPlugin.

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

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

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

4.6. В системе из задачи 4.5 реализована модель обработки сообщений с помощью паттерна Observer: событие представлено классом Event. источник событий представлен абстрактным классом EventSource, обработчик событий – интерфейсом IEventListener. Класс EventSource определяет общедоступную операцию добавления обработчиков и защищенную операцию возбуждения событий, которая вызывается в дочерних классах и приводит к рассылке события всем обработчикам.

а. Добавьте в модель источники событий управляющего компонента EngineSource и представления UISource.

б. Укажите, что добавленные источники создают события, специфичные этим источникам, и поэтому принимают только специализированные обработчики IEngineListener и IUIListener соответственно.

в. Каждый модуль (см. задачу 4.5) может обрабатывать события управления и представления. Добавьте класс PluginEventHandlers, содержащий IEngineListener и IUIListener. При этом нужен доступ к обработчикам событий в конкретном экземпляре модуля.

4.7. В классе Object определена операция сравнения equals с параметром obj типа Object и определен метод (behavior), реализующий данную операцию. Дочерний класс Rectangle перекрывает и реализует операцию equals. Другой дочерний класс Clickable класса Object также перекрывает и реализует операцию equals.

а. Пусть экземпляр класса Rectangle присвоен переменной var класса Object. Какая из реализаций будет выполнена при вызове операции equals у переменной var c параметром var? Ответ поясните.

б. (*) Добавьте в модель класс Button дочерним к Rectangle и Clickable. Используя механизм переопределения (redefinition), добавьте в класс Button операцию equals, реализация которой использует наследованные операции сравнения в зависимости от типа параметра obj времени выполнения. Приведите реализацию операции на подходящем языке программирования.

4.8. Массив Table состоит из нескольких элементов Element.

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

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

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

4.9. Преподаватель Teacher ведет teaches несколько курсов CourseOffering.

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

б. Укажите, что преподаватель ведет семинары как ассистент assistant и читает лекции как лектор lecturer.

в. (*) Измените свойство teaches так, чтобы оно всегда указывало только на курсы, по которым преподаватель читает лекции или ведет семинары.

4.10. Каждый экземпляр абстрактного класса контроллер Controller связан по ассоциации Sensor с несколькими датчиками поезда TrainSensor. В ассоциации контроллер играет роль управляющего controller. Датчик поезда участвует в ассоциации как датчик sensor с частной видимостью.

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

б. Измените класс контроллера, укажите, что класс принимает сигналы приближения поезда TrainSpotted и отдаления поезда TrainLeft, имеет общедоступную операцию выполнения команд execute с параметром команда cmd типа данных Command и возвращает значение типа данных Result.

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

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

4.11. Игрок Player заключает контракт Contract c командой Team. Команда может заключить до двадцати контрактов с разными игроками. Контракт заключается на определенный срок period с компенсацией salary (класс ассоциации). В контракте игрок указан работником worker, команда – нанимателем employer.

а. Добавьте в контракт пункты Item, каждый из которых содержит текст statement. Укажите, что менеджер Manager управляет manages контрактами.

б. Укажите, что игрок может быть нападающим Forward, защитником Guard или центровым Center. Игрок не может иметь несколько специализаций.

в. Игроки реагируют на команды тренера Coach. Во время игры, тренер может отправлять им указания: нападать attack, перейти к обороне guard, играть совместно join с другим игроком peer. Работа тренера состоит во взаимодействии и обучении Train команды.

г. Измените модель таким образом, чтобы игроки могли изменять специализацию: нападающий, защитник или центровой. Решение поясните.

4.12. (*) В файловой системе диска Disk данные сохраняются в кластерах Cluster, информация о связанных цепочках кластеров записана в таблице размещения файлов FAT. Таблица содержит записи FAT Record, номер записи в таблице соответствует номеру cl_no соответствующего кластера при этом номер кластера cl_no на диске. Содержимое записи указывает либо на следующий кластер, либо на конец цепочки EOF. Кластер директории Folder содержит entries упорядоченный набор отметок Entry о файлах и директориях. Каждая из записей указывает имя name и начальный кластер first.

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

б. Добавьте кластер данных File, содержащий данные файлов. Операция getData позволяет получить данные файла в виде массива с элементами типа Byte с диапазоном значений от 0 до 255.

в. Добавьте резервные кластеры Reserved, укажите, что отметка об испорченности Bad, пустоте Empty кластера хранится в FAT Record.

г. Приведите пример диска с таблицей размещения файлов на пять кластеров с одной директорией folder и двумя файлами A и B. Файл с именем B размещен в кластере с номером 4, файл A в кластерах 2 и 3. Таблица размещается в нулевом кластере, корневая директория в первом.

4.13. Процессом рецензирования Review управляет Manages пользователь User в роли редактора editor. Процесс включает разговор Conversation с пользователями автором author и разговоры с пользователям-рецензентами peers. Разговор состоит msgs из сообщений Message. Каждое сообщение связано с одним пользователем – отправителем sender сообщения, и несколькими получателями recipients. К сообщению может быть прикреплен документ Document в виде вложения attachment.

а. Укажите, что множество участников participants разговора включает всех получателей и отправителей сообщений.

б. Пусть процесс рецензирования связан с рукописью manuscript, которая является документом. Уточните модель, укажите, что рукопись получена как вложение в одном из сообщений разговора с автором.

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

4.14. Кабина лифта Cabin содержит кнопки Button и датчики Sensor. В классе Button имеется операция определения нажатия isPressed, которая возвращает логическое значение. Кнопки бывают двух видов: служебные кнопки UtilityButton и кнопки этажей FloorButton. В классе FloorButton определена операция сброса reset. Известно, что класс Cabin реализует собственное поведение, а класс Sensor реализует паттерн Template Method.

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

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

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

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

4.15. Рассмотрим модель, которая описывает графы вычислений. Действия Action и ребра Edge являются элементами Element модели. Ребро указывает направление вычисления и может иметь начальное действие from и конечное to.

а. Добавьте в модель симметричные действия сложения и умножения и одноместные действия обращения и получения противоположного элемента. Эти действия принимают и возвращают значения по ребрам Edge. Класс Value уточняет Edge и имеет атрибут val, но не имеет источника from, откуда получить значение.

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

в. Приведите пример графа вычислений выражения 4 * (а + b). Введите необходимые типы значений.

4.16. В графическом редакторе использованы фигуры: отрезок Line между начальной и конечной точками Point и дуга Arc второго порядка, построенная по упорядоченному набору из трех точек. Фигуры позволяют задать свое положение точками и умеют рисовать paint себя на холсте Canvas.

а. Определите в подходящем существующем или новом классе статические операции создания отрезков и дуг по передаваемым точкам, и операцию создания точек по координатам на плоскости. Примените паттерн Factory Method.

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

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



г. Для замкнутых ломаных без самопересечений Polygon определите операцию заливки fill внутренней области одним из трех цветов RGB: красным, зеленым или синим. Примените паттерн Decorator. В каком классе следует реализовать проверку свойств замкнутости и отсутствия самопересечений ломаных?

4.17. Заготовка модели биллинга приведена на рис. 12. Экземпляры транзакций Transaction формируются, когда пользователь с учетной записью Account совершает действие по расходованию ресурса Resource согласно тарифу Tariff.

а. Покажите на диаграмме, что учетные записи пользователей бывают временные Temporary и постоянные Permanent с одной стороны, и корпоративные Corporate и индивидуальные Personal с другой. Для временной записи заданы время старта и завершения. Используйте множества обобщений и супертипы Type и Time.

б. Используя паттерн Decorator, добавьте динамическую учетную запись DynAccount, которая может менять тип и время действия по определенным ранее вариантам.

в. Покажите на диаграмме состояние системы после совершения двух транзакций по ресурсам r1 и r2 постоянным корпоративным пользователем с DynAccount по тарифу basicTariff. В каждой транзакции расходуется две единицы ресурса.

4.18. В Domain-Driven Design при моделировании предметной области используются особые виды типов данных и классов: объект-значение Value Object, сущность Entity, корень агрегата Root. В дополнение к определению класса UML используемые в DDD виды классов несут дополнительную информацию, которая не может быть представлена в стандартной метамодели UML. На рис. 13 показана заготовка профиля.

а. Используя уже имеющиеся стереотипы, добавьте в заготовку профиля виды классов Entity и Root. Приведите решение на диаграмме профилей.

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

в. Покажите, что некоторые пакеты могут быть модулями Module. Модуль определяет операцию encloses проверки принадлежности элементов непосредственно модулю или пакету, вложенному в него.

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

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

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


Внимание! Это не конец книги.

Если начало книги вам понравилось, то полную версию можно приобрести у нашего партнёра - распространителя легального контента. Поддержите автора!

Страницы книги >> Предыдущая | 1 2 3
  • 0 Оценок: 0

Правообладателям!

Данное произведение размещено по согласованию с ООО "ЛитРес" (20% исходного текста). Если размещение книги нарушает чьи-либо права, то сообщите об этом.

Читателям!

Оплатили, но не знаете что делать дальше?


Популярные книги за неделю


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