Электронная библиотека » Кент Бек » » онлайн чтение - страница 3


  • Текст добавлен: 30 июня 2017, 19:58


Автор книги: Кент Бек


Жанр: Зарубежная компьютерная литература, Зарубежная литература


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

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

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

Шрифт:
- 100% +
4. Данные должны быть закрытыми

$5 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым членом класса

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов


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


public void testMultiplication() {

Dollar five = new Dollar(5);

Dollar product = five.times(2);

assertEquals(10, product.amount);

product = five.times(3); assertEquals(15, product.amount);

}


Мы можем переписать первую проверку и сравнить в ней объекты Dollar:


public void testMultiplication() {

Dollar five = new Dollar(5);

Dollar product = five.times(2);

assertEquals(new Dollar(10), product);

product = five.times(3);

assertEquals(15, product.amount);

}


Выглядит неплохо, поэтому перепишем и вторую проверку:


public void testMultiplication() {

Dollar five = new Dollar(5);

Dollar product = five.times(2);

assertEquals(new Dollar(10), product);

product = five.times(3);

assertEquals(new Dollar(15), product);

}


Теперь нам не нужна вспомогательная переменная product, поэтому устраним ее:


public void testMultiplication() {

Dollar five= new Dollar(5);

assertEquals(new Dollar(10), five.times(2));

assertEquals(new Dollar(15), five.times(3));

}


Согласитесь, этот вариант теста значительно нагляднее.

Учтем внесенные изменения. Теперь только класс Dollar использует переменную экземпляра amount, поэтому мы можем сделать ее закрытой:


Dollar

private int amount;

$5 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым членом класса

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов


Вычеркиваем еще один пункт из списка задач. Заметьте, мы подвергли себя риску: если тест, проверяющий равенство, не смог бы точно определить корректность операции сравнения, тогда и тест умножения не смог бы проверить, правильно ли оно работает. В TDD принято активное управление риском. Мы не гонимся за совершенством. Выражая все двумя способами – тестами и кодом, – мы надеемся уменьшить дефекты настолько, чтобы уверенно идти дальше. Время от времени наши рассуждения будут нас подводить, позволяя появляться ошибкам. Когда это случится, мы вспомним урок о том, что надо написать тест и двигаться дальше. Все остальное время мы отважно продвигаемся вперед под победно развевающейся зеленой полоской нашего индикатора (вообще-то мой индикатор не развевается, но я люблю помечтать).

Подведем итоги:

□ использовали только что разработанную функциональность для улучшения теста;

□ заметили, что, если одновременно два теста терпят неудачу, наши дела плохи;

□ продолжили несмотря на риск;

□ использовали новую функциональность тестируемого объекта для уменьшения зависимости между тестами и кодом.

5. Поговорим о франках

$5 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar? Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 10 CHF


Можем ли мы приступить к реализации первого, самого интересного теста в данном списке? Мне все еще кажется, что это будет слишком большой шаг. Я не представляю себе, как можно написать этот тест за один маленький шажок. Мне кажется, что вначале необходимо создать объект наподобие Dollar, который соответствовал бы не долларам, а франкам. Пусть это будет объект с названием Franc. Для начала объект Franc может функционировать в точности как объект Dollar – если у нас будет такой объект, нам будет проще размышлять о реализации теста, связанного со смешанным сложением двух разных валют.

А если объект Franc работает так же, как объект Dollar, значит, мы можем просто скопировать и слегка отредактировать тест для объекта Dollar:


public void testFrancMultiplication() {

Franc five = new Franc(5);

assertEquals(new Franc(10), five.times(2));

assertEquals(new Franc(15), five.times(3));

}


(Хорошо, что в главе 4 мы упростили тест для Dollar. Благодаря этому работа по редактированию теста существенно упростилась. Похоже, в данной книге дела идут довольно гладко, однако я не могут гарантировать, что в будущем все будет так же хорошо.)

Теперь нам надо получить зеленую полоску. Какой способ будет самым простым? Проще всего скопировать код класса Dollar и заменить Dollar на Franc.

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

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

1. Написать тест.

2. Добиться его безошибочной компиляции.

3. Запустить тест и убедиться, что он потерпел неудачу.

4. Добиться успешного выполнения теста.

5. Устранить дублирование.

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

