День 3: Пакеты, классы, тесты, отладчик

Posted on May 13, 2022

Содержание

  • Классы: обзор, создание, описание;
    • Кратчайшее введение в объектную систему Smalltalk;
  • Iceberg: Интеграция с Git;
  • Обзор System Browser. Переменные-члены.
  • Тесты: Написание и запуск;
  • Отладка: Чиним упавший тест;
  • Что делает Smalltalk особенным: #dnu;
  • Домашнее задание.

Классы: обзор, создание, описание

Кратчайшее введение в объектную систему Smalltalk

О создании классов:

  1. Всё – объект (просто напоминаю). Классы – тоже объекты.
  2. Как и в традиционных языках, классы определяют наполнение и поведение своих экземпляров (объектов).
  3. Поведение объектов определяется методами – их реакциями на сообщения. Методы реализуются классами.
  4. Классы могут образовывать иерархии наследования. В Смолтоке доступно только одиночное наследование: у класса может быть только один “родитель”. Множественного наследования не предусмотрено.
  5. Наследование – это сообщение. Классы создаются путем отправки сообщений.
    • Если есть сообщение, то есть и объект, которому оно было отправлено. Таким образом, у каждого класса есть свой родительский класс.

Об обработке сообщений:

  1. Когда объект получает сообщение, происходит процедура, известная как Method lookup: в классе объекта-получателя ищется метод, соответствующий данному сообщению.
  2. Если в данном классе не найден подходящий метод, поиск осуществляется в родительском классе – и так далее, пока метод не будет найден.
  3. Если в процессе поиска метода мы спутились до самого нижнего класса в иерархии наследования, Object, и там этот метод так же не найден, объект посылает сам себе сообщение #doesNotUnderstand: с деталями о необработанном сообщении.
  4. Реализация #doesNotUnderstand: по-умолчанию определена в классе Object и выбрасывает соответствующее исключение.

Ответы на вопросы для самых внимательных:

  1. Q: Если у каждого класса есть родитель, то где начинается иерархия классов?

    A: Иерархия классов действительно входит в цикл, однако принято считать, что она начинается от Object. На самом деле, в Pharo класс Object отнаследован от ProtoObject, который по коду отправляет сообщение наследования сам себе (почти!), а затем выставляет свой superclass в nil (который, в свою очередь, объект).

  2. Q: Если всё – объект, и классы тоже объекты, то объектами каких классов являются сами классы?

    A: В Смолтоке создаваемой иерархии классов соответсвует автоматически пополняемая иерархия метаклассов; каждый класс является объектом соответствующего ему метакласса. Иерархия метаклассов является параллельным отражением иерархии классов.

  3. Q: А объектами каких классов являются метаклассы?

    A: Каждый метакласс является объектом класса Metaclass. В том числе и метакласс самого класса Metaclass!

  4. Q: Что лежит в корне иерархии метаклассов?

    A: В корне иерархии метаклассов лежат классы Behavior и Class, которые уже отнаследованы от Object.

Образец иерархии классов и метаклассов
Образец иерархии классов и метаклассов

Общая информация о классах и метаклассах дана в книге Pharo By Example, из которой и взята иллюстрация выше. Реализация языков программирования на самих себе является древней забавой; процедура разрыва цикла курицы и яйца называется “бутстраппингом” и для Pharo этот процесс описан, например, здесь.

Iceberg: Интеграция с Git

  1. Открываем репозиторий для данного занятия и форкаем его в свой аккаунт.
  2. Открываем рабочий образ Pharo и клонируем свой форк:
    • Открываем Iceberg: Ctrl+O+I или Browse -> Iceberg;
    • Жмём Add, в списке слева выбираем “Clone from github.com”;
    • Вводим данные своего форка (название вашего аккаунта, проекта) и меняем протокол на HTTP*.
    • Нажимаем OK и скрещиваем пальцы.

* – на момент написания данного руководства (май 2022 г.) наблюдались проблемы в работе Айсберга с новыми ключами SSH, которые с недавних пор требует GitHub. Дело в том, что вы уже могли последовать рекомендациям GitHub и перегенерировать свои ключи SSH на более новые. Однако Pharo для работы с Git использует не консольный клиент git, который у вас наверняка работает без проблем, а библиотеку libgit собственной версии, слинкованную с библиотеками ssh/ssl тоже конкретных версий – которые, будучи зафиксированными, могут ещё не поддерживать ключи в новом формате. Из-за этого клонирование и пуш в репозитории по протоколу SSH через Айсберг могут быть недоступны. В этом случае pull/push можно сделать из консоли, сменив remote в репозитории, ранее скачанном и зарегистрированном в Айсберге: эти репозитории расположены в директории ~/Pharo/images/$IMAGE_NAME/pharo-local/iceberg.

