Skip to content
Alexandr Akulich edited this page Sep 21, 2017 · 4 revisions

Задачи

  1. Вынести write из методов в TextWriter
  2. Вынести реализацию writeData в FileWriter
  3. Добавить класс String
  4. Добавить const char *String::constData()
  5. Добавить в класс String операторы == и !=.
  6. Добавить оператор вывода String в TextWriter.
  7. Реализуйте BufferWriter
  8. Исправляем кривую архитектуру.
  9. В чём проблема?
  10. Задание
  11. Добавить в TextWriter setter/getter для устройства.

0) Перечитать код и исправить ошибки

Их всего две.

Было бы здорово сделать так, чтобы TextWriter мог писать вывод не только в консоль, но и, например, в какой-то буфер, который мы сможем как-то использовать в дальнейшем.

1) Вынести write из методов в TextWriter

Переписываем TextWriter так, чтобы вызов функции write был вынесен в отдельную функцию, вместе с такими деталями реализации, как данные о потоке (handle). В результате данного рефакторинга у вас в коде должно остаться ровно одно упоминание write(). Назовите функцию writeData() и сделайте так, чтобы её нельзя было вызвать "снаружи" (не из класса). После этого код, подобный cout.writeData(0, 0);, должен вызывать ошибку компиляции.

2) Вынести реализацию writeData в FileWriter

Из TextWriter выносим реализацию writeData() в новый класс FileWriter. Идея такая: в классе TextWriter у нас будет реализация операторов вывода различных типов переменных. При этом TextWriter не будет знать "детали реализации" вывода. Вместо этого мы создадим класс FileWriter - наследник TextWriter, в котором переопределим функцию writeData так, чтобы она вызывала системную функцию write и записывала данные в нужный поток (так же, как и раньше, переданный в конструктор). Разумеется, из лекции intuit вы помните про виртуальные методы. Это — как раз такой случай. Функция writeData() должна быть виртуальной, что позволит переопределять её в дочерних классах так, что методы родительского класса будут пользоваться именно переопределённой функцией. В общем, перед функций writeData() в TextWriter пишете ключевое слово virtual. Теперь при вызове этой функции будет происходить вызов последней переопределённой версии. Более того, давайте сделаем так, чтобы отсутствие переопределённой реализации writeData() вызывало ошибку компиляции. Для это вместо тела функции ( фигурных скобок с содержимым) нужно написать = 0. Итого: virtual void writeData(const void *data, int size) = 0; Класс TextWriter должен "забыть" о функции write и о номере потока handle. Это всё переходит в новый класс FileWriter.

3) Добавить класс String

Реализуем класс String, в который в дальнейшем будем выводить всё, записанное TextWriter'ом в буфер.

  • Предусмотрите место для 256 символов. Так же, как в аудитории, пока можете никак не обрабатывать ситуации, когда недостаточно места.
  • Добавьте стандартные методы, такие как size(), clear(), isEmpty(), оператор [](int index) и метод получения символа по индексу at(int index) (то же, что и [], но возвращает не ссылку, а копию "по значению").
  • Обратите внимание на функцию own_memcpy(). Предлагаю использовать её в реализации String.

Для проверки добавьте в main следующий код:

    String s1 = "Hello";
    String s2 = "world";

    s1 += " ";
    s1 += s2;

    s1 += '!';

    s1 += endl;

    s1 += 2123;

Советы по реализации:

  • Предлагаю использовать поле char m_buffer[256];
  • Для инициализации s1 и s2 нужно добавить конструктор String(const char *initStr).

4) Добавить const char *String::constData()

Для реализации вывода String в TextWriter предусмотрите в String публичный метод получения указателя на внутренний массив.

  • Сделайте внутренний массив совместимым со строками Си, то есть с нуль-терминированными const char *.

5) Добавить в класс String операторы == и !=.

Понадобятся для проверки работы остальных методов.

6) Добавить оператор вывода String в TextWriter.

Для проверки добавьте в main:

    cout << s1 << endl;

    cout << "String complex test: ";
    if (s1 != "Hello world!\n2123") {
        cout << "Fail!" << endl;
    } else {
        cout << "OK." << endl;
    }

    cout << "String isEmpty(): ";
    if (s1.isEmpty()) {
        cout << "Fail!" << endl;
    } else {
        cout << "OK." << endl;
    }
    s1.clear();

    cout << "String isEmpty() after clear(): ";
    if (!s1.isEmpty()) {
        cout << "Fail!" << endl;
    } else {
        cout << "OK." << endl;
    }