Первые три фазы цикла разработки TDD должны выполняться как можно быстрее. Определяющая характеристика этих этапов – скорость. На этих этапах в жертву скорости можно принести очень многое, в том числе чистоту дизайна. Честно говоря, сейчас я несколько волнуюсь. Я только что разрешил вам забыть о принципах хорошего дизайна. Представляю, как вы приходите к своим коллегам, подчиненным и во всеуслышание объявляете: «Кент сказал, что все эти разговоры про хороший дизайн – полная ерунда!» Остановитесь. Цикл еще не закончен. Четырехногий уродец из благородного семейства пятиногих стульев вечно падает. Первые четыре шага нашего цикла не работают без пятого. Хороший дизайн в подходящее время! Сначала сделаем, чтобы код заработал, потом сделаем, чтобы код был правильным (make it run, make it right).

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


Franc

class Franc { private int amount;


Franc(int amount) {

this.amount = amount;

}


Franc times(int multiplier) {

return new Franc(amount * multiplier);

}


public boolean equals(Object object) {

Franc franc = (Franc) object;

return amount == franc.amount;

}

}

$5 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 10 CHF

Дублирование Dollar/Franc

Общие операции equals()

Общие операции times()


Чтобы запустить код, нам не потребовалось прикладывать каких-либо усилий, поэтому мы смогли «перепрыгнуть» через этап «добиться безошибочной компиляции кода» (Make it compile).

Зато теперь в нашем коде полно повторяющихся фрагментов. Прежде чем приступить к разработке следующего теста, мы должны избавиться от дублирования. Думаю, что следует начать с обобщения метода equals(). Однако об этом в следующей главе. На текущий момент мы можем вычеркнуть из нашего списка еще один пункт, однако вместе с этим нам придется добавить в него два дополнительных пункта.

В данной главе мы

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

□ создали код теста путем бесстыдного копирования и редактирования;

□ хуже того, добились успешного выполнения теста путем копирования и редактирования разработанного ранее кода;

□ дали себе обещание ни в коем случае не уходить домой до тех пор, пока не устраним дублирование.

6. Равенство для всех, вторая серия

$5 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 10 CHF

Дублирование Dollar/Franc


Общие операции equals()

Общие операции times()


В книге Crossing to Safety автор Вэлленц Стегнер (Wallance Stegner) описывает рабочее место одного из персонажей. Каждый инструмент находится на предназначенном для него месте, пол чисто вымыт и подметен, повсюду превосходный порядок и чистота. Однако чтобы добиться подобного положения вещей, персонаж не делал никаких специальных подготовительных процедур. «Подготовка была делом всей его жизни. Он подготавливался, затем убирался на рабочем месте». (Конец книги заставил меня громко рассмеяться в бизнес-классе трансатлантического «Боинга-747». Так что если решите ее прочитать, читайте с осторожностью.)

В главе 5 мы добились успешного выполнения теста. Это значит, что требуемая функциональность реализована. Однако чтобы сделать это быстро, нам пришлось продублировать огромный объем кода. Теперь пришло время убрать за собой.

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


Рис. 6.1. Общий суперкласс для двух разработанных нами классов


Для начала попробуем реализовать в базовом классе Money общий для обоих производных классов метод equals(). Начнем с малого:


Money

class Money


Запустим тесты – они по-прежнему выполняются. Конечно же, мы пока не сделали ничего такого, что нарушило бы выполнение наших тестов, однако в любом случае лишний раз запустить тесты не помешает. Теперь попробуем сделать класс Dollar производным от класса Money:


Dollar

class Dollar extends Money {

private int amount;

}


Работают ли тесты? Работают. Можем двигаться дальше. Перемещаем переменную amount в класс Money:


Money

class Money {

protected int amount;

}


Dollar

class Dollar extends Money {

}


Режим видимости переменной amount потребовалось изменить: теперь вместо private используем модификатор доступа protected. В противном случае подкласс не сможет обратиться к этой переменной. (Если бы мы хотели двигаться еще медленнее, мы могли бы на первом шаге объявить переменную в классе Money, а на втором шаге удалить ее объявление из класса Dollar, однако я решил действовать смело и решительно.)

Теперь можно переместить код метода equals() вверх по иерархии классов, то есть в класс Money. Прежде всего мы изменим объявление временной переменной:


Dollar

public boolean equals(Object object) {

Money dollar = (Dollar) object;

return amount == dollar.amount;

}


Все тесты по-прежнему работают. Теперь попробуем изменить приведение типа.


Dollar

public boolean equals(Object object) {

Money dollar = (Money) object;

return amount == dollar.amount;

}


Чтобы исходный код получился более осмысленным, изменим имя временной переменной:


Dollar

public boolean equals(Object object) {

Money money = (Money) object;

return amount == money.amount;

}