Клонирование нового репозитория
Клонирование нового репозитория
  1. После того, как репозиторий будет загружен, в окне “Repositories” появится соответствующая строчка. Открываем этот репозиторий двойным щелчком и для пакетов StClassRus и StClassRus-Tests нажимаем Load в контекстном меню. Пакеты загружены в систему.

Внимание: Позже пакеты были переименованы в StClassRus-Day3 и StClassRus-Day3-Tests соответственно. В записи видео сохранились старые названия.

Обзор System Browser

(Обзор System Browser, навигация по классам загруженного пакета)

  1. Где наши пакеты? Как их отфильтровать.
  2. “Объявление” класса. Как выглядит, из чего состоит.
  3. Категории методов.
  4. Методы.
  5. Class side.

Подробнее о переменных-членах:

  1. Переменные-члены доступны только в методах класса. По аналогии с С++, считайте, что они всегда protected.
  2. Переменные-члены базового класса доступны в методах их дочерних классов.
  3. Извне получить доступ к внутренним переменным-членам объекта нельзя*, можно только спросить его об этом.
    • * на самом деле можно через рефлексию, но в повседневных задачах это не нужно.

Переменные-члены могут быть нескольких типов, определяющих их область видимости и доступности:

  • Instance variables – самый частый случай. Собственные переменные объекта. Каждый объект конкретного класса обладает своими независимыми instance variables.
  • Class instance variables – то же самое, но на стороне класса. Поскольку класс – это объект, он может обладать собственным состоянием и своими внутренними переменными. При наследовании класса и, следовательно, метакласса, в классах наследниках будет доступна своя копия такой переменной. Такие переменные могут быть использованы для реализации синглтонов или каких-либо глобальных счетчиков или пулов.
  • Class variables – являются разделяемыми (общими) переменными для всех объектов данного класса, и для самого класса, а так же их дочерним классам. Некий аналог static из С++. В отличие от предыдущих двух, такие переменные-члены принято именовать с большой буквы.

Главный хоткей в System browser: Ctrl+S.

  • Обзор класса STCEmailAddress.
  • Работа с классом STCEmailAddress.
  • Разбор overrides.

Тесты: Написание и запуск

Test Runner: Ctrl+O+U, Browse -> Test Runner.

  • Тесты - это классы;
  • Тесты - это Fixture (автоматом);
  • Как тесты интегрированы с кодом;

Отладка: Чиним упавший тест

Наша реализация не идеальна, и всегда можно написать такой тест, который с ней “упадёт”.

(Пишем новый тест, попадаем в отладчик, фиксим)

Что делает Smalltalk особенным: #dnu;

  • #doesNotUnderstand: – это сообщение, которое объект отправляет сам себе, когда не знает, как обработать входящее сообщение;
  • Все правила Method Lookup справедливы и в этом случае;
  • Как это использовать: реализуем декоратор.

(Живая демонстрация)

Домашнее задание

Попробуйте реализовать парсер адресов e-mail “по всем правилам”:

  • Правила перечислены в RFC3696;
  • В Смолтоке доступны комбинаторные парсеры, которые делают решение куда более гибким и элегантным:
    • Пакет для Pharo устанавливется как:

      Metacello new
          baseline: 'PetitParser';
          repository: 'github://moosetechnology/PetitParser:v3.x.x/src';
          load.

      Во избежание проблем с пуллом и ключами, описанными ранее, рекомендую выставить “Remote type” в “HTTPS” в глобальных настройках Айсберга:

Settings -> Iceberg
Settings -> Iceberg

В качестве отправной точки можно использовать такой набросок:

| emailAddress identifier localName hostName zone |
identifier := (#letter asParser / #digit asParser) plus flatten.

localName := identifier. "just reuse parser"

zone := $. asParser, identifier. "reuse parser, again"
hostName := (localName, zone) flatten.

emailAddress := (localName, $@ asParser, hostName)
    ==> [ :nodes | STCEmailAddress new
                            user: nodes first;
                            domain: nodes third;
                            yourself ].

emailAddress parse: 'user@example.com'
  • Задание “со звездочкой”: внедрить такой парсер и валидацию через него в основной класс, покрыть тестами.
Вернуться к курсу