понедельник, 28 ноября 2011 г.

Немного об ООП в контексте ObjectiveC, Qt и C++




Одновременно с ростом популярности поделок от компании Apple, растет интерес к языку программирования ObjectiveC, который, наряду с библиотекой ObjectiveC-классов Cocoa, широко применяется для программирования под современными платформами Mac OS X (персональные компьютеры от Apple) и iOS (портативные устройства от Apple).

Случилось так, что недавно мне подвернулась удача познакомиться с этим вплотную и, даже, неплохо подзаработать именно на программировании в ObjectiveC (Cocoa), используя популярную в Mac OS X бесплатную среду разработки Xcode.

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

Итак, по порядку. Начнем с общих вопросов объектно-ориентированного программирования (ООП).

Современный стереотип ООП


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

Таково, на мой взгляд, типичное, и, я бы добавил, - стереотипное, представление об ООП сегодня. Более того, в большинстве книг, посвященных языкам Java, Си++ или современным вариантам Pascal, при обсуждении вопросов ООП, согласно моему опыту, не касаются других вопросов. Я немного сомневаюсь насчет языка C#, так как недостаточно хорошо его знаю, но, видимо, там все тоже самое. Отсюда и стереотип относительно ООП, утверждённый в наших головах изучением этих популярных сегодня языков программирования.

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

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

А взаимодействие объектов, как говорит наука, может быть в стиле function-oriented и в стиле message-oriented. Последний вариант взаимодействия объектов, по мнению этой науки, более кошерный, но он не нашел применения в языках Java, C++ и современных объектно-ориентированных диалектах Pascal. Однако, именно message-oriented стиль общения объектов используется в ObjectiveC и является вариантом общения в Qt.

Рассмотрим эти стили взаимодействия объектов более подробно.

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

class A {
public:
  void fun_1();
private:
  void fun_2();
};

A *a = new A();

a->fun_1(); // Напрямую обратились к public-методу fun_1() объекта a, 
            // созданного по образу и подобию класса A.
a->fun_2(); // Так делать нельзя!!! Метод fun_2() закрыт правилами инкапсуляции
            // для внешнего использования.

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

Серьёзным недостатком данного способа общения объектов является возможная проблема невалидности указателя на объект класса, что не редкость для больших программ со сложным расчётом времени жизни объектов. Наверное все программисты сталкивались с тем, что программа пытается обратиться к методу объекта после того, как объект был уничтожен. Как правило, такая ситуация приводит к падению программы, что не нравится ни заказчикам, ни разработчикам.

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

Способ совершенно безопасный, но требует поддержки в виде реализации некоторого почтового диспетчера в виде специального run-time объекта. Недостатком этого способа можно сразу назвать потери времени, требуемые на работу почтового диспетчера при каждом обращении к объектам.

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

Теперь, пришло время рассмотреть две известные реализации message-oriented взаимодействия, которые были построены для расширения языков C и C++. Речь, идет об ObjectiveC и Qt, соответственно.

Сообщения в ObjectiveC


ObjectiveC был создан в начале 80-х годов 20-го столетия Бредом Коксом (Brad Cox) в стиле минимальных расширений к языку C, целью которых была реализация поддержки ООП в варианте, навеянном языком Smalltalk.

Тут сразу можно заметить ещё одну существенную разницу между языками C++ и ObjectiveC. На этот раз, не с точки зрения взаимодействия объектов, а с точки зрения совместимости с самим C. Для ObjectiveC справедливо правило: "Любая программа на языке C является программой на языке ObjectiveC.". Для C++ последнее несправедливо.

Разумеется, если мы, по прошествии более 30 лет с момента появления ObjectiveC, говорим о нем как о языке, который является фактически основным языком программирования на популярных платформах от Apple, то понятно, что у него была относительно успешная история. Сегодня мне известны две реализации ObjectiveC: одна из них используется в Apple вместе с важнейшей для "яблочного" программотворчества библиотекой классов Cocoa, а вторая входит в состав коллекции компиляторов GNU и может быть установлена для работы в любом *nix. Вариант ObjectiveC от Apple можно тоже попробовать бесплатно, если использовать популярную для Mac OS X среду разработки Xcode.