Теперь переместим метод из класса Dollar в класс Money:


Money

public boolean equals(Object object) {

Money money= (Money) object;

return amount == money.amount;

}


Теперь настало время удалить метод Franc.equals(). Прежде всего мы обнаруживаем, что у нас до сих пор нет теста, проверяющего равенство двух объектов класса Franc, – когда мы, особо не раздумывая, дублировали код класса Dollar, мы нагрешили еще больше, чем думали. Поэтому, прежде чем модифицировать код, мы должны написать все необходимые тесты.

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

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

К счастью, в нашем случае написать тесты совсем несложно. Для этого достаточно скопировать и немножко отредактировать тесты для класса Dollar:


public void testEquality() {

assertTrue(new Dollar(5).equals(new Dollar(5)));

assertFalse(new Dollar(5).equals(new Dollar(6)));

assertTrue(new Franc(5).equals(new Franc(5)));

assertFalse(new Franc(5).equals(new Franc(6)));

}


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

Теперь, когда тесты на месте, мы можем сделать класс Franc производным от класса Money:


Franc

class Franc extends Money {

private int amount;

}


Далее мы можем уничтожить поле amount в классе Franc, так как это значение будет храниться в одноименном поле класса Money:


Franc

class Franc extends Money {

}


Метод Franc.equals() выглядит фактически так же, как и метод Money.equals(). Сделав их абсолютно одинаковыми, мы сможем удалить реализацию этого метода из класса Franc. При этом смысл нашей программы не изменится. Для начала изменим объявление временной переменной:


Franc

public boolean equals(Object object) {

Money franc = (Franc) object;

return amount == franc.amount;

}


После этого изменим операцию преобразования типа:


Franc

public boolean equals(Object object) {

Money franc = (Money) object;

return amount == franc.amount;

}


Теперь, даже не меняя имя временной переменной, можно видеть, что метод получился фактически таким же, как одноименный метод в классе Money. Однако для пущей уверенности переименуем временную переменную:


Franc

public boolean equals(Object object) {

Money money = (Money) object;

return amount == money.amount;

}

$5 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 10 CHF

Дублирование Dollar/Franc

Общие операции equals()

Общие операции times()

Сравнение франков (Franc) и долларов (Dollar)


Теперь нет никакой разницы между методами Franc.equals() и Money.equals(), и мы можем удалить избыточную реализацию этого метода из класса Franc. Запускаем тесты. Они выполняются успешно.

Что должно происходить при сравнении франков и долларов? Мы рассмотрим этот вопрос в главе 7.

В данной главе мы

□ поэтапно переместили общий код из одного класса (Dollar) в суперкласс (Money);

□ сделали второй класс (Franc) подклассом общего суперкласса (Money);

□ унифицировали две реализации метода equals() и удалили избыточную реализацию в классе Franc.

7. Яблоки и апельсины

$5 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 10 CHF

Дублирование Dollar/Franc

Общие операции equals()

Общие операции times()

Сравнение франков (Franc) и долларов (Dollar)


В конце главы 6 перед нами встал интересный вопрос: что будет, если мы сравним франки и доллары? Мы немедленно добавили соответствующий пункт в список предстоящих задач. Нам никак не избавиться от этой мысли. И в самом деле, что произойдет?


public void testEquality() {

assertTrue(new Dollar(5).equals(new Dollar(5)));

assertFalse(new Dollar(5).equals(new Dollar(6)));

assertTrue(new Franc(5).equals(new Franc(5)));

assertFalse(new Franc(5).equals(new Franc(6)));

assertFalse(new Franc(5).equals(new Dollar(5)));

}


Тест завершается неудачей. С точки зрения написанного кода доллары – это франки. Прежде чем у наших швейцарских клиентов глаза вылезут на лоб, давайте попробуем исправить код. Код сравнения двух денежных значений должен убедиться в том, что он не сравнивает доллары с франками. Для этого мы должны проверить классы сравниваемых объектов – два объекта класса Money считаются равными только в том случае, если у них равны значения amount и классы.


public boolean equals(Object object) {

Money money = (Money) object;

return amount == money.amount

&& getClass().equals(money.getClass());

}


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


$5 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 10 CHF

Дублирование Dollar/Franc

Общие операции equals()

Общие операции times()

Сравнение франков (Franc) и долларов (Dollar)

Валюта?


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

□ мы превратили мучающее нас сомнение в тест;

□ добились успешного выполнения теста приемлемым, но не идеальным способом – getClass();

□ решили не добавлять в программу дополнительной логики, пока у нас не появится более весомая мотивация.