7) Реализуйте BufferWriter

BufferWriter — второй подкласс TextWriter'a.

  • Предлагаю использовать поле char m_buffer[256];
  • Вам понадобится запоминать "текущее положение" в буфере для того, чтобы писать следующие данные за предыдущими.
  • Можно сделать конструктор без аргументов.
  • Не забывайте инициализировать начальное "текущее положение" до начала записи данных. То есть: в конструкторе должна быть строчка вроде m_currentPos = 0;.
  • Добавьте метод toString(), который будет возвращать строку с содержимым буфера.
  • Предусмотрите метод reset() для сброса буфера.

Для проверки добавьте в main:

    BufferWriter bufferWriter;
    bufferWriter << "Text";
    bufferWriter << 747;

    String expected = "Text747";

    cout << "BufferWriter and String !=: ";
    if (bufferWriter.toString() != expected) {
        cout << "Fail!" << endl;
    } else {
        cout << "OK." << endl;
    }

8) Исправляем кривую "архитектуру".

В чём проблема?

Если вы хорошо подумаете о реализации класса BinWriter, который будет записывать данные не в текстовом, а в двоичном, то есть "буквальном" виде, то окажется, что нам придётся добавлять ещё BufferBinWriter и FileBinWriter. То есть при таком подходе у нас резко увеличивается количество классов. Добавление нового класса потоковой записи требует добавления новых классов, реализующих запись в файл и в буфер. Получается значительное дублирование и раздувание кода:

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

Если мы добавим к этому запись данных в сетевой сокет, нам придётся добавить ещё по одному классу для текста и двоичных данных. Это может быть особенно актуально для операционных систем, в которых сокет не доступен "как файл".

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

Назовём этот класс IODevice (устройство ввода-вывода). Такое название выбрано потому, что если в терминал и в файл, как правило, происходит только одно действие (например запись), то для сетевых сокетов часто необходимо осуществлять как запись, так и чтение.

Что нужно от этого класса? Класс будет состоять из единственной (на этот раз - публично доступной) функции writeData(). В базовом классе функция опять должна быть виртуальной, то есть пригодной к переопределению потомками. Кроме того, нужно сделать функцию обязательно переопределяемой. Для этого опять вместо реализации функции пишем, что она "равна нулю".

Задание

  • Измените реализацию класса TextWriter так, чтобы для записи он использовал переданный в конструктор указатель на устройство для записи.
  • Реализуйте дочерний для IODevice класс File, который будет осуществлять запись в файл (поток), переданный конструктору.
  • Реализуйте дочерний для IODevice класс Buffer, который будет осуществлять запись в собственный буфер, который можно получить в виде строки с помощью метода toString(). Так же в этот класс переходит метод reset().

После этого необходимо обновить определение cout:

-FileWriter cout(STDOUT_FILENO);
+File coutFile(STDOUT_FILENO);
+TextWriter cout(&coutFile);

и реализацию теста в функции main:

-    BufferWriter bufferWriter;
+    Buffer buffer;
+    TextWriter bufferWriter(&buffer);
     bufferWriter << "Text";
     bufferWriter << 747;

     String expected = "Text747";

-    cout << "BufferWriter and String !=: ";
-    if (bufferWriter.toString() != expected) {
+    cout << "Buffer, TextWriter and String !=: ";
+    if (buffer.toString() != expected) {
         cout << "Fail!" << endl;
     } else {
         cout << "OK." << endl;

9) Добавить в TextWriter setter/getter для устройства.

Реализуйте в TextWriter методы IODevice *device() и void setDevice(IODevice *device).

Для проверки добавьте в main:

    buffer.reset();

    cout.setDevice(&buffer);

    cout << "Buffer test" << endl;
    cout << 12345 << endl;

    cout.setDevice(&coutFile);

    String expectedResult = "Buffer test\n12345\n";

    cout << "TextWriter::setDevice() and String ==: ";
    if (buffer.toString() == expectedResult) {
        cout << "OK" << endl;
    } else {
        cout << "Fail" << endl;
        cout << buffer.toString();
    }