Если вам интересно узнать историю языка и поверхностные подробности его синтаксиса и семантики, то, я бы посоветовал отличную статью в википедии по адресу http://ru.wikipedia.org/wiki/Objective-C. Здесь мы не будем повторяться и поговорим лишь о взаимодействии объектов.

В языке ObjectiveC, если у нас есть объект a, то мы можем послать ему сообщение message, например, таким образом:

[a message];

Для простоты даже не будем говорить о передаче параметров в сообщение, а поговорим только о том, как это работает. В составе средств ObjectiveC имеется специальная система run-time, в которой есть диспетчер, обрабатывающий все сообщения направляющиеся объектам. В рамках правил языка определяются жизненные циклы объектов, которые во всех подробностях известны этому диспетчеру. Таким образом диспетчер точно знает, существует ли в данный момент объект, которому назначается сообщение или нет. Кроме того, диспетчер знает о том, какие сообщения и с какими параметрами принимает каждый объект учитывая всю иерархию классов объекта. Таким образом, диспетчер знает, может ли данный объект обработать данное сообщение. В случае, если что-то не так, то сообщение переадресуется обратно отправителю. Если все нормально, то объект получает предназначенное ему сообщение. Теоретически, вы можете отправлять любую строку соответствующую синтаксису сообщения - диспетчер разберётся что с ней делать.

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

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

Сообщения в Qt


На вопрос о том, что такое Qt, ответить непросто. Верно лишь то, что неправильно рассматривать Qt просто как обширную систему классов языка C++ для создания кроссплатформенных графических интерфейсов для разных операционных систем и устройств (сегодня это более 5000 классов и 9000 функций).

Неправильно это по двум серьезным причинам.

Во-первых, код Qt написан не на языке С++, а на специальном расширении этого языка, поддерживающим предварительную обработку кода специальным метакомпилятором (утилита moc - Meta Object Compiler). Правильным будет еще упомянуть утилиту uic (User Interface Compiler), которая преобразует некое XML-описание форм графического интерфейса, созданное графическим редактором форм, в код на Qt. Утилитой uic я не пользуюсь, предпочитая описывать интерфейс пользователя самостоятельно в коде программы.

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

История Qt, для меня, тесно пересекается с историей Java (0ak). Обе платформы были задуманы примерно в одинаковое время - в начале 90-х годов 20-го столетия. Наверное, годом рождения обоих идей и начальной реализацией можно назвать 1991 год. Кроме того, библиотеки классов, относящиеся к Qt и Java SE, на мой взгляд, многим похожи. Одна идея кроссплатформенна (Qt, попытка расширить ООП концепции языка C++), а вторая межплатформенна (Java, возможность реализовать ООП без наследия неудач C++).

Основателями Qt были программисты Хаавард Норд и Эрик Чамбенг. Первый, вскоре стал главным управляющим, а второй - президентом организованной ими компании Trolltech, которая и занималась развитием и продвижением Qt до января 2008 года, когда компания была выкуплена Nokia и была переименована в "Qt Software". Значение Qt в современном мире программирования трудно переоценить, о чем можно легко найти в Интернет много информации по запросам типа "история Qt".

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

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

Приведем пример описания сигналов.

signals:
  void clicked();
  void itemChanged(int index);

Здесь описаны сигналы с разными сигнатурами. Сигнал clicked() не передает в себе параметров, а сигнал itemChanged(int index) передает целое значение.

Чтобы выполнить эмиссию сигнала в коде объекта, в котором сигнал описан, надо написать так:
emit clicked();
  ...
  int x = ...
  emit itemChanged(x);

Теперь, если некий объект, написанный в стиле Qt, хочет получить возможность реагировать на сигналы определённой сигнатуры, то он должен описать слот указанной сигнатуры.

Приведем пример того, как можно оформить слоты для описанных выше сигналов.

public slots:
  void first();
  void second(int index);

С точки зрения языка Си++, слоты являются обычными методами, т.е. где-то должна быть написана реализация этих методов. Более того, эти методы могут быть вызваны обычным образом, как и любой другой метод класса, учитывая правила инкапсуляции. Т.е. слоты, также как и другие члены класса, могут быть описаны с квалификаторами области видимости public, protected или private, только к ним добавляется ключевое слово slots, обрабатываемое метакомпилятором.

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

connect(pObjectA, SIGNAL(clicked()), pObjectB, SLOT(first()));
connect(pObjectA, SIGNAL(itemChanged(int)), pObjectB, SLOT(second(int)));