8. Создание объектов

$5 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 10 CHF

Дублирование Dollar/Franc

Общие операции equals()

Общие операции times()

Сравнение франков (Franc) и долларов (Dollar)

Валюта?


Две разные реализации метода times() выглядят на удивление похоже:


Franc

Franc times(int multiplier) {

return new Franc(amount * multiplier)

}

Dollar

Dollar times(int multiplier) {

return new Dollar(amount * multiplier)

}


Мы можем сделать их еще более похожими, изменив тип возвращаемого значения на Money:


Franc

Money times(int multiplier) {

return new Franc(amount * multiplier)

}

Dollar

Money times(int multiplier) {

return new Dollar(amount * multiplier)

}


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

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


public void testMultiplication() {

Dollar five = Money.dollar(5);

assertEquals(new Dollar(10), five.times(2));

assertEquals(new Dollar(15), five.times(3));

}


Реализация этого метода создает объект класса Dollar и возвращает его:


Money

static Dollar dollar(int amount) {

return new Dollar(amount);

}


Однако мы хотим избавиться от ссылок на Dollar, поэтому изменим объявление переменной в коде теста:


public void testMultiplication() {

Money five = Money.dollar(5);

assertEquals(new Dollar(10), five.times(2));

assertEquals(new Dollar(15), five.times(3));

}


Компилятор вежливо сообщает нам, что метод times() в классе Money не определен. На текущий момент мы не можем реализовать его, поэтому объявим класс Money абстрактным (может быть, с этого стоило начать?) и объявим также абстрактным метод Money.times():


Money

abstract class Money

abstract Money times(int multiplier);


Теперь мы можем изменить объявление фабричного метода:


Money

static Money dollar(int amount) {

return new Dollar(amount);

}


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


public void testMultiplication() {

Money five = Money.dollar(5);

assertEquals(Money.dollar(10), five.times(2));

assertEquals(Money.dollar(15), five.times(3));

}

public void testEquality() {

assertTrue(Money.dollar(5).equals(Money.dollar(5)));

assertFalse(Money.dollar(5).equals(Money.dollar(6)));

assertTrue(new Franc(5).equals(new Franc(5)));

assertFalse(new Franc(5).equals(new Franc(6)));

assertFalse(new Franc(5).equals(Money.dollar(5)));

}


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

Прежде чем механически исправлять код теста testFrancMultiplication(), обратите внимание, что теперь он не тестирует никакой логики, кроме той, что уже протестирована функцией testMultiplication(). Напрашивается вопрос: нужна ли нам функция testFrancMultiplication()? Если мы удалим этот тест, потеряем ли мы уверенность в нашем коде? Похоже, что нет, однако мы все же сохраним пока этот тест просто так – на всякий случай.


public void testEquality() {

assertTrue(Money.dollar(5).equals(Money.dollar(5)));

assertFalse(Money.dollar(5).equals(Money.dollar(6)));

assertTrue(Money.franc(5).equals(Money.franc(5)));

assertFalse(Money.franc(5).equals(Money.franc(6)));

assertFalse(Money.franc(5).equals(Money.dollar(5)));

}

public void testFrancMultiplication() {

Money five = Money.franc(5);

assertEquals(Money.franc(10), five.times(2));

assertEquals(Money.franc(15), five.times(3));

}


Реализация метода Money.franc() почти такая же, как и реализация метода Money.dollar():


Money

static Money franc(int amount) {

return new Franc(amount);

}

$5 + 10 CHF = $10, если курс обмена 2:1

$5 * 2 = $10

Сделать переменную amount закрытым (private) членом

Побочные эффекты в классе Dollar?

Округление денежных величин?

equals()

hashCode()

Равенство значению null

Равенство объектов

5 CHF * 2 = 10 CHF

Дублирование Dollar/Franc

Общие операции equals()

Общие операции times()

Сравнение франков (Franc) и долларов (Dollar)

Валюта?

Нужен ли тест testFrancMultiplication()?


Далее мы планируем перейти к устранению дублирования в методах times().

А сейчас вспомним, что в данной главе мы

□ сделали шаг на пути к устранению дублирования – сформировали общую сигнатуру для двух вариантов одного метода – times();

□ добавили объявление метода в общий суперкласс;

□ освободили тестовый код от ссылок на производные классы, для этого были созданы фабричные методы;

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


Страницы книги >> Предыдущая | 1 2 3 4 5 6 | Следующая
  • 4.6 Оценок: 5

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

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

Читателям!

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


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


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