Связь сигнал-слот является связью многие ко многим. Наверное, это то, что существенно отличает его от электронного аналога, где подключение нескольких выходов на один вход является задачей нетривиальной.

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

Сравнение ObjectiveC и Qt


Я бы выделил три существенных различия в реализации message-oriented взаимодействия в ObjectiveC и Qt.

1. Для ObjectiveC передача сообщения является единственным способом обращения к методу объекта. Обращаться к полям объекта можно безо всяких фокусов, но такое обращение, в общем случае, некошерно, учитывая отсутствие надежных правил инкапсуляции данных в объектах ObjectiveC. Для объектов Qt общение через систему сигналов и слотов является не более чем расширением возможностей function-oriented возможностей взаимодействия объектов C++.

2. Сообщения ObjectiveC могут быть динамически созданы и переданы строкой, которая потом будет разобрана диспетчером сообщений, в то время как для Qt и сигналы и слоты должны быть описаны до этапа метакомпиляции.

3. Qt не имеет средств ускорения взаимодействия сигнал-слот за счет возможности запоминания хешированной сигнатуры сообщения.

Отдельно хочется заметить общие особенности между ObjectiveC и Ot. Оба эти артефакта расширяют существующие популярные языки программирования. ObjectiveC расширяет язык C, а Qt расширяет язык C++. В обоих случаях это расширение, фактически представлено предварительной метаобработкой кода и runtime поддержкой полученного кода при его исполнении специальными диспетчерами. Так же как "код на языке C является кодом для ObjectiveC", так же "код на языке C++ является кодом для Qt". Последнее свойство Qt я последнее время активно использую в разработке на языке C++. Мне настолько нравится среда разработки QtCreator, которая используется для кроссплатформенной разработки в Qt, что я также использую её и для обычных кроссплатформенных разработок на языке C++, используя в качестве кроссплатформенных средств сборки qmake и cmake.

6 комментариев:

Игорь комментирует...

emit это не более чем синтаксический сахар, за ним ничего не стоит, это пустой define. Поэтому если Вы в своем коде опустите emit, ничего не изменится. emit нужен только для наглядности. Ну это я просто, как дополнение к статье написал :)

Сигналы и слоты есть и в boost. Есть еще легковесная библиотека libsigc++. Обе реализуют сигнал-слот взаимодействие посредством шаблонов. Если Вы и об этих реализациях напишите, будет очень здорово. :)

P.S. И да, Qt - это чистый с++.

knzsoft комментирует...

О сигналах и слотах в boost я не знал, также как и о существовании libsigc++. Приму к сведению. Спасибо :)

knzsoft комментирует...

Не совсем понял насчет замечания
>> P.S. И да, Qt - это чистый с++.
После moc-обработки мы получим код в чистом C++, но не раньше. Если Qt назвать языком, имея в виду характерные синтаксические расширения, то moc это то, что отделяет этот язык от C++.
Если не трудно, Игорь, то поясни этот момент подробнее.

Игорь комментирует...

Qt - это не новый язык программирования.

Разве запуск метокомпилятора как то модифицирует Ваш исходный код? Нет. Разве Qt добавляет новые синтаксические конструкции? Тоже нет. Всё что мы пишем, мы пишем на чистом с++. А moc просто создает некий код за нас, тоже на с++, избавляя нас от рутины.

Так что не нужно давать Qt новый статус, на которой он никогда и не претендовал.

knzsoft комментирует...

Я не рассматриваю Qt как новый язык программирования, я говорю тоньше - "если назвать Qt языком" и поясняю на что я опираю такую терминологическую возможность. В данном контексте, я считаю допустимым рассматривать документированные в Qt директивы компиляции как некое расширение языка, которое анализируется метакомпилятором. И конечно же, moc изменяет код исходной программы. Он не только делает изменения в листе компиляции, но и добавляет новые листы формируя moc.cpp файлы.

Думаю, что тут просто вопрос границ терминологии. Предмет для спора отсутствует. :)

Игорь комментирует...

moc не изменит ни одной строчки кода написанной Вами.

Да и только вдумайтесь в то что вы написали "код Qt написан не на языке С++, а на специальном расширении этого языка". Вашему воображению вижу просто нет границ. Поэтому дальше спорить тоже не буду :)